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
241 # NOTE: we manually invalidate _last_read each time we do any
242 # modifications ourselves, otherwise we might get tripped up by
243 # bogus mtime behaviour on some systems (see issue #6896).
245 def add(self
, message
):
246 """Add message and return assigned key."""
247 tmp_file
= self
._create
_tmp
()
249 self
._dump
_message
(message
, tmp_file
)
251 _sync_close(tmp_file
)
252 if isinstance(message
, MaildirMessage
):
253 subdir
= message
.get_subdir()
254 suffix
= self
.colon
+ message
.get_info()
255 if suffix
== self
.colon
:
260 uniq
= os
.path
.basename(tmp_file
.name
).split(self
.colon
)[0]
261 dest
= os
.path
.join(self
._path
, subdir
, uniq
+ suffix
)
263 if hasattr(os
, 'link'):
264 os
.link(tmp_file
.name
, dest
)
265 os
.remove(tmp_file
.name
)
267 os
.rename(tmp_file
.name
, dest
)
269 os
.remove(tmp_file
.name
)
270 if e
.errno
== errno
.EEXIST
:
271 raise ExternalClashError('Name clash with existing message: %s'
275 if isinstance(message
, MaildirMessage
):
276 os
.utime(dest
, (os
.path
.getatime(dest
), message
.get_date()))
277 # Invalidate cached toc
278 self
._last
_read
= None
281 def remove(self
, key
):
282 """Remove the keyed message; raise KeyError if it doesn't exist."""
283 os
.remove(os
.path
.join(self
._path
, self
._lookup
(key
)))
284 # Invalidate cached toc (only on success)
285 self
._last
_read
= None
287 def discard(self
, key
):
288 """If the keyed message exists, remove it."""
289 # This overrides an inapplicable implementation in the superclass.
295 if e
.errno
!= errno
.ENOENT
:
298 def __setitem__(self
, key
, message
):
299 """Replace the keyed message; raise KeyError if it doesn't exist."""
300 old_subpath
= self
._lookup
(key
)
301 temp_key
= self
.add(message
)
302 temp_subpath
= self
._lookup
(temp_key
)
303 if isinstance(message
, MaildirMessage
):
304 # temp's subdir and suffix were specified by message.
305 dominant_subpath
= temp_subpath
307 # temp's subdir and suffix were defaults from add().
308 dominant_subpath
= old_subpath
309 subdir
= os
.path
.dirname(dominant_subpath
)
310 if self
.colon
in dominant_subpath
:
311 suffix
= self
.colon
+ dominant_subpath
.split(self
.colon
)[-1]
315 new_path
= os
.path
.join(self
._path
, subdir
, key
+ suffix
)
316 os
.rename(os
.path
.join(self
._path
, temp_subpath
), new_path
)
317 if isinstance(message
, MaildirMessage
):
318 os
.utime(new_path
, (os
.path
.getatime(new_path
),
320 # Invalidate cached toc
321 self
._last
_read
= None
323 def get_message(self
, key
):
324 """Return a Message representation or raise a KeyError."""
325 subpath
= self
._lookup
(key
)
326 f
= open(os
.path
.join(self
._path
, subpath
), 'r')
329 msg
= self
._factory
(f
)
331 msg
= MaildirMessage(f
)
334 subdir
, name
= os
.path
.split(subpath
)
335 msg
.set_subdir(subdir
)
336 if self
.colon
in name
:
337 msg
.set_info(name
.split(self
.colon
)[-1])
338 msg
.set_date(os
.path
.getmtime(os
.path
.join(self
._path
, subpath
)))
341 def get_string(self
, key
):
342 """Return a string representation or raise a KeyError."""
343 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r')
349 def get_file(self
, key
):
350 """Return a file-like representation or raise a KeyError."""
351 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'rb')
355 """Return an iterator over keys."""
357 for key
in self
._toc
:
364 def has_key(self
, key
):
365 """Return True if the keyed message exists, False otherwise."""
367 return key
in self
._toc
370 """Return a count of messages in the mailbox."""
372 return len(self
._toc
)
375 """Write any pending changes to disk."""
376 # Maildir changes are always written immediately, so there's nothing
377 # to do except invalidate our cached toc.
378 self
._last
_read
= None
381 """Lock the mailbox."""
385 """Unlock the mailbox if it is locked."""
389 """Flush and close the mailbox."""
392 def list_folders(self
):
393 """Return a list of folder names."""
395 for entry
in os
.listdir(self
._path
):
396 if len(entry
) > 1 and entry
[0] == '.' and \
397 os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
398 result
.append(entry
[1:])
401 def get_folder(self
, folder
):
402 """Return a Maildir instance for the named folder."""
403 return Maildir(os
.path
.join(self
._path
, '.' + folder
),
404 factory
=self
._factory
,
407 def add_folder(self
, folder
):
408 """Create a folder and return a Maildir instance representing it."""
409 path
= os
.path
.join(self
._path
, '.' + folder
)
410 result
= Maildir(path
, factory
=self
._factory
)
411 maildirfolder_path
= os
.path
.join(path
, 'maildirfolder')
412 if not os
.path
.exists(maildirfolder_path
):
413 os
.close(os
.open(maildirfolder_path
, os
.O_CREAT | os
.O_WRONLY
,
417 def remove_folder(self
, folder
):
418 """Delete the named folder, which must be empty."""
419 path
= os
.path
.join(self
._path
, '.' + folder
)
420 for entry
in os
.listdir(os
.path
.join(path
, 'new')) + \
421 os
.listdir(os
.path
.join(path
, 'cur')):
422 if len(entry
) < 1 or entry
[0] != '.':
423 raise NotEmptyError('Folder contains message(s): %s' % folder
)
424 for entry
in os
.listdir(path
):
425 if entry
!= 'new' and entry
!= 'cur' and entry
!= 'tmp' and \
426 os
.path
.isdir(os
.path
.join(path
, entry
)):
427 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
429 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
431 os
.remove(os
.path
.join(root
, entry
))
433 os
.rmdir(os
.path
.join(root
, entry
))
437 """Delete old files in "tmp"."""
439 for entry
in os
.listdir(os
.path
.join(self
._path
, 'tmp')):
440 path
= os
.path
.join(self
._path
, 'tmp', entry
)
441 if now
- os
.path
.getatime(path
) > 129600: # 60 * 60 * 36
444 _count
= 1 # This is used to generate unique file names.
446 def _create_tmp(self
):
447 """Create a file in the tmp subdirectory and open and return it."""
449 hostname
= socket
.gethostname()
451 hostname
= hostname
.replace('/', r
'\057')
453 hostname
= hostname
.replace(':', r
'\072')
454 uniq
= "%s.M%sP%sQ%s.%s" % (int(now
), int(now
% 1 * 1e6
), os
.getpid(),
455 Maildir
._count
, hostname
)
456 path
= os
.path
.join(self
._path
, 'tmp', uniq
)
460 if e
.errno
== errno
.ENOENT
:
463 return _create_carefully(path
)
465 if e
.errno
!= errno
.EEXIST
:
470 # Fall through to here if stat succeeded or open raised EEXIST.
471 raise ExternalClashError('Name clash prevented file creation: %s' %
475 """Update table of contents mapping."""
476 if self
._last
_read
is not None:
477 for subdir
in ('new', 'cur'):
478 mtime
= os
.path
.getmtime(os
.path
.join(self
._path
, subdir
))
479 if mtime
> self
._last
_read
:
484 # We record the current time - 1sec so that, if _refresh() is called
485 # again in the same second, we will always re-read the mailbox
486 # just in case it's been modified. (os.path.mtime() only has
487 # 1sec resolution.) This results in a few unnecessary re-reads
488 # when _refresh() is called multiple times in the same second,
489 # but once the clock ticks over, we will only re-read as needed.
490 now
= time
.time() - 1
493 def update_dir (subdir
):
494 path
= os
.path
.join(self
._path
, subdir
)
495 for entry
in os
.listdir(path
):
496 p
= os
.path
.join(path
, entry
)
499 uniq
= entry
.split(self
.colon
)[0]
500 self
._toc
[uniq
] = os
.path
.join(subdir
, entry
)
505 self
._last
_read
= now
507 def _lookup(self
, key
):
508 """Use TOC to return subpath for given key, or raise a KeyError."""
510 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
511 return self
._toc
[key
]
516 return self
._toc
[key
]
518 raise KeyError('No message with key: %s' % key
)
520 # This method is for backward compatibility only.
522 """Return the next message in a one-time iteration."""
523 if not hasattr(self
, '_onetime_keys'):
524 self
._onetime
_keys
= self
.iterkeys()
527 return self
[self
._onetime
_keys
.next()]
528 except StopIteration:
534 class _singlefileMailbox(Mailbox
):
535 """A single-file mailbox."""
537 def __init__(self
, path
, factory
=None, create
=True):
538 """Initialize a single-file mailbox."""
539 Mailbox
.__init
__(self
, path
, factory
, create
)
541 f
= open(self
._path
, 'rb+')
543 if e
.errno
== errno
.ENOENT
:
545 f
= open(self
._path
, 'wb+')
547 raise NoSuchMailboxError(self
._path
)
548 elif e
.errno
== errno
.EACCES
:
549 f
= open(self
._path
, 'rb')
555 self
._pending
= False # No changes require rewriting the file.
557 self
._file
_length
= None # Used to record mailbox size
559 def add(self
, message
):
560 """Add message and return assigned key."""
562 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
565 return self
._next
_key
- 1
567 def remove(self
, key
):
568 """Remove the keyed message; raise KeyError if it doesn't exist."""
573 def __setitem__(self
, key
, message
):
574 """Replace the keyed message; raise KeyError if it doesn't exist."""
576 self
._toc
[key
] = self
._append
_message
(message
)
580 """Return an iterator over keys."""
582 for key
in self
._toc
.keys():
585 def has_key(self
, key
):
586 """Return True if the keyed message exists, False otherwise."""
588 return key
in self
._toc
591 """Return a count of messages in the mailbox."""
593 return len(self
._toc
)
596 """Lock the mailbox."""
598 _lock_file(self
._file
)
602 """Unlock the mailbox if it is locked."""
604 _unlock_file(self
._file
)
608 """Write any pending changes to disk."""
609 if not self
._pending
:
612 # In order to be writing anything out at all, self._toc must
613 # already have been generated (and presumably has been modified
614 # by adding or deleting an item).
615 assert self
._toc
is not None
617 # Check length of self._file; if it's changed, some other process
618 # has modified the mailbox since we scanned it.
619 self
._file
.seek(0, 2)
620 cur_len
= self
._file
.tell()
621 if cur_len
!= self
._file
_length
:
622 raise ExternalClashError('Size of mailbox file changed '
623 '(expected %i, found %i)' %
624 (self
._file
_length
, cur_len
))
626 new_file
= _create_temporary(self
._path
)
629 self
._pre
_mailbox
_hook
(new_file
)
630 for key
in sorted(self
._toc
.keys()):
631 start
, stop
= self
._toc
[key
]
632 self
._file
.seek(start
)
633 self
._pre
_message
_hook
(new_file
)
634 new_start
= new_file
.tell()
636 buffer = self
._file
.read(min(4096,
637 stop
- self
._file
.tell()))
640 new_file
.write(buffer)
641 new_toc
[key
] = (new_start
, new_file
.tell())
642 self
._post
_message
_hook
(new_file
)
645 os
.remove(new_file
.name
)
647 _sync_close(new_file
)
648 # self._file is about to get replaced, so no need to sync.
651 os
.rename(new_file
.name
, self
._path
)
653 if e
.errno
== errno
.EEXIST
or \
654 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
655 os
.remove(self
._path
)
656 os
.rename(new_file
.name
, self
._path
)
659 self
._file
= open(self
._path
, 'rb+')
661 self
._pending
= False
663 _lock_file(self
._file
, dotlock
=False)
665 def _pre_mailbox_hook(self
, f
):
666 """Called before writing the mailbox to file f."""
669 def _pre_message_hook(self
, f
):
670 """Called before writing each message to file f."""
673 def _post_message_hook(self
, f
):
674 """Called after writing each message to file f."""
678 """Flush and close the mailbox."""
682 self
._file
.close() # Sync has been done by self.flush() above.
684 def _lookup(self
, key
=None):
685 """Return (start, stop) or raise KeyError."""
686 if self
._toc
is None:
690 return self
._toc
[key
]
692 raise KeyError('No message with key: %s' % key
)
694 def _append_message(self
, message
):
695 """Append message to mailbox and return (start, stop) offsets."""
696 self
._file
.seek(0, 2)
697 self
._pre
_message
_hook
(self
._file
)
698 offsets
= self
._install
_message
(message
)
699 self
._post
_message
_hook
(self
._file
)
701 self
._file
_length
= self
._file
.tell() # Record current length of mailbox
706 class _mboxMMDF(_singlefileMailbox
):
707 """An mbox or MMDF mailbox."""
711 def get_message(self
, key
):
712 """Return a Message representation or raise a KeyError."""
713 start
, stop
= self
._lookup
(key
)
714 self
._file
.seek(start
)
715 from_line
= self
._file
.readline().replace(os
.linesep
, '')
716 string
= self
._file
.read(stop
- self
._file
.tell())
717 msg
= self
._message
_factory
(string
.replace(os
.linesep
, '\n'))
718 msg
.set_from(from_line
[5:])
721 def get_string(self
, key
, from_
=False):
722 """Return a string representation or raise a KeyError."""
723 start
, stop
= self
._lookup
(key
)
724 self
._file
.seek(start
)
726 self
._file
.readline()
727 string
= self
._file
.read(stop
- self
._file
.tell())
728 return string
.replace(os
.linesep
, '\n')
730 def get_file(self
, key
, from_
=False):
731 """Return a file-like representation or raise a KeyError."""
732 start
, stop
= self
._lookup
(key
)
733 self
._file
.seek(start
)
735 self
._file
.readline()
736 return _PartialFile(self
._file
, self
._file
.tell(), stop
)
738 def _install_message(self
, message
):
739 """Format a message and blindly write to self._file."""
741 if isinstance(message
, str) and message
.startswith('From '):
742 newline
= message
.find('\n')
744 from_line
= message
[:newline
]
745 message
= message
[newline
+ 1:]
749 elif isinstance(message
, _mboxMMDFMessage
):
750 from_line
= 'From ' + message
.get_from()
751 elif isinstance(message
, email
.message
.Message
):
752 from_line
= message
.get_unixfrom() # May be None.
753 if from_line
is None:
754 from_line
= 'From MAILER-DAEMON %s' % time
.asctime(time
.gmtime())
755 start
= self
._file
.tell()
756 self
._file
.write(from_line
+ os
.linesep
)
757 self
._dump
_message
(message
, self
._file
, self
._mangle
_from
_)
758 stop
= self
._file
.tell()
762 class mbox(_mboxMMDF
):
763 """A classic mbox mailbox."""
767 def __init__(self
, path
, factory
=None, create
=True):
768 """Initialize an mbox mailbox."""
769 self
._message
_factory
= mboxMessage
770 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
772 def _pre_message_hook(self
, f
):
773 """Called before writing each message to file f."""
777 def _generate_toc(self
):
778 """Generate key-to-(start, stop) table of contents."""
779 starts
, stops
= [], []
782 line_pos
= self
._file
.tell()
783 line
= self
._file
.readline()
784 if line
.startswith('From '):
785 if len(stops
) < len(starts
):
786 stops
.append(line_pos
- len(os
.linesep
))
787 starts
.append(line_pos
)
789 stops
.append(line_pos
)
791 self
._toc
= dict(enumerate(zip(starts
, stops
)))
792 self
._next
_key
= len(self
._toc
)
793 self
._file
_length
= self
._file
.tell()
796 class MMDF(_mboxMMDF
):
797 """An MMDF mailbox."""
799 def __init__(self
, path
, factory
=None, create
=True):
800 """Initialize an MMDF mailbox."""
801 self
._message
_factory
= MMDFMessage
802 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
804 def _pre_message_hook(self
, f
):
805 """Called before writing each message to file f."""
806 f
.write('\001\001\001\001' + os
.linesep
)
808 def _post_message_hook(self
, f
):
809 """Called after writing each message to file f."""
810 f
.write(os
.linesep
+ '\001\001\001\001' + os
.linesep
)
812 def _generate_toc(self
):
813 """Generate key-to-(start, stop) table of contents."""
814 starts
, stops
= [], []
819 line
= self
._file
.readline()
820 next_pos
= self
._file
.tell()
821 if line
.startswith('\001\001\001\001' + os
.linesep
):
822 starts
.append(next_pos
)
825 line
= self
._file
.readline()
826 next_pos
= self
._file
.tell()
827 if line
== '\001\001\001\001' + os
.linesep
:
828 stops
.append(line_pos
- len(os
.linesep
))
831 stops
.append(line_pos
)
835 self
._toc
= dict(enumerate(zip(starts
, stops
)))
836 self
._next
_key
= len(self
._toc
)
837 self
._file
.seek(0, 2)
838 self
._file
_length
= self
._file
.tell()
844 def __init__(self
, path
, factory
=None, create
=True):
845 """Initialize an MH instance."""
846 Mailbox
.__init
__(self
, path
, factory
, create
)
847 if not os
.path
.exists(self
._path
):
849 os
.mkdir(self
._path
, 0700)
850 os
.close(os
.open(os
.path
.join(self
._path
, '.mh_sequences'),
851 os
.O_CREAT | os
.O_EXCL | os
.O_WRONLY
, 0600))
853 raise NoSuchMailboxError(self
._path
)
856 def add(self
, message
):
857 """Add message and return assigned key."""
862 new_key
= max(keys
) + 1
863 new_path
= os
.path
.join(self
._path
, str(new_key
))
864 f
= _create_carefully(new_path
)
869 self
._dump
_message
(message
, f
)
870 if isinstance(message
, MHMessage
):
871 self
._dump
_sequences
(message
, new_key
)
879 def remove(self
, key
):
880 """Remove the keyed message; raise KeyError if it doesn't exist."""
881 path
= os
.path
.join(self
._path
, str(key
))
883 f
= open(path
, 'rb+')
885 if e
.errno
== errno
.ENOENT
:
886 raise KeyError('No message with key: %s' % key
)
894 os
.remove(os
.path
.join(self
._path
, str(key
)))
901 def __setitem__(self
, key
, message
):
902 """Replace the keyed message; raise KeyError if it doesn't exist."""
903 path
= os
.path
.join(self
._path
, str(key
))
905 f
= open(path
, 'rb+')
907 if e
.errno
== errno
.ENOENT
:
908 raise KeyError('No message with key: %s' % key
)
915 os
.close(os
.open(path
, os
.O_WRONLY | os
.O_TRUNC
))
916 self
._dump
_message
(message
, f
)
917 if isinstance(message
, MHMessage
):
918 self
._dump
_sequences
(message
, key
)
925 def get_message(self
, key
):
926 """Return a Message representation or raise a KeyError."""
929 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
931 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
933 if e
.errno
== errno
.ENOENT
:
934 raise KeyError('No message with key: %s' % key
)
947 for name
, key_list
in self
.get_sequences().iteritems():
949 msg
.add_sequence(name
)
952 def get_string(self
, key
):
953 """Return a string representation or raise a KeyError."""
956 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
958 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
960 if e
.errno
== errno
.ENOENT
:
961 raise KeyError('No message with key: %s' % key
)
975 def get_file(self
, key
):
976 """Return a file-like representation or raise a KeyError."""
978 f
= open(os
.path
.join(self
._path
, str(key
)), 'rb')
980 if e
.errno
== errno
.ENOENT
:
981 raise KeyError('No message with key: %s' % key
)
987 """Return an iterator over keys."""
988 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
991 def has_key(self
, key
):
992 """Return True if the keyed message exists, False otherwise."""
993 return os
.path
.exists(os
.path
.join(self
._path
, str(key
)))
996 """Return a count of messages in the mailbox."""
997 return len(list(self
.iterkeys()))
1000 """Lock the mailbox."""
1001 if not self
._locked
:
1002 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
1003 _lock_file(self
._file
)
1007 """Unlock the mailbox if it is locked."""
1009 _unlock_file(self
._file
)
1010 _sync_close(self
._file
)
1012 self
._locked
= False
1015 """Write any pending changes to the disk."""
1019 """Flush and close the mailbox."""
1023 def list_folders(self
):
1024 """Return a list of folder names."""
1026 for entry
in os
.listdir(self
._path
):
1027 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
1028 result
.append(entry
)
1031 def get_folder(self
, folder
):
1032 """Return an MH instance for the named folder."""
1033 return MH(os
.path
.join(self
._path
, folder
),
1034 factory
=self
._factory
, create
=False)
1036 def add_folder(self
, folder
):
1037 """Create a folder and return an MH instance representing it."""
1038 return MH(os
.path
.join(self
._path
, folder
),
1039 factory
=self
._factory
)
1041 def remove_folder(self
, folder
):
1042 """Delete the named folder, which must be empty."""
1043 path
= os
.path
.join(self
._path
, folder
)
1044 entries
= os
.listdir(path
)
1045 if entries
== ['.mh_sequences']:
1046 os
.remove(os
.path
.join(path
, '.mh_sequences'))
1050 raise NotEmptyError('Folder not empty: %s' % self
._path
)
1053 def get_sequences(self
):
1054 """Return a name-to-key-list dictionary to define each sequence."""
1056 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r')
1058 all_keys
= set(self
.keys())
1061 name
, contents
= line
.split(':')
1063 for spec
in contents
.split():
1067 start
, stop
= (int(x
) for x
in spec
.split('-'))
1068 keys
.update(range(start
, stop
+ 1))
1069 results
[name
] = [key
for key
in sorted(keys
) \
1071 if len(results
[name
]) == 0:
1074 raise FormatError('Invalid sequence specification: %s' %
1080 def set_sequences(self
, sequences
):
1081 """Set sequences using the given name-to-key-list dictionary."""
1082 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r+')
1084 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
1085 for name
, keys
in sequences
.iteritems():
1088 f
.write('%s:' % name
)
1091 for key
in sorted(set(keys
)):
1098 f
.write('%s %s' % (prev
, key
))
1100 f
.write(' %s' % key
)
1103 f
.write(str(prev
) + '\n')
1110 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1111 sequences
= self
.get_sequences()
1114 for key
in self
.iterkeys():
1116 changes
.append((key
, prev
+ 1))
1117 if hasattr(os
, 'link'):
1118 os
.link(os
.path
.join(self
._path
, str(key
)),
1119 os
.path
.join(self
._path
, str(prev
+ 1)))
1120 os
.unlink(os
.path
.join(self
._path
, str(key
)))
1122 os
.rename(os
.path
.join(self
._path
, str(key
)),
1123 os
.path
.join(self
._path
, str(prev
+ 1)))
1125 self
._next
_key
= prev
+ 1
1126 if len(changes
) == 0:
1128 for name
, key_list
in sequences
.items():
1129 for old
, new
in changes
:
1131 key_list
[key_list
.index(old
)] = new
1132 self
.set_sequences(sequences
)
1134 def _dump_sequences(self
, message
, key
):
1135 """Inspect a new MHMessage and update sequences appropriately."""
1136 pending_sequences
= message
.get_sequences()
1137 all_sequences
= self
.get_sequences()
1138 for name
, key_list
in all_sequences
.iteritems():
1139 if name
in pending_sequences
:
1140 key_list
.append(key
)
1141 elif key
in key_list
:
1142 del key_list
[key_list
.index(key
)]
1143 for sequence
in pending_sequences
:
1144 if sequence
not in all_sequences
:
1145 all_sequences
[sequence
] = [key
]
1146 self
.set_sequences(all_sequences
)
1149 class Babyl(_singlefileMailbox
):
1150 """An Rmail-style Babyl mailbox."""
1152 _special_labels
= frozenset(('unseen', 'deleted', 'filed', 'answered',
1153 'forwarded', 'edited', 'resent'))
1155 def __init__(self
, path
, factory
=None, create
=True):
1156 """Initialize a Babyl mailbox."""
1157 _singlefileMailbox
.__init
__(self
, path
, factory
, create
)
1160 def add(self
, message
):
1161 """Add message and return assigned key."""
1162 key
= _singlefileMailbox
.add(self
, message
)
1163 if isinstance(message
, BabylMessage
):
1164 self
._labels
[key
] = message
.get_labels()
1167 def remove(self
, key
):
1168 """Remove the keyed message; raise KeyError if it doesn't exist."""
1169 _singlefileMailbox
.remove(self
, key
)
1170 if key
in self
._labels
:
1171 del self
._labels
[key
]
1173 def __setitem__(self
, key
, message
):
1174 """Replace the keyed message; raise KeyError if it doesn't exist."""
1175 _singlefileMailbox
.__setitem
__(self
, key
, message
)
1176 if isinstance(message
, BabylMessage
):
1177 self
._labels
[key
] = message
.get_labels()
1179 def get_message(self
, key
):
1180 """Return a Message representation or raise a KeyError."""
1181 start
, stop
= self
._lookup
(key
)
1182 self
._file
.seek(start
)
1183 self
._file
.readline() # Skip '1,' line specifying labels.
1184 original_headers
= StringIO
.StringIO()
1186 line
= self
._file
.readline()
1187 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1189 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1190 visible_headers
= StringIO
.StringIO()
1192 line
= self
._file
.readline()
1193 if line
== os
.linesep
or line
== '':
1195 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1196 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1198 msg
= BabylMessage(original_headers
.getvalue() + body
)
1199 msg
.set_visible(visible_headers
.getvalue())
1200 if key
in self
._labels
:
1201 msg
.set_labels(self
._labels
[key
])
1204 def get_string(self
, key
):
1205 """Return a string representation or raise a KeyError."""
1206 start
, stop
= self
._lookup
(key
)
1207 self
._file
.seek(start
)
1208 self
._file
.readline() # Skip '1,' line specifying labels.
1209 original_headers
= StringIO
.StringIO()
1211 line
= self
._file
.readline()
1212 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1214 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1216 line
= self
._file
.readline()
1217 if line
== os
.linesep
or line
== '':
1219 return original_headers
.getvalue() + \
1220 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1223 def get_file(self
, key
):
1224 """Return a file-like representation or raise a KeyError."""
1225 return StringIO
.StringIO(self
.get_string(key
).replace('\n',
1228 def get_labels(self
):
1229 """Return a list of user-defined labels in the mailbox."""
1232 for label_list
in self
._labels
.values():
1233 labels
.update(label_list
)
1234 labels
.difference_update(self
._special
_labels
)
1237 def _generate_toc(self
):
1238 """Generate key-to-(start, stop) table of contents."""
1239 starts
, stops
= [], []
1245 line
= self
._file
.readline()
1246 next_pos
= self
._file
.tell()
1247 if line
== '\037\014' + os
.linesep
:
1248 if len(stops
) < len(starts
):
1249 stops
.append(line_pos
- len(os
.linesep
))
1250 starts
.append(next_pos
)
1251 labels
= [label
.strip() for label
1252 in self
._file
.readline()[1:].split(',')
1253 if label
.strip() != '']
1254 label_lists
.append(labels
)
1255 elif line
== '\037' or line
== '\037' + os
.linesep
:
1256 if len(stops
) < len(starts
):
1257 stops
.append(line_pos
- len(os
.linesep
))
1259 stops
.append(line_pos
- len(os
.linesep
))
1261 self
._toc
= dict(enumerate(zip(starts
, stops
)))
1262 self
._labels
= dict(enumerate(label_lists
))
1263 self
._next
_key
= len(self
._toc
)
1264 self
._file
.seek(0, 2)
1265 self
._file
_length
= self
._file
.tell()
1267 def _pre_mailbox_hook(self
, f
):
1268 """Called before writing the mailbox to file f."""
1269 f
.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1270 (os
.linesep
, os
.linesep
, ','.join(self
.get_labels()),
1273 def _pre_message_hook(self
, f
):
1274 """Called before writing each message to file f."""
1275 f
.write('\014' + os
.linesep
)
1277 def _post_message_hook(self
, f
):
1278 """Called after writing each message to file f."""
1279 f
.write(os
.linesep
+ '\037')
1281 def _install_message(self
, message
):
1282 """Write message contents and return (start, stop)."""
1283 start
= self
._file
.tell()
1284 if isinstance(message
, BabylMessage
):
1287 for label
in message
.get_labels():
1288 if label
in self
._special
_labels
:
1289 special_labels
.append(label
)
1291 labels
.append(label
)
1292 self
._file
.write('1')
1293 for label
in special_labels
:
1294 self
._file
.write(', ' + label
)
1295 self
._file
.write(',,')
1296 for label
in labels
:
1297 self
._file
.write(' ' + label
+ ',')
1298 self
._file
.write(os
.linesep
)
1300 self
._file
.write('1,,' + os
.linesep
)
1301 if isinstance(message
, email
.message
.Message
):
1302 orig_buffer
= StringIO
.StringIO()
1303 orig_generator
= email
.generator
.Generator(orig_buffer
, False, 0)
1304 orig_generator
.flatten(message
)
1307 line
= orig_buffer
.readline()
1308 self
._file
.write(line
.replace('\n', os
.linesep
))
1309 if line
== '\n' or line
== '':
1311 self
._file
.write('*** EOOH ***' + os
.linesep
)
1312 if isinstance(message
, BabylMessage
):
1313 vis_buffer
= StringIO
.StringIO()
1314 vis_generator
= email
.generator
.Generator(vis_buffer
, False, 0)
1315 vis_generator
.flatten(message
.get_visible())
1317 line
= vis_buffer
.readline()
1318 self
._file
.write(line
.replace('\n', os
.linesep
))
1319 if line
== '\n' or line
== '':
1324 line
= orig_buffer
.readline()
1325 self
._file
.write(line
.replace('\n', os
.linesep
))
1326 if line
== '\n' or line
== '':
1329 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
1332 self
._file
.write(buffer.replace('\n', os
.linesep
))
1333 elif isinstance(message
, str):
1334 body_start
= message
.find('\n\n') + 2
1335 if body_start
- 2 != -1:
1336 self
._file
.write(message
[:body_start
].replace('\n',
1338 self
._file
.write('*** EOOH ***' + os
.linesep
)
1339 self
._file
.write(message
[:body_start
].replace('\n',
1341 self
._file
.write(message
[body_start
:].replace('\n',
1344 self
._file
.write('*** EOOH ***' + os
.linesep
+ os
.linesep
)
1345 self
._file
.write(message
.replace('\n', os
.linesep
))
1346 elif hasattr(message
, 'readline'):
1347 original_pos
= message
.tell()
1350 line
= message
.readline()
1351 self
._file
.write(line
.replace('\n', os
.linesep
))
1352 if line
== '\n' or line
== '':
1353 self
._file
.write('*** EOOH ***' + os
.linesep
)
1356 message
.seek(original_pos
)
1360 buffer = message
.read(4096) # Buffer size is arbitrary.
1363 self
._file
.write(buffer.replace('\n', os
.linesep
))
1365 raise TypeError('Invalid message type: %s' % type(message
))
1366 stop
= self
._file
.tell()
1367 return (start
, stop
)
1370 class Message(email
.message
.Message
):
1371 """Message with mailbox-format-specific properties."""
1373 def __init__(self
, message
=None):
1374 """Initialize a Message instance."""
1375 if isinstance(message
, email
.message
.Message
):
1376 self
._become
_message
(copy
.deepcopy(message
))
1377 if isinstance(message
, Message
):
1378 message
._explain
_to
(self
)
1379 elif isinstance(message
, str):
1380 self
._become
_message
(email
.message_from_string(message
))
1381 elif hasattr(message
, "read"):
1382 self
._become
_message
(email
.message_from_file(message
))
1383 elif message
is None:
1384 email
.message
.Message
.__init
__(self
)
1386 raise TypeError('Invalid message type: %s' % type(message
))
1388 def _become_message(self
, message
):
1389 """Assume the non-format-specific state of message."""
1390 for name
in ('_headers', '_unixfrom', '_payload', '_charset',
1391 'preamble', 'epilogue', 'defects', '_default_type'):
1392 self
.__dict
__[name
] = message
.__dict
__[name
]
1394 def _explain_to(self
, message
):
1395 """Copy format-specific state to message insofar as possible."""
1396 if isinstance(message
, Message
):
1397 return # There's nothing format-specific to explain.
1399 raise TypeError('Cannot convert to specified type')
1402 class MaildirMessage(Message
):
1403 """Message with Maildir-specific properties."""
1405 def __init__(self
, message
=None):
1406 """Initialize a MaildirMessage instance."""
1407 self
._subdir
= 'new'
1409 self
._date
= time
.time()
1410 Message
.__init
__(self
, message
)
1412 def get_subdir(self
):
1413 """Return 'new' or 'cur'."""
1416 def set_subdir(self
, subdir
):
1417 """Set subdir to 'new' or 'cur'."""
1418 if subdir
== 'new' or subdir
== 'cur':
1419 self
._subdir
= subdir
1421 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir
)
1423 def get_flags(self
):
1424 """Return as a string the flags that are set."""
1425 if self
._info
.startswith('2,'):
1426 return self
._info
[2:]
1430 def set_flags(self
, flags
):
1431 """Set the given flags and unset all others."""
1432 self
._info
= '2,' + ''.join(sorted(flags
))
1434 def add_flag(self
, flag
):
1435 """Set the given flag(s) without changing others."""
1436 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1438 def remove_flag(self
, flag
):
1439 """Unset the given string flag(s) without changing others."""
1440 if self
.get_flags() != '':
1441 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1444 """Return delivery date of message, in seconds since the epoch."""
1447 def set_date(self
, date
):
1448 """Set delivery date of message, in seconds since the epoch."""
1450 self
._date
= float(date
)
1452 raise TypeError("can't convert to float: %s" % date
)
1455 """Get the message's "info" as a string."""
1458 def set_info(self
, info
):
1459 """Set the message's "info" string."""
1460 if isinstance(info
, str):
1463 raise TypeError('info must be a string: %s' % type(info
))
1465 def _explain_to(self
, message
):
1466 """Copy Maildir-specific state to message insofar as possible."""
1467 if isinstance(message
, MaildirMessage
):
1468 message
.set_flags(self
.get_flags())
1469 message
.set_subdir(self
.get_subdir())
1470 message
.set_date(self
.get_date())
1471 elif isinstance(message
, _mboxMMDFMessage
):
1472 flags
= set(self
.get_flags())
1474 message
.add_flag('R')
1475 if self
.get_subdir() == 'cur':
1476 message
.add_flag('O')
1478 message
.add_flag('D')
1480 message
.add_flag('F')
1482 message
.add_flag('A')
1483 message
.set_from('MAILER-DAEMON', time
.gmtime(self
.get_date()))
1484 elif isinstance(message
, MHMessage
):
1485 flags
= set(self
.get_flags())
1486 if 'S' not in flags
:
1487 message
.add_sequence('unseen')
1489 message
.add_sequence('replied')
1491 message
.add_sequence('flagged')
1492 elif isinstance(message
, BabylMessage
):
1493 flags
= set(self
.get_flags())
1494 if 'S' not in flags
:
1495 message
.add_label('unseen')
1497 message
.add_label('deleted')
1499 message
.add_label('answered')
1501 message
.add_label('forwarded')
1502 elif isinstance(message
, Message
):
1505 raise TypeError('Cannot convert to specified type: %s' %
1509 class _mboxMMDFMessage(Message
):
1510 """Message with mbox- or MMDF-specific properties."""
1512 def __init__(self
, message
=None):
1513 """Initialize an mboxMMDFMessage instance."""
1514 self
.set_from('MAILER-DAEMON', True)
1515 if isinstance(message
, email
.message
.Message
):
1516 unixfrom
= message
.get_unixfrom()
1517 if unixfrom
is not None and unixfrom
.startswith('From '):
1518 self
.set_from(unixfrom
[5:])
1519 Message
.__init
__(self
, message
)
1522 """Return contents of "From " line."""
1525 def set_from(self
, from_
, time_
=None):
1526 """Set "From " line, formatting and appending time_ if specified."""
1527 if time_
is not None:
1529 time_
= time
.gmtime()
1530 from_
+= ' ' + time
.asctime(time_
)
1533 def get_flags(self
):
1534 """Return as a string the flags that are set."""
1535 return self
.get('Status', '') + self
.get('X-Status', '')
1537 def set_flags(self
, flags
):
1538 """Set the given flags and unset all others."""
1540 status_flags
, xstatus_flags
= '', ''
1541 for flag
in ('R', 'O'):
1543 status_flags
+= flag
1545 for flag
in ('D', 'F', 'A'):
1547 xstatus_flags
+= flag
1549 xstatus_flags
+= ''.join(sorted(flags
))
1551 self
.replace_header('Status', status_flags
)
1553 self
.add_header('Status', status_flags
)
1555 self
.replace_header('X-Status', xstatus_flags
)
1557 self
.add_header('X-Status', xstatus_flags
)
1559 def add_flag(self
, flag
):
1560 """Set the given flag(s) without changing others."""
1561 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1563 def remove_flag(self
, flag
):
1564 """Unset the given string flag(s) without changing others."""
1565 if 'Status' in self
or 'X-Status' in self
:
1566 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1568 def _explain_to(self
, message
):
1569 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1570 if isinstance(message
, MaildirMessage
):
1571 flags
= set(self
.get_flags())
1573 message
.set_subdir('cur')
1575 message
.add_flag('F')
1577 message
.add_flag('R')
1579 message
.add_flag('S')
1581 message
.add_flag('T')
1582 del message
['status']
1583 del message
['x-status']
1584 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1586 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1587 '%a %b %d %H:%M:%S %Y')))
1588 except (ValueError, OverflowError):
1590 elif isinstance(message
, _mboxMMDFMessage
):
1591 message
.set_flags(self
.get_flags())
1592 message
.set_from(self
.get_from())
1593 elif isinstance(message
, MHMessage
):
1594 flags
= set(self
.get_flags())
1595 if 'R' not in flags
:
1596 message
.add_sequence('unseen')
1598 message
.add_sequence('replied')
1600 message
.add_sequence('flagged')
1601 del message
['status']
1602 del message
['x-status']
1603 elif isinstance(message
, BabylMessage
):
1604 flags
= set(self
.get_flags())
1605 if 'R' not in flags
:
1606 message
.add_label('unseen')
1608 message
.add_label('deleted')
1610 message
.add_label('answered')
1611 del message
['status']
1612 del message
['x-status']
1613 elif isinstance(message
, Message
):
1616 raise TypeError('Cannot convert to specified type: %s' %
1620 class mboxMessage(_mboxMMDFMessage
):
1621 """Message with mbox-specific properties."""
1624 class MHMessage(Message
):
1625 """Message with MH-specific properties."""
1627 def __init__(self
, message
=None):
1628 """Initialize an MHMessage instance."""
1629 self
._sequences
= []
1630 Message
.__init
__(self
, message
)
1632 def get_sequences(self
):
1633 """Return a list of sequences that include the message."""
1634 return self
._sequences
[:]
1636 def set_sequences(self
, sequences
):
1637 """Set the list of sequences that include the message."""
1638 self
._sequences
= list(sequences
)
1640 def add_sequence(self
, sequence
):
1641 """Add sequence to list of sequences including the message."""
1642 if isinstance(sequence
, str):
1643 if not sequence
in self
._sequences
:
1644 self
._sequences
.append(sequence
)
1646 raise TypeError('sequence must be a string: %s' % type(sequence
))
1648 def remove_sequence(self
, sequence
):
1649 """Remove sequence from the list of sequences including the message."""
1651 self
._sequences
.remove(sequence
)
1655 def _explain_to(self
, message
):
1656 """Copy MH-specific state to message insofar as possible."""
1657 if isinstance(message
, MaildirMessage
):
1658 sequences
= set(self
.get_sequences())
1659 if 'unseen' in sequences
:
1660 message
.set_subdir('cur')
1662 message
.set_subdir('cur')
1663 message
.add_flag('S')
1664 if 'flagged' in sequences
:
1665 message
.add_flag('F')
1666 if 'replied' in sequences
:
1667 message
.add_flag('R')
1668 elif isinstance(message
, _mboxMMDFMessage
):
1669 sequences
= set(self
.get_sequences())
1670 if 'unseen' not in sequences
:
1671 message
.add_flag('RO')
1673 message
.add_flag('O')
1674 if 'flagged' in sequences
:
1675 message
.add_flag('F')
1676 if 'replied' in sequences
:
1677 message
.add_flag('A')
1678 elif isinstance(message
, MHMessage
):
1679 for sequence
in self
.get_sequences():
1680 message
.add_sequence(sequence
)
1681 elif isinstance(message
, BabylMessage
):
1682 sequences
= set(self
.get_sequences())
1683 if 'unseen' in sequences
:
1684 message
.add_label('unseen')
1685 if 'replied' in sequences
:
1686 message
.add_label('answered')
1687 elif isinstance(message
, Message
):
1690 raise TypeError('Cannot convert to specified type: %s' %
1694 class BabylMessage(Message
):
1695 """Message with Babyl-specific properties."""
1697 def __init__(self
, message
=None):
1698 """Initialize an BabylMessage instance."""
1700 self
._visible
= Message()
1701 Message
.__init
__(self
, message
)
1703 def get_labels(self
):
1704 """Return a list of labels on the message."""
1705 return self
._labels
[:]
1707 def set_labels(self
, labels
):
1708 """Set the list of labels on the message."""
1709 self
._labels
= list(labels
)
1711 def add_label(self
, label
):
1712 """Add label to list of labels on the message."""
1713 if isinstance(label
, str):
1714 if label
not in self
._labels
:
1715 self
._labels
.append(label
)
1717 raise TypeError('label must be a string: %s' % type(label
))
1719 def remove_label(self
, label
):
1720 """Remove label from the list of labels on the message."""
1722 self
._labels
.remove(label
)
1726 def get_visible(self
):
1727 """Return a Message representation of visible headers."""
1728 return Message(self
._visible
)
1730 def set_visible(self
, visible
):
1731 """Set the Message representation of visible headers."""
1732 self
._visible
= Message(visible
)
1734 def update_visible(self
):
1735 """Update and/or sensibly generate a set of visible headers."""
1736 for header
in self
._visible
.keys():
1738 self
._visible
.replace_header(header
, self
[header
])
1740 del self
._visible
[header
]
1741 for header
in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1742 if header
in self
and header
not in self
._visible
:
1743 self
._visible
[header
] = self
[header
]
1745 def _explain_to(self
, message
):
1746 """Copy Babyl-specific state to message insofar as possible."""
1747 if isinstance(message
, MaildirMessage
):
1748 labels
= set(self
.get_labels())
1749 if 'unseen' in labels
:
1750 message
.set_subdir('cur')
1752 message
.set_subdir('cur')
1753 message
.add_flag('S')
1754 if 'forwarded' in labels
or 'resent' in labels
:
1755 message
.add_flag('P')
1756 if 'answered' in labels
:
1757 message
.add_flag('R')
1758 if 'deleted' in labels
:
1759 message
.add_flag('T')
1760 elif isinstance(message
, _mboxMMDFMessage
):
1761 labels
= set(self
.get_labels())
1762 if 'unseen' not in labels
:
1763 message
.add_flag('RO')
1765 message
.add_flag('O')
1766 if 'deleted' in labels
:
1767 message
.add_flag('D')
1768 if 'answered' in labels
:
1769 message
.add_flag('A')
1770 elif isinstance(message
, MHMessage
):
1771 labels
= set(self
.get_labels())
1772 if 'unseen' in labels
:
1773 message
.add_sequence('unseen')
1774 if 'answered' in labels
:
1775 message
.add_sequence('replied')
1776 elif isinstance(message
, BabylMessage
):
1777 message
.set_visible(self
.get_visible())
1778 for label
in self
.get_labels():
1779 message
.add_label(label
)
1780 elif isinstance(message
, Message
):
1783 raise TypeError('Cannot convert to specified type: %s' %
1787 class MMDFMessage(_mboxMMDFMessage
):
1788 """Message with MMDF-specific properties."""
1792 """A read-only wrapper of a file."""
1794 def __init__(self
, f
, pos
=None):
1795 """Initialize a _ProxyFile."""
1798 self
._pos
= f
.tell()
1802 def read(self
, size
=None):
1804 return self
._read
(size
, self
._file
.read
)
1806 def readline(self
, size
=None):
1808 return self
._read
(size
, self
._file
.readline
)
1810 def readlines(self
, sizehint
=None):
1811 """Read multiple lines."""
1815 if sizehint
is not None:
1816 sizehint
-= len(line
)
1822 """Iterate over lines."""
1823 return iter(self
.readline
, "")
1826 """Return the position."""
1829 def seek(self
, offset
, whence
=0):
1830 """Change position."""
1832 self
._file
.seek(self
._pos
)
1833 self
._file
.seek(offset
, whence
)
1834 self
._pos
= self
._file
.tell()
1837 """Close the file."""
1840 def _read(self
, size
, read_method
):
1841 """Read size bytes using read_method."""
1844 self
._file
.seek(self
._pos
)
1845 result
= read_method(size
)
1846 self
._pos
= self
._file
.tell()
1850 class _PartialFile(_ProxyFile
):
1851 """A read-only wrapper of part of a file."""
1853 def __init__(self
, f
, start
=None, stop
=None):
1854 """Initialize a _PartialFile."""
1855 _ProxyFile
.__init
__(self
, f
, start
)
1860 """Return the position with respect to start."""
1861 return _ProxyFile
.tell(self
) - self
._start
1863 def seek(self
, offset
, whence
=0):
1864 """Change position, possibly with respect to start or stop."""
1866 self
._pos
= self
._start
1869 self
._pos
= self
._stop
1871 _ProxyFile
.seek(self
, offset
, whence
)
1873 def _read(self
, size
, read_method
):
1874 """Read size bytes using read_method, honoring start and stop."""
1875 remaining
= self
._stop
- self
._pos
1878 if size
is None or size
< 0 or size
> remaining
:
1880 return _ProxyFile
._read
(self
, size
, read_method
)
1883 def _lock_file(f
, dotlock
=True):
1884 """Lock file f using lockf and dot locking."""
1885 dotlock_done
= False
1889 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1891 if e
.errno
in (errno
.EAGAIN
, errno
.EACCES
):
1892 raise ExternalClashError('lockf: lock unavailable: %s' %
1898 pre_lock
= _create_temporary(f
.name
+ '.lock')
1901 if e
.errno
== errno
.EACCES
:
1902 return # Without write access, just skip dotlocking.
1906 if hasattr(os
, 'link'):
1907 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1909 os
.unlink(pre_lock
.name
)
1911 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
1914 if e
.errno
== errno
.EEXIST
or \
1915 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
1916 os
.remove(pre_lock
.name
)
1917 raise ExternalClashError('dot lock unavailable: %s' %
1923 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1925 os
.remove(f
.name
+ '.lock')
1928 def _unlock_file(f
):
1929 """Unlock file f using lockf and dot locking."""
1931 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1932 if os
.path
.exists(f
.name
+ '.lock'):
1933 os
.remove(f
.name
+ '.lock')
1935 def _create_carefully(path
):
1936 """Create a file if it doesn't exist and open for reading and writing."""
1937 fd
= os
.open(path
, os
.O_CREAT | os
.O_EXCL | os
.O_RDWR
, 0666)
1939 return open(path
, 'rb+')
1943 def _create_temporary(path
):
1944 """Create a temp file based on path and open for reading and writing."""
1945 return _create_carefully('%s.%s.%s.%s' % (path
, int(time
.time()),
1946 socket
.gethostname(),
1950 """Ensure changes to file f are physically on disk."""
1952 if hasattr(os
, 'fsync'):
1953 os
.fsync(f
.fileno())
1956 """Close file f, ensuring all changes are physically on disk."""
1960 ## Start: classes from the original module (for backward compatibility).
1962 # Note that the Maildir class, whose name is unchanged, itself offers a next()
1963 # method for backward compatibility.
1967 def __init__(self
, fp
, factory
=rfc822
.Message
):
1970 self
.factory
= factory
1973 return iter(self
.next
, None)
1977 self
.fp
.seek(self
.seekp
)
1979 self
._search
_start
()
1981 self
.seekp
= self
.fp
.tell()
1983 start
= self
.fp
.tell()
1985 self
.seekp
= stop
= self
.fp
.tell()
1988 return self
.factory(_PartialFile(self
.fp
, start
, stop
))
1990 # Recommended to use PortableUnixMailbox instead!
1991 class UnixMailbox(_Mailbox
):
1993 def _search_start(self
):
1995 pos
= self
.fp
.tell()
1996 line
= self
.fp
.readline()
1999 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
2003 def _search_end(self
):
2004 self
.fp
.readline() # Throw away header line
2006 pos
= self
.fp
.tell()
2007 line
= self
.fp
.readline()
2010 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
2014 # An overridable mechanism to test for From-line-ness. You can either
2015 # specify a different regular expression or define a whole new
2016 # _isrealfromline() method. Note that this only gets called for lines
2017 # starting with the 5 characters "From ".
2020 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
2021 # the only portable, reliable way to find message delimiters in a BSD (i.e
2022 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
2023 # beginning of the file, "^From .*\n". While _fromlinepattern below seems
2024 # like a good idea, in practice, there are too many variations for more
2025 # strict parsing of the line to be completely accurate.
2027 # _strict_isrealfromline() is the old version which tries to do stricter
2028 # parsing of the From_ line. _portable_isrealfromline() simply returns
2029 # true, since it's never called if the line doesn't already start with
2032 # This algorithm, and the way it interacts with _search_start() and
2033 # _search_end() may not be completely correct, because it doesn't check
2034 # that the two characters preceding "From " are \n\n or the beginning of
2035 # the file. Fixing this would require a more extensive rewrite than is
2036 # necessary. For convenience, we've added a PortableUnixMailbox class
2037 # which does no checking of the format of the 'From' line.
2039 _fromlinepattern
= (r
"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
2040 r
"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
2045 def _strict_isrealfromline(self
, line
):
2046 if not self
._regexp
:
2048 self
._regexp
= re
.compile(self
._fromlinepattern
)
2049 return self
._regexp
.match(line
)
2051 def _portable_isrealfromline(self
, line
):
2054 _isrealfromline
= _strict_isrealfromline
2057 class PortableUnixMailbox(UnixMailbox
):
2058 _isrealfromline
= UnixMailbox
._portable
_isrealfromline
2061 class MmdfMailbox(_Mailbox
):
2063 def _search_start(self
):
2065 line
= self
.fp
.readline()
2068 if line
[:5] == '\001\001\001\001\n':
2071 def _search_end(self
):
2073 pos
= self
.fp
.tell()
2074 line
= self
.fp
.readline()
2077 if line
== '\001\001\001\001\n':
2084 def __init__(self
, dirname
, factory
=rfc822
.Message
):
2086 pat
= re
.compile('^[1-9][0-9]*$')
2087 self
.dirname
= dirname
2088 # the three following lines could be combined into:
2089 # list = map(long, filter(pat.match, os.listdir(self.dirname)))
2090 list = os
.listdir(self
.dirname
)
2091 list = filter(pat
.match
, list)
2092 list = map(long, list)
2094 # This only works in Python 1.6 or later;
2095 # before that str() added 'L':
2096 self
.boxes
= map(str, list)
2097 self
.boxes
.reverse()
2098 self
.factory
= factory
2101 return iter(self
.next
, None)
2106 fn
= self
.boxes
.pop()
2107 fp
= open(os
.path
.join(self
.dirname
, fn
))
2108 msg
= self
.factory(fp
)
2111 except (AttributeError, TypeError):
2116 class BabylMailbox(_Mailbox
):
2118 def _search_start(self
):
2120 line
= self
.fp
.readline()
2123 if line
== '*** EOOH ***\n':
2126 def _search_end(self
):
2128 pos
= self
.fp
.tell()
2129 line
= self
.fp
.readline()
2132 if line
== '\037\014\n' or line
== '\037':
2136 ## End: classes from the original module (for backward compatibility).
2139 class Error(Exception):
2140 """Raised for module-specific errors."""
2142 class NoSuchMailboxError(Error
):
2143 """The specified mailbox does not exist and won't be created."""
2145 class NotEmptyError(Error
):
2146 """The specified mailbox is not empty and deletion was requested."""
2148 class ExternalClashError(Error
):
2149 """Another process caused an action to fail."""
2151 class FormatError(Error
):
2152 """A file appears to have an invalid format."""