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
23 if sys
.platform
== 'os2emx':
24 # OS/2 EMX fcntl() not adequate
30 __all__
= [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
31 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
32 'BabylMessage', 'MMDFMessage']
35 """A group of messages in a particular place."""
37 def __init__(self
, path
, factory
=None, create
=True):
38 """Initialize a Mailbox instance."""
39 self
._path
= os
.path
.abspath(os
.path
.expanduser(path
))
40 self
._factory
= factory
42 def add(self
, message
):
43 """Add message and return assigned key."""
44 raise NotImplementedError('Method must be implemented by subclass')
46 def remove(self
, key
):
47 """Remove the keyed message; raise KeyError if it doesn't exist."""
48 raise NotImplementedError('Method must be implemented by subclass')
50 def __delitem__(self
, key
):
53 def discard(self
, key
):
54 """If the keyed message exists, remove it."""
60 def __setitem__(self
, key
, message
):
61 """Replace the keyed message; raise KeyError if it doesn't exist."""
62 raise NotImplementedError('Method must be implemented by subclass')
64 def get(self
, key
, default
=None):
65 """Return the keyed message, or default if it doesn't exist."""
67 return self
.__getitem
__(key
)
71 def __getitem__(self
, key
):
72 """Return the keyed message; raise KeyError if it doesn't exist."""
74 return self
.get_message(key
)
76 return self
._factory
(self
.get_file(key
))
78 def get_message(self
, key
):
79 """Return a Message representation or raise a KeyError."""
80 raise NotImplementedError('Method must be implemented by subclass')
82 def get_string(self
, key
):
83 """Return a string representation or raise a KeyError."""
84 raise NotImplementedError('Method must be implemented by subclass')
86 def get_file(self
, key
):
87 """Return a file-like representation or raise a KeyError."""
88 raise NotImplementedError('Method must be implemented by subclass')
91 """Return an iterator over keys."""
92 raise NotImplementedError('Method must be implemented by subclass')
95 """Return a list of keys."""
96 return list(self
.iterkeys())
99 """Return an iterator over all messages."""
100 for key
in self
.keys():
108 return self
.itervalues()
111 """Return a list of messages. Memory intensive."""
112 return list(self
.itervalues())
115 """Return an iterator over (key, message) tuples."""
116 for key
in self
.keys():
124 """Return a list of (key, message) tuples. Memory intensive."""
125 return list(self
.iteritems())
127 def __contains__(self
, key
):
128 """Return True if the keyed message exists, False otherwise."""
129 raise NotImplementedError('Method must be implemented by subclass')
132 """Return a count of messages in the mailbox."""
133 raise NotImplementedError('Method must be implemented by subclass')
136 """Delete all messages."""
137 for key
in self
.keys():
140 def pop(self
, key
, default
=None):
141 """Delete the keyed message and return it, or default."""
150 """Delete an arbitrary (key, message) pair and return it."""
151 for key
in self
.keys():
152 return (key
, self
.pop(key
)) # This is only run once.
154 raise KeyError('No messages in mailbox')
156 def update(self
, arg
=None):
157 """Change the messages that correspond to certain keys."""
158 if hasattr(arg
, 'iteritems'):
160 elif hasattr(arg
, 'items'):
165 for key
, message
in source
:
171 raise KeyError('No message with key(s)')
174 """Write any pending changes to the disk."""
175 raise NotImplementedError('Method must be implemented by subclass')
178 """Lock the mailbox."""
179 raise NotImplementedError('Method must be implemented by subclass')
182 """Unlock the mailbox if it is locked."""
183 raise NotImplementedError('Method must be implemented by subclass')
186 """Flush and close the mailbox."""
187 raise NotImplementedError('Method must be implemented by subclass')
189 def _dump_message(self
, message
, target
, mangle_from_
=False):
190 # This assumes the target file is open in *text* mode with the
191 # desired encoding and newline setting.
192 """Dump message contents to target file."""
193 if isinstance(message
, email
.message
.Message
):
194 buffer = io
.StringIO()
195 gen
= email
.generator
.Generator(buffer, mangle_from_
, 0)
199 ##data = data.replace('\n', os.linesep)
201 elif isinstance(message
, str):
203 message
= message
.replace('\nFrom ', '\n>From ')
204 ##message = message.replace('\n', os.linesep)
205 target
.write(message
)
206 elif hasattr(message
, 'read'):
208 line
= message
.readline()
211 if mangle_from_
and line
.startswith('From '):
212 line
= '>From ' + line
[5:]
213 ##line = line.replace('\n', os.linesep)
216 raise TypeError('Invalid message type: %s' % type(message
))
219 class Maildir(Mailbox
):
220 """A qmail-style Maildir mailbox."""
224 def __init__(self
, dirname
, factory
=None, create
=True):
225 """Initialize a Maildir instance."""
226 Mailbox
.__init
__(self
, dirname
, factory
, create
)
227 if not os
.path
.exists(self
._path
):
229 os
.mkdir(self
._path
, 0o700)
230 os
.mkdir(os
.path
.join(self
._path
, 'tmp'), 0o700)
231 os
.mkdir(os
.path
.join(self
._path
, 'new'), 0o700)
232 os
.mkdir(os
.path
.join(self
._path
, 'cur'), 0o700)
234 raise NoSuchMailboxError(self
._path
)
236 self
._last
_read
= None # Records last time we read cur/new
237 # NOTE: we manually invalidate _last_read each time we do any
238 # modifications ourselves, otherwise we might get tripped up by
239 # bogus mtime behaviour on some systems (see issue #6896).
241 def add(self
, message
):
242 """Add message and return assigned key."""
243 tmp_file
= self
._create
_tmp
()
245 self
._dump
_message
(message
, tmp_file
)
247 _sync_close(tmp_file
)
248 if isinstance(message
, MaildirMessage
):
249 subdir
= message
.get_subdir()
250 suffix
= self
.colon
+ message
.get_info()
251 if suffix
== self
.colon
:
256 uniq
= os
.path
.basename(tmp_file
.name
).split(self
.colon
)[0]
257 dest
= os
.path
.join(self
._path
, subdir
, uniq
+ suffix
)
259 if hasattr(os
, 'link'):
260 os
.link(tmp_file
.name
, dest
)
261 os
.remove(tmp_file
.name
)
263 os
.rename(tmp_file
.name
, dest
)
265 os
.remove(tmp_file
.name
)
266 if e
.errno
== errno
.EEXIST
:
267 raise ExternalClashError('Name clash with existing message: %s'
271 if isinstance(message
, MaildirMessage
):
272 os
.utime(dest
, (os
.path
.getatime(dest
), message
.get_date()))
273 # Invalidate cached toc
274 self
._last
_read
= None
277 def remove(self
, key
):
278 """Remove the keyed message; raise KeyError if it doesn't exist."""
279 os
.remove(os
.path
.join(self
._path
, self
._lookup
(key
)))
280 # Invalidate cached toc (only on success)
281 self
._last
_read
= None
283 def discard(self
, key
):
284 """If the keyed message exists, remove it."""
285 # This overrides an inapplicable implementation in the superclass.
291 if e
.errno
!= errno
.ENOENT
:
294 def __setitem__(self
, key
, message
):
295 """Replace the keyed message; raise KeyError if it doesn't exist."""
296 old_subpath
= self
._lookup
(key
)
297 temp_key
= self
.add(message
)
298 temp_subpath
= self
._lookup
(temp_key
)
299 if isinstance(message
, MaildirMessage
):
300 # temp's subdir and suffix were specified by message.
301 dominant_subpath
= temp_subpath
303 # temp's subdir and suffix were defaults from add().
304 dominant_subpath
= old_subpath
305 subdir
= os
.path
.dirname(dominant_subpath
)
306 if self
.colon
in dominant_subpath
:
307 suffix
= self
.colon
+ dominant_subpath
.split(self
.colon
)[-1]
311 new_path
= os
.path
.join(self
._path
, subdir
, key
+ suffix
)
312 os
.rename(os
.path
.join(self
._path
, temp_subpath
), new_path
)
313 if isinstance(message
, MaildirMessage
):
314 os
.utime(new_path
, (os
.path
.getatime(new_path
),
316 # Invalidate cached toc
317 self
._last
_read
= None
319 def get_message(self
, key
):
320 """Return a Message representation or raise a KeyError."""
321 subpath
= self
._lookup
(key
)
322 f
= open(os
.path
.join(self
._path
, subpath
), 'r', newline
='')
325 msg
= self
._factory
(f
)
327 msg
= MaildirMessage(f
)
330 subdir
, name
= os
.path
.split(subpath
)
331 msg
.set_subdir(subdir
)
332 if self
.colon
in name
:
333 msg
.set_info(name
.split(self
.colon
)[-1])
334 msg
.set_date(os
.path
.getmtime(os
.path
.join(self
._path
, subpath
)))
337 def get_string(self
, key
):
338 """Return a string representation or raise a KeyError."""
339 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r', newline
='')
345 def get_file(self
, key
):
346 """Return a file-like representation or raise a KeyError."""
347 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r', newline
='')
351 """Return an iterator over keys."""
353 for key
in self
._toc
:
360 def __contains__(self
, key
):
361 """Return True if the keyed message exists, False otherwise."""
363 return key
in self
._toc
366 """Return a count of messages in the mailbox."""
368 return len(self
._toc
)
371 """Write any pending changes to disk."""
372 # Maildir changes are always written immediately, so there's nothing
373 # to do except invalidate our cached toc.
374 self
._last
_read
= None
377 """Lock the mailbox."""
381 """Unlock the mailbox if it is locked."""
385 """Flush and close the mailbox."""
388 def list_folders(self
):
389 """Return a list of folder names."""
391 for entry
in os
.listdir(self
._path
):
392 if len(entry
) > 1 and entry
[0] == '.' and \
393 os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
394 result
.append(entry
[1:])
397 def get_folder(self
, folder
):
398 """Return a Maildir instance for the named folder."""
399 return Maildir(os
.path
.join(self
._path
, '.' + folder
),
400 factory
=self
._factory
,
403 def add_folder(self
, folder
):
404 """Create a folder and return a Maildir instance representing it."""
405 path
= os
.path
.join(self
._path
, '.' + folder
)
406 result
= Maildir(path
, factory
=self
._factory
)
407 maildirfolder_path
= os
.path
.join(path
, 'maildirfolder')
408 if not os
.path
.exists(maildirfolder_path
):
409 os
.close(os
.open(maildirfolder_path
, os
.O_CREAT | os
.O_WRONLY
,
413 def remove_folder(self
, folder
):
414 """Delete the named folder, which must be empty."""
415 path
= os
.path
.join(self
._path
, '.' + folder
)
416 for entry
in os
.listdir(os
.path
.join(path
, 'new')) + \
417 os
.listdir(os
.path
.join(path
, 'cur')):
418 if len(entry
) < 1 or entry
[0] != '.':
419 raise NotEmptyError('Folder contains message(s): %s' % folder
)
420 for entry
in os
.listdir(path
):
421 if entry
!= 'new' and entry
!= 'cur' and entry
!= 'tmp' and \
422 os
.path
.isdir(os
.path
.join(path
, entry
)):
423 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
425 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
427 os
.remove(os
.path
.join(root
, entry
))
429 os
.rmdir(os
.path
.join(root
, entry
))
433 """Delete old files in "tmp"."""
435 for entry
in os
.listdir(os
.path
.join(self
._path
, 'tmp')):
436 path
= os
.path
.join(self
._path
, 'tmp', entry
)
437 if now
- os
.path
.getatime(path
) > 129600: # 60 * 60 * 36
440 _count
= 1 # This is used to generate unique file names.
442 def _create_tmp(self
):
443 """Create a file in the tmp subdirectory and open and return it."""
445 hostname
= socket
.gethostname()
447 hostname
= hostname
.replace('/', r
'\057')
449 hostname
= hostname
.replace(':', r
'\072')
450 uniq
= "%s.M%sP%sQ%s.%s" % (int(now
), int(now
% 1 * 1e6
), os
.getpid(),
451 Maildir
._count
, hostname
)
452 path
= os
.path
.join(self
._path
, 'tmp', uniq
)
456 if e
.errno
== errno
.ENOENT
:
459 return _create_carefully(path
)
461 if e
.errno
!= errno
.EEXIST
:
466 # Fall through to here if stat succeeded or open raised EEXIST.
467 raise ExternalClashError('Name clash prevented file creation: %s' %
471 """Update table of contents mapping."""
472 new_mtime
= os
.path
.getmtime(os
.path
.join(self
._path
, 'new'))
473 cur_mtime
= os
.path
.getmtime(os
.path
.join(self
._path
, 'cur'))
475 if (self
._last
_read
is not None and
476 new_mtime
<= self
._last
_read
and cur_mtime
<= self
._last
_read
):
480 def update_dir (subdir
):
481 path
= os
.path
.join(self
._path
, subdir
)
482 for entry
in os
.listdir(path
):
483 p
= os
.path
.join(path
, entry
)
486 uniq
= entry
.split(self
.colon
)[0]
487 self
._toc
[uniq
] = os
.path
.join(subdir
, entry
)
492 # We record the current time - 1sec so that, if _refresh() is called
493 # again in the same second, we will always re-read the mailbox
494 # just in case it's been modified. (os.path.mtime() only has
495 # 1sec resolution.) This results in a few unnecessary re-reads
496 # when _refresh() is called multiple times in the same second,
497 # but once the clock ticks over, we will only re-read as needed.
498 now
= int(time
.time() - 1)
499 self
._last
_read
= time
.time() - 1
501 def _lookup(self
, key
):
502 """Use TOC to return subpath for given key, or raise a KeyError."""
504 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
505 return self
._toc
[key
]
510 return self
._toc
[key
]
512 raise KeyError('No message with key: %s' % key
)
514 # This method is for backward compatibility only.
516 """Return the next message in a one-time iteration."""
517 if not hasattr(self
, '_onetime_keys'):
518 self
._onetime
_keys
= iter(self
.keys())
521 return self
[next(self
._onetime
_keys
)]
522 except StopIteration:
528 class _singlefileMailbox(Mailbox
):
529 """A single-file mailbox."""
531 def __init__(self
, path
, factory
=None, create
=True):
532 """Initialize a single-file mailbox."""
533 Mailbox
.__init
__(self
, path
, factory
, create
)
535 f
= open(self
._path
, 'r+', newline
='')
537 if e
.errno
== errno
.ENOENT
:
539 f
= open(self
._path
, 'w+', newline
='')
541 raise NoSuchMailboxError(self
._path
)
542 elif e
.errno
== errno
.EACCES
:
543 f
= open(self
._path
, 'r', newline
='')
549 self
._pending
= False # No changes require rewriting the file.
551 self
._file
_length
= None # Used to record mailbox size
553 def add(self
, message
):
554 """Add message and return assigned key."""
556 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
559 return self
._next
_key
- 1
561 def remove(self
, key
):
562 """Remove the keyed message; raise KeyError if it doesn't exist."""
567 def __setitem__(self
, key
, message
):
568 """Replace the keyed message; raise KeyError if it doesn't exist."""
570 self
._toc
[key
] = self
._append
_message
(message
)
574 """Return an iterator over keys."""
576 for key
in self
._toc
.keys():
579 def __contains__(self
, key
):
580 """Return True if the keyed message exists, False otherwise."""
582 return key
in self
._toc
585 """Return a count of messages in the mailbox."""
587 return len(self
._toc
)
590 """Lock the mailbox."""
592 _lock_file(self
._file
)
596 """Unlock the mailbox if it is locked."""
598 _unlock_file(self
._file
)
602 """Write any pending changes to disk."""
603 if not self
._pending
:
606 # In order to be writing anything out at all, self._toc must
607 # already have been generated (and presumably has been modified
608 # by adding or deleting an item).
609 assert self
._toc
is not None
611 # Check length of self._file; if it's changed, some other process
612 # has modified the mailbox since we scanned it.
613 self
._file
.seek(0, 2)
614 cur_len
= self
._file
.tell()
615 if cur_len
!= self
._file
_length
:
616 raise ExternalClashError('Size of mailbox file changed '
617 '(expected %i, found %i)' %
618 (self
._file
_length
, cur_len
))
620 new_file
= _create_temporary(self
._path
)
623 self
._pre
_mailbox
_hook
(new_file
)
624 for key
in sorted(self
._toc
.keys()):
625 start
, stop
= self
._toc
[key
]
626 self
._file
.seek(start
)
627 self
._pre
_message
_hook
(new_file
)
628 new_start
= new_file
.tell()
630 buffer = self
._file
.read(min(4096,
631 stop
- self
._file
.tell()))
634 new_file
.write(buffer)
635 new_toc
[key
] = (new_start
, new_file
.tell())
636 self
._post
_message
_hook
(new_file
)
639 os
.remove(new_file
.name
)
641 _sync_close(new_file
)
642 # self._file is about to get replaced, so no need to sync.
645 os
.rename(new_file
.name
, self
._path
)
647 if e
.errno
== errno
.EEXIST
or \
648 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
649 os
.remove(self
._path
)
650 os
.rename(new_file
.name
, self
._path
)
653 self
._file
= open(self
._path
, 'rb+')
655 self
._pending
= False
657 _lock_file(self
._file
, dotlock
=False)
659 def _pre_mailbox_hook(self
, f
):
660 """Called before writing the mailbox to file f."""
663 def _pre_message_hook(self
, f
):
664 """Called before writing each message to file f."""
667 def _post_message_hook(self
, f
):
668 """Called after writing each message to file f."""
672 """Flush and close the mailbox."""
676 self
._file
.close() # Sync has been done by self.flush() above.
678 def _lookup(self
, key
=None):
679 """Return (start, stop) or raise KeyError."""
680 if self
._toc
is None:
684 return self
._toc
[key
]
686 raise KeyError('No message with key: %s' % key
)
688 def _append_message(self
, message
):
689 """Append message to mailbox and return (start, stop) offsets."""
690 self
._file
.seek(0, 2)
691 self
._pre
_message
_hook
(self
._file
)
692 offsets
= self
._install
_message
(message
)
693 self
._post
_message
_hook
(self
._file
)
695 self
._file
_length
= self
._file
.tell() # Record current length of mailbox
700 class _mboxMMDF(_singlefileMailbox
):
701 """An mbox or MMDF mailbox."""
705 def get_message(self
, key
):
706 """Return a Message representation or raise a KeyError."""
707 start
, stop
= self
._lookup
(key
)
708 self
._file
.seek(start
)
709 from_line
= self
._file
.readline().replace(os
.linesep
, '')
710 string
= self
._file
.read(stop
- self
._file
.tell())
711 msg
= self
._message
_factory
(string
.replace(os
.linesep
, '\n'))
712 msg
.set_from(from_line
[5:])
715 def get_string(self
, key
, from_
=False):
716 """Return a string representation or raise a KeyError."""
717 start
, stop
= self
._lookup
(key
)
718 self
._file
.seek(start
)
720 self
._file
.readline()
721 string
= self
._file
.read(stop
- self
._file
.tell())
722 return string
.replace(os
.linesep
, '\n')
724 def get_file(self
, key
, from_
=False):
725 """Return a file-like representation or raise a KeyError."""
726 start
, stop
= self
._lookup
(key
)
727 self
._file
.seek(start
)
729 self
._file
.readline()
730 return _PartialFile(self
._file
, self
._file
.tell(), stop
)
732 def _install_message(self
, message
):
733 """Format a message and blindly write to self._file."""
735 if isinstance(message
, str) and message
.startswith('From '):
736 newline
= message
.find('\n')
738 from_line
= message
[:newline
]
739 message
= message
[newline
+ 1:]
743 elif isinstance(message
, _mboxMMDFMessage
):
744 from_line
= 'From ' + message
.get_from()
745 elif isinstance(message
, email
.message
.Message
):
746 from_line
= message
.get_unixfrom() # May be None.
747 if from_line
is None:
748 from_line
= 'From MAILER-DAEMON %s' % time
.asctime(time
.gmtime())
749 start
= self
._file
.tell()
750 self
._file
.write(from_line
+ os
.linesep
)
751 self
._dump
_message
(message
, self
._file
, self
._mangle
_from
_)
752 stop
= self
._file
.tell()
756 class mbox(_mboxMMDF
):
757 """A classic mbox mailbox."""
761 def __init__(self
, path
, factory
=None, create
=True):
762 """Initialize an mbox mailbox."""
763 self
._message
_factory
= mboxMessage
764 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
766 def _pre_message_hook(self
, f
):
767 """Called before writing each message to file f."""
771 def _generate_toc(self
):
772 """Generate key-to-(start, stop) table of contents."""
773 starts
, stops
= [], []
776 line_pos
= self
._file
.tell()
777 line
= self
._file
.readline()
778 if line
.startswith('From '):
779 if len(stops
) < len(starts
):
780 stops
.append(line_pos
- len(os
.linesep
))
781 starts
.append(line_pos
)
783 stops
.append(line_pos
)
785 self
._toc
= dict(enumerate(zip(starts
, stops
)))
786 self
._next
_key
= len(self
._toc
)
787 self
._file
_length
= self
._file
.tell()
790 class MMDF(_mboxMMDF
):
791 """An MMDF mailbox."""
793 def __init__(self
, path
, factory
=None, create
=True):
794 """Initialize an MMDF mailbox."""
795 self
._message
_factory
= MMDFMessage
796 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
798 def _pre_message_hook(self
, f
):
799 """Called before writing each message to file f."""
800 f
.write('\001\001\001\001' + os
.linesep
)
802 def _post_message_hook(self
, f
):
803 """Called after writing each message to file f."""
804 f
.write(os
.linesep
+ '\001\001\001\001' + os
.linesep
)
806 def _generate_toc(self
):
807 """Generate key-to-(start, stop) table of contents."""
808 starts
, stops
= [], []
813 line
= self
._file
.readline()
814 next_pos
= self
._file
.tell()
815 if line
.startswith('\001\001\001\001' + os
.linesep
):
816 starts
.append(next_pos
)
819 line
= self
._file
.readline()
820 next_pos
= self
._file
.tell()
821 if line
== '\001\001\001\001' + os
.linesep
:
822 stops
.append(line_pos
- len(os
.linesep
))
825 stops
.append(line_pos
)
829 self
._toc
= dict(enumerate(zip(starts
, stops
)))
830 self
._next
_key
= len(self
._toc
)
831 self
._file
.seek(0, 2)
832 self
._file
_length
= self
._file
.tell()
838 def __init__(self
, path
, factory
=None, create
=True):
839 """Initialize an MH instance."""
840 Mailbox
.__init
__(self
, path
, factory
, create
)
841 if not os
.path
.exists(self
._path
):
843 os
.mkdir(self
._path
, 0o700)
844 os
.close(os
.open(os
.path
.join(self
._path
, '.mh_sequences'),
845 os
.O_CREAT | os
.O_EXCL | os
.O_WRONLY
, 0o600))
847 raise NoSuchMailboxError(self
._path
)
850 def add(self
, message
):
851 """Add message and return assigned key."""
856 new_key
= max(keys
) + 1
857 new_path
= os
.path
.join(self
._path
, str(new_key
))
858 f
= _create_carefully(new_path
)
863 self
._dump
_message
(message
, f
)
864 if isinstance(message
, MHMessage
):
865 self
._dump
_sequences
(message
, new_key
)
873 def remove(self
, key
):
874 """Remove the keyed message; raise KeyError if it doesn't exist."""
875 path
= os
.path
.join(self
._path
, str(key
))
877 f
= open(path
, 'rb+')
879 if e
.errno
== errno
.ENOENT
:
880 raise KeyError('No message with key: %s' % key
)
888 os
.remove(os
.path
.join(self
._path
, str(key
)))
895 def __setitem__(self
, key
, message
):
896 """Replace the keyed message; raise KeyError if it doesn't exist."""
897 path
= os
.path
.join(self
._path
, str(key
))
899 f
= open(path
, 'r+', newline
='')
901 if e
.errno
== errno
.ENOENT
:
902 raise KeyError('No message with key: %s' % key
)
909 os
.close(os
.open(path
, os
.O_WRONLY | os
.O_TRUNC
))
910 self
._dump
_message
(message
, f
)
911 if isinstance(message
, MHMessage
):
912 self
._dump
_sequences
(message
, key
)
919 def get_message(self
, key
):
920 """Return a Message representation or raise a KeyError."""
923 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+', newline
='')
925 f
= open(os
.path
.join(self
._path
, str(key
)), 'r', newline
='')
927 if e
.errno
== errno
.ENOENT
:
928 raise KeyError('No message with key: %s' % key
)
941 for name
, key_list
in self
.get_sequences().items():
943 msg
.add_sequence(name
)
946 def get_string(self
, key
):
947 """Return a string representation or raise a KeyError."""
950 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+', newline
='')
952 f
= open(os
.path
.join(self
._path
, str(key
)), 'r', newline
='')
954 if e
.errno
== errno
.ENOENT
:
955 raise KeyError('No message with key: %s' % key
)
969 def get_file(self
, key
):
970 """Return a file-like representation or raise a KeyError."""
972 f
= open(os
.path
.join(self
._path
, str(key
)), 'r', newline
='')
974 if e
.errno
== errno
.ENOENT
:
975 raise KeyError('No message with key: %s' % key
)
981 """Return an iterator over keys."""
982 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
985 def __contains__(self
, key
):
986 """Return True if the keyed message exists, False otherwise."""
987 return os
.path
.exists(os
.path
.join(self
._path
, str(key
)))
990 """Return a count of messages in the mailbox."""
991 return len(list(self
.keys()))
994 """Lock the mailbox."""
996 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
997 _lock_file(self
._file
)
1001 """Unlock the mailbox if it is locked."""
1003 _unlock_file(self
._file
)
1004 _sync_close(self
._file
)
1006 self
._locked
= False
1009 """Write any pending changes to the disk."""
1013 """Flush and close the mailbox."""
1017 def list_folders(self
):
1018 """Return a list of folder names."""
1020 for entry
in os
.listdir(self
._path
):
1021 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
1022 result
.append(entry
)
1025 def get_folder(self
, folder
):
1026 """Return an MH instance for the named folder."""
1027 return MH(os
.path
.join(self
._path
, folder
),
1028 factory
=self
._factory
, create
=False)
1030 def add_folder(self
, folder
):
1031 """Create a folder and return an MH instance representing it."""
1032 return MH(os
.path
.join(self
._path
, folder
),
1033 factory
=self
._factory
)
1035 def remove_folder(self
, folder
):
1036 """Delete the named folder, which must be empty."""
1037 path
= os
.path
.join(self
._path
, folder
)
1038 entries
= os
.listdir(path
)
1039 if entries
== ['.mh_sequences']:
1040 os
.remove(os
.path
.join(path
, '.mh_sequences'))
1044 raise NotEmptyError('Folder not empty: %s' % self
._path
)
1047 def get_sequences(self
):
1048 """Return a name-to-key-list dictionary to define each sequence."""
1050 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r', newline
='')
1052 all_keys
= set(self
.keys())
1055 name
, contents
= line
.split(':')
1057 for spec
in contents
.split():
1061 start
, stop
= (int(x
) for x
in spec
.split('-'))
1062 keys
.update(range(start
, stop
+ 1))
1063 results
[name
] = [key
for key
in sorted(keys
) \
1065 if len(results
[name
]) == 0:
1068 raise FormatError('Invalid sequence specification: %s' %
1074 def set_sequences(self
, sequences
):
1075 """Set sequences using the given name-to-key-list dictionary."""
1076 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r+', newline
='')
1078 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
1079 for name
, keys
in sequences
.items():
1082 f
.write('%s:' % name
)
1085 for key
in sorted(set(keys
)):
1092 f
.write('%s %s' % (prev
, key
))
1094 f
.write(' %s' % key
)
1097 f
.write(str(prev
) + '\n')
1104 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1105 sequences
= self
.get_sequences()
1108 for key
in self
.keys():
1110 changes
.append((key
, prev
+ 1))
1111 if hasattr(os
, 'link'):
1112 os
.link(os
.path
.join(self
._path
, str(key
)),
1113 os
.path
.join(self
._path
, str(prev
+ 1)))
1114 os
.unlink(os
.path
.join(self
._path
, str(key
)))
1116 os
.rename(os
.path
.join(self
._path
, str(key
)),
1117 os
.path
.join(self
._path
, str(prev
+ 1)))
1119 self
._next
_key
= prev
+ 1
1120 if len(changes
) == 0:
1122 for name
, key_list
in sequences
.items():
1123 for old
, new
in changes
:
1125 key_list
[key_list
.index(old
)] = new
1126 self
.set_sequences(sequences
)
1128 def _dump_sequences(self
, message
, key
):
1129 """Inspect a new MHMessage and update sequences appropriately."""
1130 pending_sequences
= message
.get_sequences()
1131 all_sequences
= self
.get_sequences()
1132 for name
, key_list
in all_sequences
.items():
1133 if name
in pending_sequences
:
1134 key_list
.append(key
)
1135 elif key
in key_list
:
1136 del key_list
[key_list
.index(key
)]
1137 for sequence
in pending_sequences
:
1138 if sequence
not in all_sequences
:
1139 all_sequences
[sequence
] = [key
]
1140 self
.set_sequences(all_sequences
)
1143 class Babyl(_singlefileMailbox
):
1144 """An Rmail-style Babyl mailbox."""
1146 _special_labels
= frozenset(('unseen', 'deleted', 'filed', 'answered',
1147 'forwarded', 'edited', 'resent'))
1149 def __init__(self
, path
, factory
=None, create
=True):
1150 """Initialize a Babyl mailbox."""
1151 _singlefileMailbox
.__init
__(self
, path
, factory
, create
)
1154 def add(self
, message
):
1155 """Add message and return assigned key."""
1156 key
= _singlefileMailbox
.add(self
, message
)
1157 if isinstance(message
, BabylMessage
):
1158 self
._labels
[key
] = message
.get_labels()
1161 def remove(self
, key
):
1162 """Remove the keyed message; raise KeyError if it doesn't exist."""
1163 _singlefileMailbox
.remove(self
, key
)
1164 if key
in self
._labels
:
1165 del self
._labels
[key
]
1167 def __setitem__(self
, key
, message
):
1168 """Replace the keyed message; raise KeyError if it doesn't exist."""
1169 _singlefileMailbox
.__setitem
__(self
, key
, message
)
1170 if isinstance(message
, BabylMessage
):
1171 self
._labels
[key
] = message
.get_labels()
1173 def get_message(self
, key
):
1174 """Return a Message representation or raise a KeyError."""
1175 start
, stop
= self
._lookup
(key
)
1176 self
._file
.seek(start
)
1177 self
._file
.readline() # Skip '1,' line specifying labels.
1178 original_headers
= io
.StringIO()
1180 line
= self
._file
.readline()
1181 if line
== '*** EOOH ***' + os
.linesep
or not line
:
1183 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1184 visible_headers
= io
.StringIO()
1186 line
= self
._file
.readline()
1187 if line
== os
.linesep
or not line
:
1189 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1190 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1192 msg
= BabylMessage(original_headers
.getvalue() + body
)
1193 msg
.set_visible(visible_headers
.getvalue())
1194 if key
in self
._labels
:
1195 msg
.set_labels(self
._labels
[key
])
1198 def get_string(self
, key
):
1199 """Return a string representation or raise a KeyError."""
1200 start
, stop
= self
._lookup
(key
)
1201 self
._file
.seek(start
)
1202 self
._file
.readline() # Skip '1,' line specifying labels.
1203 original_headers
= io
.StringIO()
1205 line
= self
._file
.readline()
1206 if line
== '*** EOOH ***' + os
.linesep
or not line
:
1208 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1210 line
= self
._file
.readline()
1211 if line
== os
.linesep
or not line
:
1213 return original_headers
.getvalue() + \
1214 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1217 def get_file(self
, key
):
1218 """Return a file-like representation or raise a KeyError."""
1219 return io
.StringIO(self
.get_string(key
).replace('\n',
1222 def get_labels(self
):
1223 """Return a list of user-defined labels in the mailbox."""
1226 for label_list
in self
._labels
.values():
1227 labels
.update(label_list
)
1228 labels
.difference_update(self
._special
_labels
)
1231 def _generate_toc(self
):
1232 """Generate key-to-(start, stop) table of contents."""
1233 starts
, stops
= [], []
1239 line
= self
._file
.readline()
1240 next_pos
= self
._file
.tell()
1241 if line
== '\037\014' + os
.linesep
:
1242 if len(stops
) < len(starts
):
1243 stops
.append(line_pos
- len(os
.linesep
))
1244 starts
.append(next_pos
)
1245 labels
= [label
.strip() for label
1246 in self
._file
.readline()[1:].split(',')
1248 label_lists
.append(labels
)
1249 elif line
== '\037' or line
== '\037' + os
.linesep
:
1250 if len(stops
) < len(starts
):
1251 stops
.append(line_pos
- len(os
.linesep
))
1253 stops
.append(line_pos
- len(os
.linesep
))
1255 self
._toc
= dict(enumerate(zip(starts
, stops
)))
1256 self
._labels
= dict(enumerate(label_lists
))
1257 self
._next
_key
= len(self
._toc
)
1258 self
._file
.seek(0, 2)
1259 self
._file
_length
= self
._file
.tell()
1261 def _pre_mailbox_hook(self
, f
):
1262 """Called before writing the mailbox to file f."""
1263 f
.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1264 (os
.linesep
, os
.linesep
, ','.join(self
.get_labels()),
1267 def _pre_message_hook(self
, f
):
1268 """Called before writing each message to file f."""
1269 f
.write('\014' + os
.linesep
)
1271 def _post_message_hook(self
, f
):
1272 """Called after writing each message to file f."""
1273 f
.write(os
.linesep
+ '\037')
1275 def _install_message(self
, message
):
1276 """Write message contents and return (start, stop)."""
1277 start
= self
._file
.tell()
1278 if isinstance(message
, BabylMessage
):
1281 for label
in message
.get_labels():
1282 if label
in self
._special
_labels
:
1283 special_labels
.append(label
)
1285 labels
.append(label
)
1286 self
._file
.write('1')
1287 for label
in special_labels
:
1288 self
._file
.write(', ' + label
)
1289 self
._file
.write(',,')
1290 for label
in labels
:
1291 self
._file
.write(' ' + label
+ ',')
1292 self
._file
.write(os
.linesep
)
1294 self
._file
.write('1,,' + os
.linesep
)
1295 if isinstance(message
, email
.message
.Message
):
1296 orig_buffer
= io
.StringIO()
1297 orig_generator
= email
.generator
.Generator(orig_buffer
, False, 0)
1298 orig_generator
.flatten(message
)
1301 line
= orig_buffer
.readline()
1302 self
._file
.write(line
.replace('\n', os
.linesep
))
1303 if line
== '\n' or not line
:
1305 self
._file
.write('*** EOOH ***' + os
.linesep
)
1306 if isinstance(message
, BabylMessage
):
1307 vis_buffer
= io
.StringIO()
1308 vis_generator
= email
.generator
.Generator(vis_buffer
, False, 0)
1309 vis_generator
.flatten(message
.get_visible())
1311 line
= vis_buffer
.readline()
1312 self
._file
.write(line
.replace('\n', os
.linesep
))
1313 if line
== '\n' or not line
:
1318 line
= orig_buffer
.readline()
1319 self
._file
.write(line
.replace('\n', os
.linesep
))
1320 if line
== '\n' or not line
:
1323 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
1326 self
._file
.write(buffer.replace('\n', os
.linesep
))
1327 elif isinstance(message
, str):
1328 body_start
= message
.find('\n\n') + 2
1329 if body_start
- 2 != -1:
1330 self
._file
.write(message
[:body_start
].replace('\n',
1332 self
._file
.write('*** EOOH ***' + os
.linesep
)
1333 self
._file
.write(message
[:body_start
].replace('\n',
1335 self
._file
.write(message
[body_start
:].replace('\n',
1338 self
._file
.write('*** EOOH ***' + os
.linesep
+ os
.linesep
)
1339 self
._file
.write(message
.replace('\n', os
.linesep
))
1340 elif hasattr(message
, 'readline'):
1341 original_pos
= message
.tell()
1344 line
= message
.readline()
1345 self
._file
.write(line
.replace('\n', os
.linesep
))
1346 if line
== '\n' or not line
:
1347 self
._file
.write('*** EOOH ***' + os
.linesep
)
1350 message
.seek(original_pos
)
1354 buffer = message
.read(4096) # Buffer size is arbitrary.
1357 self
._file
.write(buffer.replace('\n', os
.linesep
))
1359 raise TypeError('Invalid message type: %s' % type(message
))
1360 stop
= self
._file
.tell()
1361 return (start
, stop
)
1364 class Message(email
.message
.Message
):
1365 """Message with mailbox-format-specific properties."""
1367 def __init__(self
, message
=None):
1368 """Initialize a Message instance."""
1369 if isinstance(message
, email
.message
.Message
):
1370 self
._become
_message
(copy
.deepcopy(message
))
1371 if isinstance(message
, Message
):
1372 message
._explain
_to
(self
)
1373 elif isinstance(message
, str):
1374 self
._become
_message
(email
.message_from_string(message
))
1375 elif hasattr(message
, "read"):
1376 self
._become
_message
(email
.message_from_file(message
))
1377 elif message
is None:
1378 email
.message
.Message
.__init
__(self
)
1380 raise TypeError('Invalid message type: %s' % type(message
))
1382 def _become_message(self
, message
):
1383 """Assume the non-format-specific state of message."""
1384 for name
in ('_headers', '_unixfrom', '_payload', '_charset',
1385 'preamble', 'epilogue', 'defects', '_default_type'):
1386 self
.__dict
__[name
] = message
.__dict
__[name
]
1388 def _explain_to(self
, message
):
1389 """Copy format-specific state to message insofar as possible."""
1390 if isinstance(message
, Message
):
1391 return # There's nothing format-specific to explain.
1393 raise TypeError('Cannot convert to specified type')
1396 class MaildirMessage(Message
):
1397 """Message with Maildir-specific properties."""
1399 def __init__(self
, message
=None):
1400 """Initialize a MaildirMessage instance."""
1401 self
._subdir
= 'new'
1403 self
._date
= time
.time()
1404 Message
.__init
__(self
, message
)
1406 def get_subdir(self
):
1407 """Return 'new' or 'cur'."""
1410 def set_subdir(self
, subdir
):
1411 """Set subdir to 'new' or 'cur'."""
1412 if subdir
== 'new' or subdir
== 'cur':
1413 self
._subdir
= subdir
1415 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir
)
1417 def get_flags(self
):
1418 """Return as a string the flags that are set."""
1419 if self
._info
.startswith('2,'):
1420 return self
._info
[2:]
1424 def set_flags(self
, flags
):
1425 """Set the given flags and unset all others."""
1426 self
._info
= '2,' + ''.join(sorted(flags
))
1428 def add_flag(self
, flag
):
1429 """Set the given flag(s) without changing others."""
1430 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1432 def remove_flag(self
, flag
):
1433 """Unset the given string flag(s) without changing others."""
1434 if self
.get_flags():
1435 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1438 """Return delivery date of message, in seconds since the epoch."""
1441 def set_date(self
, date
):
1442 """Set delivery date of message, in seconds since the epoch."""
1444 self
._date
= float(date
)
1446 raise TypeError("can't convert to float: %s" % date
)
1449 """Get the message's "info" as a string."""
1452 def set_info(self
, info
):
1453 """Set the message's "info" string."""
1454 if isinstance(info
, str):
1457 raise TypeError('info must be a string: %s' % type(info
))
1459 def _explain_to(self
, message
):
1460 """Copy Maildir-specific state to message insofar as possible."""
1461 if isinstance(message
, MaildirMessage
):
1462 message
.set_flags(self
.get_flags())
1463 message
.set_subdir(self
.get_subdir())
1464 message
.set_date(self
.get_date())
1465 elif isinstance(message
, _mboxMMDFMessage
):
1466 flags
= set(self
.get_flags())
1468 message
.add_flag('R')
1469 if self
.get_subdir() == 'cur':
1470 message
.add_flag('O')
1472 message
.add_flag('D')
1474 message
.add_flag('F')
1476 message
.add_flag('A')
1477 message
.set_from('MAILER-DAEMON', time
.gmtime(self
.get_date()))
1478 elif isinstance(message
, MHMessage
):
1479 flags
= set(self
.get_flags())
1480 if 'S' not in flags
:
1481 message
.add_sequence('unseen')
1483 message
.add_sequence('replied')
1485 message
.add_sequence('flagged')
1486 elif isinstance(message
, BabylMessage
):
1487 flags
= set(self
.get_flags())
1488 if 'S' not in flags
:
1489 message
.add_label('unseen')
1491 message
.add_label('deleted')
1493 message
.add_label('answered')
1495 message
.add_label('forwarded')
1496 elif isinstance(message
, Message
):
1499 raise TypeError('Cannot convert to specified type: %s' %
1503 class _mboxMMDFMessage(Message
):
1504 """Message with mbox- or MMDF-specific properties."""
1506 def __init__(self
, message
=None):
1507 """Initialize an mboxMMDFMessage instance."""
1508 self
.set_from('MAILER-DAEMON', True)
1509 if isinstance(message
, email
.message
.Message
):
1510 unixfrom
= message
.get_unixfrom()
1511 if unixfrom
is not None and unixfrom
.startswith('From '):
1512 self
.set_from(unixfrom
[5:])
1513 Message
.__init
__(self
, message
)
1516 """Return contents of "From " line."""
1519 def set_from(self
, from_
, time_
=None):
1520 """Set "From " line, formatting and appending time_ if specified."""
1521 if time_
is not None:
1523 time_
= time
.gmtime()
1524 from_
+= ' ' + time
.asctime(time_
)
1527 def get_flags(self
):
1528 """Return as a string the flags that are set."""
1529 return self
.get('Status', '') + self
.get('X-Status', '')
1531 def set_flags(self
, flags
):
1532 """Set the given flags and unset all others."""
1534 status_flags
, xstatus_flags
= '', ''
1535 for flag
in ('R', 'O'):
1537 status_flags
+= flag
1539 for flag
in ('D', 'F', 'A'):
1541 xstatus_flags
+= flag
1543 xstatus_flags
+= ''.join(sorted(flags
))
1545 self
.replace_header('Status', status_flags
)
1547 self
.add_header('Status', status_flags
)
1549 self
.replace_header('X-Status', xstatus_flags
)
1551 self
.add_header('X-Status', xstatus_flags
)
1553 def add_flag(self
, flag
):
1554 """Set the given flag(s) without changing others."""
1555 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1557 def remove_flag(self
, flag
):
1558 """Unset the given string flag(s) without changing others."""
1559 if 'Status' in self
or 'X-Status' in self
:
1560 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1562 def _explain_to(self
, message
):
1563 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1564 if isinstance(message
, MaildirMessage
):
1565 flags
= set(self
.get_flags())
1567 message
.set_subdir('cur')
1569 message
.add_flag('F')
1571 message
.add_flag('R')
1573 message
.add_flag('S')
1575 message
.add_flag('T')
1576 del message
['status']
1577 del message
['x-status']
1578 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1580 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1581 '%a %b %d %H:%M:%S %Y')))
1582 except (ValueError, OverflowError):
1584 elif isinstance(message
, _mboxMMDFMessage
):
1585 message
.set_flags(self
.get_flags())
1586 message
.set_from(self
.get_from())
1587 elif isinstance(message
, MHMessage
):
1588 flags
= set(self
.get_flags())
1589 if 'R' not in flags
:
1590 message
.add_sequence('unseen')
1592 message
.add_sequence('replied')
1594 message
.add_sequence('flagged')
1595 del message
['status']
1596 del message
['x-status']
1597 elif isinstance(message
, BabylMessage
):
1598 flags
= set(self
.get_flags())
1599 if 'R' not in flags
:
1600 message
.add_label('unseen')
1602 message
.add_label('deleted')
1604 message
.add_label('answered')
1605 del message
['status']
1606 del message
['x-status']
1607 elif isinstance(message
, Message
):
1610 raise TypeError('Cannot convert to specified type: %s' %
1614 class mboxMessage(_mboxMMDFMessage
):
1615 """Message with mbox-specific properties."""
1618 class MHMessage(Message
):
1619 """Message with MH-specific properties."""
1621 def __init__(self
, message
=None):
1622 """Initialize an MHMessage instance."""
1623 self
._sequences
= []
1624 Message
.__init
__(self
, message
)
1626 def get_sequences(self
):
1627 """Return a list of sequences that include the message."""
1628 return self
._sequences
[:]
1630 def set_sequences(self
, sequences
):
1631 """Set the list of sequences that include the message."""
1632 self
._sequences
= list(sequences
)
1634 def add_sequence(self
, sequence
):
1635 """Add sequence to list of sequences including the message."""
1636 if isinstance(sequence
, str):
1637 if not sequence
in self
._sequences
:
1638 self
._sequences
.append(sequence
)
1640 raise TypeError('sequence must be a string: %s' % type(sequence
))
1642 def remove_sequence(self
, sequence
):
1643 """Remove sequence from the list of sequences including the message."""
1645 self
._sequences
.remove(sequence
)
1649 def _explain_to(self
, message
):
1650 """Copy MH-specific state to message insofar as possible."""
1651 if isinstance(message
, MaildirMessage
):
1652 sequences
= set(self
.get_sequences())
1653 if 'unseen' in sequences
:
1654 message
.set_subdir('cur')
1656 message
.set_subdir('cur')
1657 message
.add_flag('S')
1658 if 'flagged' in sequences
:
1659 message
.add_flag('F')
1660 if 'replied' in sequences
:
1661 message
.add_flag('R')
1662 elif isinstance(message
, _mboxMMDFMessage
):
1663 sequences
= set(self
.get_sequences())
1664 if 'unseen' not in sequences
:
1665 message
.add_flag('RO')
1667 message
.add_flag('O')
1668 if 'flagged' in sequences
:
1669 message
.add_flag('F')
1670 if 'replied' in sequences
:
1671 message
.add_flag('A')
1672 elif isinstance(message
, MHMessage
):
1673 for sequence
in self
.get_sequences():
1674 message
.add_sequence(sequence
)
1675 elif isinstance(message
, BabylMessage
):
1676 sequences
= set(self
.get_sequences())
1677 if 'unseen' in sequences
:
1678 message
.add_label('unseen')
1679 if 'replied' in sequences
:
1680 message
.add_label('answered')
1681 elif isinstance(message
, Message
):
1684 raise TypeError('Cannot convert to specified type: %s' %
1688 class BabylMessage(Message
):
1689 """Message with Babyl-specific properties."""
1691 def __init__(self
, message
=None):
1692 """Initialize an BabylMessage instance."""
1694 self
._visible
= Message()
1695 Message
.__init
__(self
, message
)
1697 def get_labels(self
):
1698 """Return a list of labels on the message."""
1699 return self
._labels
[:]
1701 def set_labels(self
, labels
):
1702 """Set the list of labels on the message."""
1703 self
._labels
= list(labels
)
1705 def add_label(self
, label
):
1706 """Add label to list of labels on the message."""
1707 if isinstance(label
, str):
1708 if label
not in self
._labels
:
1709 self
._labels
.append(label
)
1711 raise TypeError('label must be a string: %s' % type(label
))
1713 def remove_label(self
, label
):
1714 """Remove label from the list of labels on the message."""
1716 self
._labels
.remove(label
)
1720 def get_visible(self
):
1721 """Return a Message representation of visible headers."""
1722 return Message(self
._visible
)
1724 def set_visible(self
, visible
):
1725 """Set the Message representation of visible headers."""
1726 self
._visible
= Message(visible
)
1728 def update_visible(self
):
1729 """Update and/or sensibly generate a set of visible headers."""
1730 for header
in self
._visible
.keys():
1732 self
._visible
.replace_header(header
, self
[header
])
1734 del self
._visible
[header
]
1735 for header
in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1736 if header
in self
and header
not in self
._visible
:
1737 self
._visible
[header
] = self
[header
]
1739 def _explain_to(self
, message
):
1740 """Copy Babyl-specific state to message insofar as possible."""
1741 if isinstance(message
, MaildirMessage
):
1742 labels
= set(self
.get_labels())
1743 if 'unseen' in labels
:
1744 message
.set_subdir('cur')
1746 message
.set_subdir('cur')
1747 message
.add_flag('S')
1748 if 'forwarded' in labels
or 'resent' in labels
:
1749 message
.add_flag('P')
1750 if 'answered' in labels
:
1751 message
.add_flag('R')
1752 if 'deleted' in labels
:
1753 message
.add_flag('T')
1754 elif isinstance(message
, _mboxMMDFMessage
):
1755 labels
= set(self
.get_labels())
1756 if 'unseen' not in labels
:
1757 message
.add_flag('RO')
1759 message
.add_flag('O')
1760 if 'deleted' in labels
:
1761 message
.add_flag('D')
1762 if 'answered' in labels
:
1763 message
.add_flag('A')
1764 elif isinstance(message
, MHMessage
):
1765 labels
= set(self
.get_labels())
1766 if 'unseen' in labels
:
1767 message
.add_sequence('unseen')
1768 if 'answered' in labels
:
1769 message
.add_sequence('replied')
1770 elif isinstance(message
, BabylMessage
):
1771 message
.set_visible(self
.get_visible())
1772 for label
in self
.get_labels():
1773 message
.add_label(label
)
1774 elif isinstance(message
, Message
):
1777 raise TypeError('Cannot convert to specified type: %s' %
1781 class MMDFMessage(_mboxMMDFMessage
):
1782 """Message with MMDF-specific properties."""
1786 """A read-only wrapper of a file."""
1788 def __init__(self
, f
, pos
=None):
1789 """Initialize a _ProxyFile."""
1792 self
._pos
= f
.tell()
1796 def read(self
, size
=None):
1798 return self
._read
(size
, self
._file
.read
)
1800 def readline(self
, size
=None):
1802 return self
._read
(size
, self
._file
.readline
)
1804 def readlines(self
, sizehint
=None):
1805 """Read multiple lines."""
1809 if sizehint
is not None:
1810 sizehint
-= len(line
)
1816 """Iterate over lines."""
1818 line
= self
.readline()
1824 """Return the position."""
1827 def seek(self
, offset
, whence
=0):
1828 """Change position."""
1830 self
._file
.seek(self
._pos
)
1831 self
._file
.seek(offset
, whence
)
1832 self
._pos
= self
._file
.tell()
1835 """Close the file."""
1838 def _read(self
, size
, read_method
):
1839 """Read size bytes using read_method."""
1842 self
._file
.seek(self
._pos
)
1843 result
= read_method(size
)
1844 self
._pos
= self
._file
.tell()
1848 class _PartialFile(_ProxyFile
):
1849 """A read-only wrapper of part of a file."""
1851 def __init__(self
, f
, start
=None, stop
=None):
1852 """Initialize a _PartialFile."""
1853 _ProxyFile
.__init
__(self
, f
, start
)
1858 """Return the position with respect to start."""
1859 return _ProxyFile
.tell(self
) - self
._start
1861 def seek(self
, offset
, whence
=0):
1862 """Change position, possibly with respect to start or stop."""
1864 self
._pos
= self
._start
1867 self
._pos
= self
._stop
1869 _ProxyFile
.seek(self
, offset
, whence
)
1871 def _read(self
, size
, read_method
):
1872 """Read size bytes using read_method, honoring start and stop."""
1873 remaining
= self
._stop
- self
._pos
1876 if size
is None or size
< 0 or size
> remaining
:
1878 return _ProxyFile
._read
(self
, size
, read_method
)
1881 def _lock_file(f
, dotlock
=True):
1882 """Lock file f using lockf and dot locking."""
1883 dotlock_done
= False
1887 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1888 except IOError as e
:
1889 if e
.errno
in (errno
.EAGAIN
, errno
.EACCES
):
1890 raise ExternalClashError('lockf: lock unavailable: %s' %
1896 pre_lock
= _create_temporary(f
.name
+ '.lock')
1898 except IOError as e
:
1899 if e
.errno
== errno
.EACCES
:
1900 return # Without write access, just skip dotlocking.
1904 if hasattr(os
, 'link'):
1905 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1907 os
.unlink(pre_lock
.name
)
1909 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
1911 except OSError as e
:
1912 if e
.errno
== errno
.EEXIST
or \
1913 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
1914 os
.remove(pre_lock
.name
)
1915 raise ExternalClashError('dot lock unavailable: %s' %
1921 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1923 os
.remove(f
.name
+ '.lock')
1926 def _unlock_file(f
):
1927 """Unlock file f using lockf and dot locking."""
1929 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1930 if os
.path
.exists(f
.name
+ '.lock'):
1931 os
.remove(f
.name
+ '.lock')
1933 def _create_carefully(path
):
1934 """Create a file if it doesn't exist and open for reading and writing."""
1935 fd
= os
.open(path
, os
.O_CREAT | os
.O_EXCL | os
.O_RDWR
, 0o666)
1937 return open(path
, 'r+', newline
='')
1941 def _create_temporary(path
):
1942 """Create a temp file based on path and open for reading and writing."""
1943 return _create_carefully('%s.%s.%s.%s' % (path
, int(time
.time()),
1944 socket
.gethostname(),
1948 """Ensure changes to file f are physically on disk."""
1950 if hasattr(os
, 'fsync'):
1951 os
.fsync(f
.fileno())
1954 """Close file f, ensuring all changes are physically on disk."""
1959 class Error(Exception):
1960 """Raised for module-specific errors."""
1962 class NoSuchMailboxError(Error
):
1963 """The specified mailbox does not exist and won't be created."""
1965 class NotEmptyError(Error
):
1966 """The specified mailbox is not empty and deletion was requested."""
1968 class ExternalClashError(Error
):
1969 """Another process caused an action to fail."""
1971 class FormatError(Error
):
1972 """A file appears to have an invalid format."""