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
238 def add(self
, message
):
239 """Add message and return assigned key."""
240 tmp_file
= self
._create
_tmp
()
242 self
._dump
_message
(message
, tmp_file
)
244 _sync_close(tmp_file
)
245 if isinstance(message
, MaildirMessage
):
246 subdir
= message
.get_subdir()
247 suffix
= self
.colon
+ message
.get_info()
248 if suffix
== self
.colon
:
253 uniq
= os
.path
.basename(tmp_file
.name
).split(self
.colon
)[0]
254 dest
= os
.path
.join(self
._path
, subdir
, uniq
+ suffix
)
256 if hasattr(os
, 'link'):
257 os
.link(tmp_file
.name
, dest
)
258 os
.remove(tmp_file
.name
)
260 os
.rename(tmp_file
.name
, dest
)
262 os
.remove(tmp_file
.name
)
263 if e
.errno
== errno
.EEXIST
:
264 raise ExternalClashError('Name clash with existing message: %s'
268 if isinstance(message
, MaildirMessage
):
269 os
.utime(dest
, (os
.path
.getatime(dest
), message
.get_date()))
272 def remove(self
, key
):
273 """Remove the keyed message; raise KeyError if it doesn't exist."""
274 os
.remove(os
.path
.join(self
._path
, self
._lookup
(key
)))
276 def discard(self
, key
):
277 """If the keyed message exists, remove it."""
278 # This overrides an inapplicable implementation in the superclass.
284 if e
.errno
!= errno
.ENOENT
:
287 def __setitem__(self
, key
, message
):
288 """Replace the keyed message; raise KeyError if it doesn't exist."""
289 old_subpath
= self
._lookup
(key
)
290 temp_key
= self
.add(message
)
291 temp_subpath
= self
._lookup
(temp_key
)
292 if isinstance(message
, MaildirMessage
):
293 # temp's subdir and suffix were specified by message.
294 dominant_subpath
= temp_subpath
296 # temp's subdir and suffix were defaults from add().
297 dominant_subpath
= old_subpath
298 subdir
= os
.path
.dirname(dominant_subpath
)
299 if self
.colon
in dominant_subpath
:
300 suffix
= self
.colon
+ dominant_subpath
.split(self
.colon
)[-1]
304 new_path
= os
.path
.join(self
._path
, subdir
, key
+ suffix
)
305 os
.rename(os
.path
.join(self
._path
, temp_subpath
), new_path
)
306 if isinstance(message
, MaildirMessage
):
307 os
.utime(new_path
, (os
.path
.getatime(new_path
),
310 def get_message(self
, key
):
311 """Return a Message representation or raise a KeyError."""
312 subpath
= self
._lookup
(key
)
313 f
= open(os
.path
.join(self
._path
, subpath
), 'r', newline
='')
316 msg
= self
._factory
(f
)
318 msg
= MaildirMessage(f
)
321 subdir
, name
= os
.path
.split(subpath
)
322 msg
.set_subdir(subdir
)
323 if self
.colon
in name
:
324 msg
.set_info(name
.split(self
.colon
)[-1])
325 msg
.set_date(os
.path
.getmtime(os
.path
.join(self
._path
, subpath
)))
328 def get_string(self
, key
):
329 """Return a string representation or raise a KeyError."""
330 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r', newline
='')
336 def get_file(self
, key
):
337 """Return a file-like representation or raise a KeyError."""
338 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r', newline
='')
342 """Return an iterator over keys."""
344 for key
in self
._toc
:
351 def __contains__(self
, key
):
352 """Return True if the keyed message exists, False otherwise."""
354 return key
in self
._toc
357 """Return a count of messages in the mailbox."""
359 return len(self
._toc
)
362 """Write any pending changes to disk."""
363 return # Maildir changes are always written immediately.
366 """Lock the mailbox."""
370 """Unlock the mailbox if it is locked."""
374 """Flush and close the mailbox."""
377 def list_folders(self
):
378 """Return a list of folder names."""
380 for entry
in os
.listdir(self
._path
):
381 if len(entry
) > 1 and entry
[0] == '.' and \
382 os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
383 result
.append(entry
[1:])
386 def get_folder(self
, folder
):
387 """Return a Maildir instance for the named folder."""
388 return Maildir(os
.path
.join(self
._path
, '.' + folder
),
389 factory
=self
._factory
,
392 def add_folder(self
, folder
):
393 """Create a folder and return a Maildir instance representing it."""
394 path
= os
.path
.join(self
._path
, '.' + folder
)
395 result
= Maildir(path
, factory
=self
._factory
)
396 maildirfolder_path
= os
.path
.join(path
, 'maildirfolder')
397 if not os
.path
.exists(maildirfolder_path
):
398 os
.close(os
.open(maildirfolder_path
, os
.O_CREAT | os
.O_WRONLY
,
402 def remove_folder(self
, folder
):
403 """Delete the named folder, which must be empty."""
404 path
= os
.path
.join(self
._path
, '.' + folder
)
405 for entry
in os
.listdir(os
.path
.join(path
, 'new')) + \
406 os
.listdir(os
.path
.join(path
, 'cur')):
407 if len(entry
) < 1 or entry
[0] != '.':
408 raise NotEmptyError('Folder contains message(s): %s' % folder
)
409 for entry
in os
.listdir(path
):
410 if entry
!= 'new' and entry
!= 'cur' and entry
!= 'tmp' and \
411 os
.path
.isdir(os
.path
.join(path
, entry
)):
412 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
414 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
416 os
.remove(os
.path
.join(root
, entry
))
418 os
.rmdir(os
.path
.join(root
, entry
))
422 """Delete old files in "tmp"."""
424 for entry
in os
.listdir(os
.path
.join(self
._path
, 'tmp')):
425 path
= os
.path
.join(self
._path
, 'tmp', entry
)
426 if now
- os
.path
.getatime(path
) > 129600: # 60 * 60 * 36
429 _count
= 1 # This is used to generate unique file names.
431 def _create_tmp(self
):
432 """Create a file in the tmp subdirectory and open and return it."""
434 hostname
= socket
.gethostname()
436 hostname
= hostname
.replace('/', r
'\057')
438 hostname
= hostname
.replace(':', r
'\072')
439 uniq
= "%s.M%sP%sQ%s.%s" % (int(now
), int(now
% 1 * 1e6
), os
.getpid(),
440 Maildir
._count
, hostname
)
441 path
= os
.path
.join(self
._path
, 'tmp', uniq
)
445 if e
.errno
== errno
.ENOENT
:
448 return _create_carefully(path
)
450 if e
.errno
!= errno
.EEXIST
:
455 # Fall through to here if stat succeeded or open raised EEXIST.
456 raise ExternalClashError('Name clash prevented file creation: %s' %
460 """Update table of contents mapping."""
461 new_mtime
= os
.path
.getmtime(os
.path
.join(self
._path
, 'new'))
462 cur_mtime
= os
.path
.getmtime(os
.path
.join(self
._path
, 'cur'))
464 if (self
._last
_read
is not None and
465 new_mtime
<= self
._last
_read
and cur_mtime
<= self
._last
_read
):
469 def update_dir (subdir
):
470 path
= os
.path
.join(self
._path
, subdir
)
471 for entry
in os
.listdir(path
):
472 p
= os
.path
.join(path
, entry
)
475 uniq
= entry
.split(self
.colon
)[0]
476 self
._toc
[uniq
] = os
.path
.join(subdir
, entry
)
481 # We record the current time - 1sec so that, if _refresh() is called
482 # again in the same second, we will always re-read the mailbox
483 # just in case it's been modified. (os.path.mtime() only has
484 # 1sec resolution.) This results in a few unnecessary re-reads
485 # when _refresh() is called multiple times in the same second,
486 # but once the clock ticks over, we will only re-read as needed.
487 now
= int(time
.time() - 1)
488 self
._last
_read
= time
.time() - 1
490 def _lookup(self
, key
):
491 """Use TOC to return subpath for given key, or raise a KeyError."""
493 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
494 return self
._toc
[key
]
499 return self
._toc
[key
]
501 raise KeyError('No message with key: %s' % key
)
503 # This method is for backward compatibility only.
505 """Return the next message in a one-time iteration."""
506 if not hasattr(self
, '_onetime_keys'):
507 self
._onetime
_keys
= iter(self
.keys())
510 return self
[next(self
._onetime
_keys
)]
511 except StopIteration:
517 class _singlefileMailbox(Mailbox
):
518 """A single-file mailbox."""
520 def __init__(self
, path
, factory
=None, create
=True):
521 """Initialize a single-file mailbox."""
522 Mailbox
.__init
__(self
, path
, factory
, create
)
524 f
= open(self
._path
, 'r+', newline
='')
526 if e
.errno
== errno
.ENOENT
:
528 f
= open(self
._path
, 'w+', newline
='')
530 raise NoSuchMailboxError(self
._path
)
531 elif e
.errno
== errno
.EACCES
:
532 f
= open(self
._path
, 'r', newline
='')
538 self
._pending
= False # No changes require rewriting the file.
540 self
._file
_length
= None # Used to record mailbox size
542 def add(self
, message
):
543 """Add message and return assigned key."""
545 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
548 return self
._next
_key
- 1
550 def remove(self
, key
):
551 """Remove the keyed message; raise KeyError if it doesn't exist."""
556 def __setitem__(self
, key
, message
):
557 """Replace the keyed message; raise KeyError if it doesn't exist."""
559 self
._toc
[key
] = self
._append
_message
(message
)
563 """Return an iterator over keys."""
565 for key
in self
._toc
.keys():
568 def __contains__(self
, key
):
569 """Return True if the keyed message exists, False otherwise."""
571 return key
in self
._toc
574 """Return a count of messages in the mailbox."""
576 return len(self
._toc
)
579 """Lock the mailbox."""
581 _lock_file(self
._file
)
585 """Unlock the mailbox if it is locked."""
587 _unlock_file(self
._file
)
591 """Write any pending changes to disk."""
592 if not self
._pending
:
595 # In order to be writing anything out at all, self._toc must
596 # already have been generated (and presumably has been modified
597 # by adding or deleting an item).
598 assert self
._toc
is not None
600 # Check length of self._file; if it's changed, some other process
601 # has modified the mailbox since we scanned it.
602 self
._file
.seek(0, 2)
603 cur_len
= self
._file
.tell()
604 if cur_len
!= self
._file
_length
:
605 raise ExternalClashError('Size of mailbox file changed '
606 '(expected %i, found %i)' %
607 (self
._file
_length
, cur_len
))
609 new_file
= _create_temporary(self
._path
)
612 self
._pre
_mailbox
_hook
(new_file
)
613 for key
in sorted(self
._toc
.keys()):
614 start
, stop
= self
._toc
[key
]
615 self
._file
.seek(start
)
616 self
._pre
_message
_hook
(new_file
)
617 new_start
= new_file
.tell()
619 buffer = self
._file
.read(min(4096,
620 stop
- self
._file
.tell()))
623 new_file
.write(buffer)
624 new_toc
[key
] = (new_start
, new_file
.tell())
625 self
._post
_message
_hook
(new_file
)
628 os
.remove(new_file
.name
)
630 _sync_close(new_file
)
631 # self._file is about to get replaced, so no need to sync.
634 os
.rename(new_file
.name
, self
._path
)
636 if e
.errno
== errno
.EEXIST
or \
637 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
638 os
.remove(self
._path
)
639 os
.rename(new_file
.name
, self
._path
)
642 self
._file
= open(self
._path
, 'rb+')
644 self
._pending
= False
646 _lock_file(self
._file
, dotlock
=False)
648 def _pre_mailbox_hook(self
, f
):
649 """Called before writing the mailbox to file f."""
652 def _pre_message_hook(self
, f
):
653 """Called before writing each message to file f."""
656 def _post_message_hook(self
, f
):
657 """Called after writing each message to file f."""
661 """Flush and close the mailbox."""
665 self
._file
.close() # Sync has been done by self.flush() above.
667 def _lookup(self
, key
=None):
668 """Return (start, stop) or raise KeyError."""
669 if self
._toc
is None:
673 return self
._toc
[key
]
675 raise KeyError('No message with key: %s' % key
)
677 def _append_message(self
, message
):
678 """Append message to mailbox and return (start, stop) offsets."""
679 self
._file
.seek(0, 2)
680 self
._pre
_message
_hook
(self
._file
)
681 offsets
= self
._install
_message
(message
)
682 self
._post
_message
_hook
(self
._file
)
684 self
._file
_length
= self
._file
.tell() # Record current length of mailbox
689 class _mboxMMDF(_singlefileMailbox
):
690 """An mbox or MMDF mailbox."""
694 def get_message(self
, key
):
695 """Return a Message representation or raise a KeyError."""
696 start
, stop
= self
._lookup
(key
)
697 self
._file
.seek(start
)
698 from_line
= self
._file
.readline().replace(os
.linesep
, '')
699 string
= self
._file
.read(stop
- self
._file
.tell())
700 msg
= self
._message
_factory
(string
.replace(os
.linesep
, '\n'))
701 msg
.set_from(from_line
[5:])
704 def get_string(self
, key
, from_
=False):
705 """Return a string representation or raise a KeyError."""
706 start
, stop
= self
._lookup
(key
)
707 self
._file
.seek(start
)
709 self
._file
.readline()
710 string
= self
._file
.read(stop
- self
._file
.tell())
711 return string
.replace(os
.linesep
, '\n')
713 def get_file(self
, key
, from_
=False):
714 """Return a file-like representation or raise a KeyError."""
715 start
, stop
= self
._lookup
(key
)
716 self
._file
.seek(start
)
718 self
._file
.readline()
719 return _PartialFile(self
._file
, self
._file
.tell(), stop
)
721 def _install_message(self
, message
):
722 """Format a message and blindly write to self._file."""
724 if isinstance(message
, str) and message
.startswith('From '):
725 newline
= message
.find('\n')
727 from_line
= message
[:newline
]
728 message
= message
[newline
+ 1:]
732 elif isinstance(message
, _mboxMMDFMessage
):
733 from_line
= 'From ' + message
.get_from()
734 elif isinstance(message
, email
.message
.Message
):
735 from_line
= message
.get_unixfrom() # May be None.
736 if from_line
is None:
737 from_line
= 'From MAILER-DAEMON %s' % time
.asctime(time
.gmtime())
738 start
= self
._file
.tell()
739 self
._file
.write(from_line
+ os
.linesep
)
740 self
._dump
_message
(message
, self
._file
, self
._mangle
_from
_)
741 stop
= self
._file
.tell()
745 class mbox(_mboxMMDF
):
746 """A classic mbox mailbox."""
750 def __init__(self
, path
, factory
=None, create
=True):
751 """Initialize an mbox mailbox."""
752 self
._message
_factory
= mboxMessage
753 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
755 def _pre_message_hook(self
, f
):
756 """Called before writing each message to file f."""
760 def _generate_toc(self
):
761 """Generate key-to-(start, stop) table of contents."""
762 starts
, stops
= [], []
765 line_pos
= self
._file
.tell()
766 line
= self
._file
.readline()
767 if line
.startswith('From '):
768 if len(stops
) < len(starts
):
769 stops
.append(line_pos
- len(os
.linesep
))
770 starts
.append(line_pos
)
772 stops
.append(line_pos
)
774 self
._toc
= dict(enumerate(zip(starts
, stops
)))
775 self
._next
_key
= len(self
._toc
)
776 self
._file
_length
= self
._file
.tell()
779 class MMDF(_mboxMMDF
):
780 """An MMDF mailbox."""
782 def __init__(self
, path
, factory
=None, create
=True):
783 """Initialize an MMDF mailbox."""
784 self
._message
_factory
= MMDFMessage
785 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
787 def _pre_message_hook(self
, f
):
788 """Called before writing each message to file f."""
789 f
.write('\001\001\001\001' + os
.linesep
)
791 def _post_message_hook(self
, f
):
792 """Called after writing each message to file f."""
793 f
.write(os
.linesep
+ '\001\001\001\001' + os
.linesep
)
795 def _generate_toc(self
):
796 """Generate key-to-(start, stop) table of contents."""
797 starts
, stops
= [], []
802 line
= self
._file
.readline()
803 next_pos
= self
._file
.tell()
804 if line
.startswith('\001\001\001\001' + os
.linesep
):
805 starts
.append(next_pos
)
808 line
= self
._file
.readline()
809 next_pos
= self
._file
.tell()
810 if line
== '\001\001\001\001' + os
.linesep
:
811 stops
.append(line_pos
- len(os
.linesep
))
814 stops
.append(line_pos
)
818 self
._toc
= dict(enumerate(zip(starts
, stops
)))
819 self
._next
_key
= len(self
._toc
)
820 self
._file
.seek(0, 2)
821 self
._file
_length
= self
._file
.tell()
827 def __init__(self
, path
, factory
=None, create
=True):
828 """Initialize an MH instance."""
829 Mailbox
.__init
__(self
, path
, factory
, create
)
830 if not os
.path
.exists(self
._path
):
832 os
.mkdir(self
._path
, 0o700)
833 os
.close(os
.open(os
.path
.join(self
._path
, '.mh_sequences'),
834 os
.O_CREAT | os
.O_EXCL | os
.O_WRONLY
, 0o600))
836 raise NoSuchMailboxError(self
._path
)
839 def add(self
, message
):
840 """Add message and return assigned key."""
845 new_key
= max(keys
) + 1
846 new_path
= os
.path
.join(self
._path
, str(new_key
))
847 f
= _create_carefully(new_path
)
852 self
._dump
_message
(message
, f
)
853 if isinstance(message
, MHMessage
):
854 self
._dump
_sequences
(message
, new_key
)
862 def remove(self
, key
):
863 """Remove the keyed message; raise KeyError if it doesn't exist."""
864 path
= os
.path
.join(self
._path
, str(key
))
866 f
= open(path
, 'rb+')
868 if e
.errno
== errno
.ENOENT
:
869 raise KeyError('No message with key: %s' % key
)
877 os
.remove(os
.path
.join(self
._path
, str(key
)))
884 def __setitem__(self
, key
, message
):
885 """Replace the keyed message; raise KeyError if it doesn't exist."""
886 path
= os
.path
.join(self
._path
, str(key
))
888 f
= open(path
, 'r+', newline
='')
890 if e
.errno
== errno
.ENOENT
:
891 raise KeyError('No message with key: %s' % key
)
898 os
.close(os
.open(path
, os
.O_WRONLY | os
.O_TRUNC
))
899 self
._dump
_message
(message
, f
)
900 if isinstance(message
, MHMessage
):
901 self
._dump
_sequences
(message
, key
)
908 def get_message(self
, key
):
909 """Return a Message representation or raise a KeyError."""
912 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+', newline
='')
914 f
= open(os
.path
.join(self
._path
, str(key
)), 'r', newline
='')
916 if e
.errno
== errno
.ENOENT
:
917 raise KeyError('No message with key: %s' % key
)
930 for name
, key_list
in self
.get_sequences().items():
932 msg
.add_sequence(name
)
935 def get_string(self
, key
):
936 """Return a string representation or raise a KeyError."""
939 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+', newline
='')
941 f
= open(os
.path
.join(self
._path
, str(key
)), 'r', newline
='')
943 if e
.errno
== errno
.ENOENT
:
944 raise KeyError('No message with key: %s' % key
)
958 def get_file(self
, key
):
959 """Return a file-like representation or raise a KeyError."""
961 f
= open(os
.path
.join(self
._path
, str(key
)), 'r', newline
='')
963 if e
.errno
== errno
.ENOENT
:
964 raise KeyError('No message with key: %s' % key
)
970 """Return an iterator over keys."""
971 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
974 def __contains__(self
, key
):
975 """Return True if the keyed message exists, False otherwise."""
976 return os
.path
.exists(os
.path
.join(self
._path
, str(key
)))
979 """Return a count of messages in the mailbox."""
980 return len(list(self
.keys()))
983 """Lock the mailbox."""
985 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
986 _lock_file(self
._file
)
990 """Unlock the mailbox if it is locked."""
992 _unlock_file(self
._file
)
993 _sync_close(self
._file
)
998 """Write any pending changes to the disk."""
1002 """Flush and close the mailbox."""
1006 def list_folders(self
):
1007 """Return a list of folder names."""
1009 for entry
in os
.listdir(self
._path
):
1010 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
1011 result
.append(entry
)
1014 def get_folder(self
, folder
):
1015 """Return an MH instance for the named folder."""
1016 return MH(os
.path
.join(self
._path
, folder
),
1017 factory
=self
._factory
, create
=False)
1019 def add_folder(self
, folder
):
1020 """Create a folder and return an MH instance representing it."""
1021 return MH(os
.path
.join(self
._path
, folder
),
1022 factory
=self
._factory
)
1024 def remove_folder(self
, folder
):
1025 """Delete the named folder, which must be empty."""
1026 path
= os
.path
.join(self
._path
, folder
)
1027 entries
= os
.listdir(path
)
1028 if entries
== ['.mh_sequences']:
1029 os
.remove(os
.path
.join(path
, '.mh_sequences'))
1033 raise NotEmptyError('Folder not empty: %s' % self
._path
)
1036 def get_sequences(self
):
1037 """Return a name-to-key-list dictionary to define each sequence."""
1039 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r', newline
='')
1041 all_keys
= set(self
.keys())
1044 name
, contents
= line
.split(':')
1046 for spec
in contents
.split():
1050 start
, stop
= (int(x
) for x
in spec
.split('-'))
1051 keys
.update(range(start
, stop
+ 1))
1052 results
[name
] = [key
for key
in sorted(keys
) \
1054 if len(results
[name
]) == 0:
1057 raise FormatError('Invalid sequence specification: %s' %
1063 def set_sequences(self
, sequences
):
1064 """Set sequences using the given name-to-key-list dictionary."""
1065 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r+', newline
='')
1067 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
1068 for name
, keys
in sequences
.items():
1071 f
.write('%s:' % name
)
1074 for key
in sorted(set(keys
)):
1081 f
.write('%s %s' % (prev
, key
))
1083 f
.write(' %s' % key
)
1086 f
.write(str(prev
) + '\n')
1093 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1094 sequences
= self
.get_sequences()
1097 for key
in self
.keys():
1099 changes
.append((key
, prev
+ 1))
1100 if hasattr(os
, 'link'):
1101 os
.link(os
.path
.join(self
._path
, str(key
)),
1102 os
.path
.join(self
._path
, str(prev
+ 1)))
1103 os
.unlink(os
.path
.join(self
._path
, str(key
)))
1105 os
.rename(os
.path
.join(self
._path
, str(key
)),
1106 os
.path
.join(self
._path
, str(prev
+ 1)))
1108 self
._next
_key
= prev
+ 1
1109 if len(changes
) == 0:
1111 for name
, key_list
in sequences
.items():
1112 for old
, new
in changes
:
1114 key_list
[key_list
.index(old
)] = new
1115 self
.set_sequences(sequences
)
1117 def _dump_sequences(self
, message
, key
):
1118 """Inspect a new MHMessage and update sequences appropriately."""
1119 pending_sequences
= message
.get_sequences()
1120 all_sequences
= self
.get_sequences()
1121 for name
, key_list
in all_sequences
.items():
1122 if name
in pending_sequences
:
1123 key_list
.append(key
)
1124 elif key
in key_list
:
1125 del key_list
[key_list
.index(key
)]
1126 for sequence
in pending_sequences
:
1127 if sequence
not in all_sequences
:
1128 all_sequences
[sequence
] = [key
]
1129 self
.set_sequences(all_sequences
)
1132 class Babyl(_singlefileMailbox
):
1133 """An Rmail-style Babyl mailbox."""
1135 _special_labels
= frozenset(('unseen', 'deleted', 'filed', 'answered',
1136 'forwarded', 'edited', 'resent'))
1138 def __init__(self
, path
, factory
=None, create
=True):
1139 """Initialize a Babyl mailbox."""
1140 _singlefileMailbox
.__init
__(self
, path
, factory
, create
)
1143 def add(self
, message
):
1144 """Add message and return assigned key."""
1145 key
= _singlefileMailbox
.add(self
, message
)
1146 if isinstance(message
, BabylMessage
):
1147 self
._labels
[key
] = message
.get_labels()
1150 def remove(self
, key
):
1151 """Remove the keyed message; raise KeyError if it doesn't exist."""
1152 _singlefileMailbox
.remove(self
, key
)
1153 if key
in self
._labels
:
1154 del self
._labels
[key
]
1156 def __setitem__(self
, key
, message
):
1157 """Replace the keyed message; raise KeyError if it doesn't exist."""
1158 _singlefileMailbox
.__setitem
__(self
, key
, message
)
1159 if isinstance(message
, BabylMessage
):
1160 self
._labels
[key
] = message
.get_labels()
1162 def get_message(self
, key
):
1163 """Return a Message representation or raise a KeyError."""
1164 start
, stop
= self
._lookup
(key
)
1165 self
._file
.seek(start
)
1166 self
._file
.readline() # Skip '1,' line specifying labels.
1167 original_headers
= io
.StringIO()
1169 line
= self
._file
.readline()
1170 if line
== '*** EOOH ***' + os
.linesep
or not line
:
1172 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1173 visible_headers
= io
.StringIO()
1175 line
= self
._file
.readline()
1176 if line
== os
.linesep
or not line
:
1178 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1179 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1181 msg
= BabylMessage(original_headers
.getvalue() + body
)
1182 msg
.set_visible(visible_headers
.getvalue())
1183 if key
in self
._labels
:
1184 msg
.set_labels(self
._labels
[key
])
1187 def get_string(self
, key
):
1188 """Return a string representation or raise a KeyError."""
1189 start
, stop
= self
._lookup
(key
)
1190 self
._file
.seek(start
)
1191 self
._file
.readline() # Skip '1,' line specifying labels.
1192 original_headers
= io
.StringIO()
1194 line
= self
._file
.readline()
1195 if line
== '*** EOOH ***' + os
.linesep
or not line
:
1197 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1199 line
= self
._file
.readline()
1200 if line
== os
.linesep
or not line
:
1202 return original_headers
.getvalue() + \
1203 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1206 def get_file(self
, key
):
1207 """Return a file-like representation or raise a KeyError."""
1208 return io
.StringIO(self
.get_string(key
).replace('\n',
1211 def get_labels(self
):
1212 """Return a list of user-defined labels in the mailbox."""
1215 for label_list
in self
._labels
.values():
1216 labels
.update(label_list
)
1217 labels
.difference_update(self
._special
_labels
)
1220 def _generate_toc(self
):
1221 """Generate key-to-(start, stop) table of contents."""
1222 starts
, stops
= [], []
1228 line
= self
._file
.readline()
1229 next_pos
= self
._file
.tell()
1230 if line
== '\037\014' + os
.linesep
:
1231 if len(stops
) < len(starts
):
1232 stops
.append(line_pos
- len(os
.linesep
))
1233 starts
.append(next_pos
)
1234 labels
= [label
.strip() for label
1235 in self
._file
.readline()[1:].split(',')
1237 label_lists
.append(labels
)
1238 elif line
== '\037' or line
== '\037' + os
.linesep
:
1239 if len(stops
) < len(starts
):
1240 stops
.append(line_pos
- len(os
.linesep
))
1242 stops
.append(line_pos
- len(os
.linesep
))
1244 self
._toc
= dict(enumerate(zip(starts
, stops
)))
1245 self
._labels
= dict(enumerate(label_lists
))
1246 self
._next
_key
= len(self
._toc
)
1247 self
._file
.seek(0, 2)
1248 self
._file
_length
= self
._file
.tell()
1250 def _pre_mailbox_hook(self
, f
):
1251 """Called before writing the mailbox to file f."""
1252 f
.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1253 (os
.linesep
, os
.linesep
, ','.join(self
.get_labels()),
1256 def _pre_message_hook(self
, f
):
1257 """Called before writing each message to file f."""
1258 f
.write('\014' + os
.linesep
)
1260 def _post_message_hook(self
, f
):
1261 """Called after writing each message to file f."""
1262 f
.write(os
.linesep
+ '\037')
1264 def _install_message(self
, message
):
1265 """Write message contents and return (start, stop)."""
1266 start
= self
._file
.tell()
1267 if isinstance(message
, BabylMessage
):
1270 for label
in message
.get_labels():
1271 if label
in self
._special
_labels
:
1272 special_labels
.append(label
)
1274 labels
.append(label
)
1275 self
._file
.write('1')
1276 for label
in special_labels
:
1277 self
._file
.write(', ' + label
)
1278 self
._file
.write(',,')
1279 for label
in labels
:
1280 self
._file
.write(' ' + label
+ ',')
1281 self
._file
.write(os
.linesep
)
1283 self
._file
.write('1,,' + os
.linesep
)
1284 if isinstance(message
, email
.message
.Message
):
1285 orig_buffer
= io
.StringIO()
1286 orig_generator
= email
.generator
.Generator(orig_buffer
, False, 0)
1287 orig_generator
.flatten(message
)
1290 line
= orig_buffer
.readline()
1291 self
._file
.write(line
.replace('\n', os
.linesep
))
1292 if line
== '\n' or not line
:
1294 self
._file
.write('*** EOOH ***' + os
.linesep
)
1295 if isinstance(message
, BabylMessage
):
1296 vis_buffer
= io
.StringIO()
1297 vis_generator
= email
.generator
.Generator(vis_buffer
, False, 0)
1298 vis_generator
.flatten(message
.get_visible())
1300 line
= vis_buffer
.readline()
1301 self
._file
.write(line
.replace('\n', os
.linesep
))
1302 if line
== '\n' or not line
:
1307 line
= orig_buffer
.readline()
1308 self
._file
.write(line
.replace('\n', os
.linesep
))
1309 if line
== '\n' or not line
:
1312 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
1315 self
._file
.write(buffer.replace('\n', os
.linesep
))
1316 elif isinstance(message
, str):
1317 body_start
= message
.find('\n\n') + 2
1318 if body_start
- 2 != -1:
1319 self
._file
.write(message
[:body_start
].replace('\n',
1321 self
._file
.write('*** EOOH ***' + os
.linesep
)
1322 self
._file
.write(message
[:body_start
].replace('\n',
1324 self
._file
.write(message
[body_start
:].replace('\n',
1327 self
._file
.write('*** EOOH ***' + os
.linesep
+ os
.linesep
)
1328 self
._file
.write(message
.replace('\n', os
.linesep
))
1329 elif hasattr(message
, 'readline'):
1330 original_pos
= message
.tell()
1333 line
= message
.readline()
1334 self
._file
.write(line
.replace('\n', os
.linesep
))
1335 if line
== '\n' or not line
:
1336 self
._file
.write('*** EOOH ***' + os
.linesep
)
1339 message
.seek(original_pos
)
1343 buffer = message
.read(4096) # Buffer size is arbitrary.
1346 self
._file
.write(buffer.replace('\n', os
.linesep
))
1348 raise TypeError('Invalid message type: %s' % type(message
))
1349 stop
= self
._file
.tell()
1350 return (start
, stop
)
1353 class Message(email
.message
.Message
):
1354 """Message with mailbox-format-specific properties."""
1356 def __init__(self
, message
=None):
1357 """Initialize a Message instance."""
1358 if isinstance(message
, email
.message
.Message
):
1359 self
._become
_message
(copy
.deepcopy(message
))
1360 if isinstance(message
, Message
):
1361 message
._explain
_to
(self
)
1362 elif isinstance(message
, str):
1363 self
._become
_message
(email
.message_from_string(message
))
1364 elif hasattr(message
, "read"):
1365 self
._become
_message
(email
.message_from_file(message
))
1366 elif message
is None:
1367 email
.message
.Message
.__init
__(self
)
1369 raise TypeError('Invalid message type: %s' % type(message
))
1371 def _become_message(self
, message
):
1372 """Assume the non-format-specific state of message."""
1373 for name
in ('_headers', '_unixfrom', '_payload', '_charset',
1374 'preamble', 'epilogue', 'defects', '_default_type'):
1375 self
.__dict
__[name
] = message
.__dict
__[name
]
1377 def _explain_to(self
, message
):
1378 """Copy format-specific state to message insofar as possible."""
1379 if isinstance(message
, Message
):
1380 return # There's nothing format-specific to explain.
1382 raise TypeError('Cannot convert to specified type')
1385 class MaildirMessage(Message
):
1386 """Message with Maildir-specific properties."""
1388 def __init__(self
, message
=None):
1389 """Initialize a MaildirMessage instance."""
1390 self
._subdir
= 'new'
1392 self
._date
= time
.time()
1393 Message
.__init
__(self
, message
)
1395 def get_subdir(self
):
1396 """Return 'new' or 'cur'."""
1399 def set_subdir(self
, subdir
):
1400 """Set subdir to 'new' or 'cur'."""
1401 if subdir
== 'new' or subdir
== 'cur':
1402 self
._subdir
= subdir
1404 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir
)
1406 def get_flags(self
):
1407 """Return as a string the flags that are set."""
1408 if self
._info
.startswith('2,'):
1409 return self
._info
[2:]
1413 def set_flags(self
, flags
):
1414 """Set the given flags and unset all others."""
1415 self
._info
= '2,' + ''.join(sorted(flags
))
1417 def add_flag(self
, flag
):
1418 """Set the given flag(s) without changing others."""
1419 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1421 def remove_flag(self
, flag
):
1422 """Unset the given string flag(s) without changing others."""
1423 if self
.get_flags():
1424 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1427 """Return delivery date of message, in seconds since the epoch."""
1430 def set_date(self
, date
):
1431 """Set delivery date of message, in seconds since the epoch."""
1433 self
._date
= float(date
)
1435 raise TypeError("can't convert to float: %s" % date
)
1438 """Get the message's "info" as a string."""
1441 def set_info(self
, info
):
1442 """Set the message's "info" string."""
1443 if isinstance(info
, str):
1446 raise TypeError('info must be a string: %s' % type(info
))
1448 def _explain_to(self
, message
):
1449 """Copy Maildir-specific state to message insofar as possible."""
1450 if isinstance(message
, MaildirMessage
):
1451 message
.set_flags(self
.get_flags())
1452 message
.set_subdir(self
.get_subdir())
1453 message
.set_date(self
.get_date())
1454 elif isinstance(message
, _mboxMMDFMessage
):
1455 flags
= set(self
.get_flags())
1457 message
.add_flag('R')
1458 if self
.get_subdir() == 'cur':
1459 message
.add_flag('O')
1461 message
.add_flag('D')
1463 message
.add_flag('F')
1465 message
.add_flag('A')
1466 message
.set_from('MAILER-DAEMON', time
.gmtime(self
.get_date()))
1467 elif isinstance(message
, MHMessage
):
1468 flags
= set(self
.get_flags())
1469 if 'S' not in flags
:
1470 message
.add_sequence('unseen')
1472 message
.add_sequence('replied')
1474 message
.add_sequence('flagged')
1475 elif isinstance(message
, BabylMessage
):
1476 flags
= set(self
.get_flags())
1477 if 'S' not in flags
:
1478 message
.add_label('unseen')
1480 message
.add_label('deleted')
1482 message
.add_label('answered')
1484 message
.add_label('forwarded')
1485 elif isinstance(message
, Message
):
1488 raise TypeError('Cannot convert to specified type: %s' %
1492 class _mboxMMDFMessage(Message
):
1493 """Message with mbox- or MMDF-specific properties."""
1495 def __init__(self
, message
=None):
1496 """Initialize an mboxMMDFMessage instance."""
1497 self
.set_from('MAILER-DAEMON', True)
1498 if isinstance(message
, email
.message
.Message
):
1499 unixfrom
= message
.get_unixfrom()
1500 if unixfrom
is not None and unixfrom
.startswith('From '):
1501 self
.set_from(unixfrom
[5:])
1502 Message
.__init
__(self
, message
)
1505 """Return contents of "From " line."""
1508 def set_from(self
, from_
, time_
=None):
1509 """Set "From " line, formatting and appending time_ if specified."""
1510 if time_
is not None:
1512 time_
= time
.gmtime()
1513 from_
+= ' ' + time
.asctime(time_
)
1516 def get_flags(self
):
1517 """Return as a string the flags that are set."""
1518 return self
.get('Status', '') + self
.get('X-Status', '')
1520 def set_flags(self
, flags
):
1521 """Set the given flags and unset all others."""
1523 status_flags
, xstatus_flags
= '', ''
1524 for flag
in ('R', 'O'):
1526 status_flags
+= flag
1528 for flag
in ('D', 'F', 'A'):
1530 xstatus_flags
+= flag
1532 xstatus_flags
+= ''.join(sorted(flags
))
1534 self
.replace_header('Status', status_flags
)
1536 self
.add_header('Status', status_flags
)
1538 self
.replace_header('X-Status', xstatus_flags
)
1540 self
.add_header('X-Status', xstatus_flags
)
1542 def add_flag(self
, flag
):
1543 """Set the given flag(s) without changing others."""
1544 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1546 def remove_flag(self
, flag
):
1547 """Unset the given string flag(s) without changing others."""
1548 if 'Status' in self
or 'X-Status' in self
:
1549 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1551 def _explain_to(self
, message
):
1552 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1553 if isinstance(message
, MaildirMessage
):
1554 flags
= set(self
.get_flags())
1556 message
.set_subdir('cur')
1558 message
.add_flag('F')
1560 message
.add_flag('R')
1562 message
.add_flag('S')
1564 message
.add_flag('T')
1565 del message
['status']
1566 del message
['x-status']
1567 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1569 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1570 '%a %b %d %H:%M:%S %Y')))
1571 except (ValueError, OverflowError):
1573 elif isinstance(message
, _mboxMMDFMessage
):
1574 message
.set_flags(self
.get_flags())
1575 message
.set_from(self
.get_from())
1576 elif isinstance(message
, MHMessage
):
1577 flags
= set(self
.get_flags())
1578 if 'R' not in flags
:
1579 message
.add_sequence('unseen')
1581 message
.add_sequence('replied')
1583 message
.add_sequence('flagged')
1584 del message
['status']
1585 del message
['x-status']
1586 elif isinstance(message
, BabylMessage
):
1587 flags
= set(self
.get_flags())
1588 if 'R' not in flags
:
1589 message
.add_label('unseen')
1591 message
.add_label('deleted')
1593 message
.add_label('answered')
1594 del message
['status']
1595 del message
['x-status']
1596 elif isinstance(message
, Message
):
1599 raise TypeError('Cannot convert to specified type: %s' %
1603 class mboxMessage(_mboxMMDFMessage
):
1604 """Message with mbox-specific properties."""
1607 class MHMessage(Message
):
1608 """Message with MH-specific properties."""
1610 def __init__(self
, message
=None):
1611 """Initialize an MHMessage instance."""
1612 self
._sequences
= []
1613 Message
.__init
__(self
, message
)
1615 def get_sequences(self
):
1616 """Return a list of sequences that include the message."""
1617 return self
._sequences
[:]
1619 def set_sequences(self
, sequences
):
1620 """Set the list of sequences that include the message."""
1621 self
._sequences
= list(sequences
)
1623 def add_sequence(self
, sequence
):
1624 """Add sequence to list of sequences including the message."""
1625 if isinstance(sequence
, str):
1626 if not sequence
in self
._sequences
:
1627 self
._sequences
.append(sequence
)
1629 raise TypeError('sequence must be a string: %s' % type(sequence
))
1631 def remove_sequence(self
, sequence
):
1632 """Remove sequence from the list of sequences including the message."""
1634 self
._sequences
.remove(sequence
)
1638 def _explain_to(self
, message
):
1639 """Copy MH-specific state to message insofar as possible."""
1640 if isinstance(message
, MaildirMessage
):
1641 sequences
= set(self
.get_sequences())
1642 if 'unseen' in sequences
:
1643 message
.set_subdir('cur')
1645 message
.set_subdir('cur')
1646 message
.add_flag('S')
1647 if 'flagged' in sequences
:
1648 message
.add_flag('F')
1649 if 'replied' in sequences
:
1650 message
.add_flag('R')
1651 elif isinstance(message
, _mboxMMDFMessage
):
1652 sequences
= set(self
.get_sequences())
1653 if 'unseen' not in sequences
:
1654 message
.add_flag('RO')
1656 message
.add_flag('O')
1657 if 'flagged' in sequences
:
1658 message
.add_flag('F')
1659 if 'replied' in sequences
:
1660 message
.add_flag('A')
1661 elif isinstance(message
, MHMessage
):
1662 for sequence
in self
.get_sequences():
1663 message
.add_sequence(sequence
)
1664 elif isinstance(message
, BabylMessage
):
1665 sequences
= set(self
.get_sequences())
1666 if 'unseen' in sequences
:
1667 message
.add_label('unseen')
1668 if 'replied' in sequences
:
1669 message
.add_label('answered')
1670 elif isinstance(message
, Message
):
1673 raise TypeError('Cannot convert to specified type: %s' %
1677 class BabylMessage(Message
):
1678 """Message with Babyl-specific properties."""
1680 def __init__(self
, message
=None):
1681 """Initialize an BabylMessage instance."""
1683 self
._visible
= Message()
1684 Message
.__init
__(self
, message
)
1686 def get_labels(self
):
1687 """Return a list of labels on the message."""
1688 return self
._labels
[:]
1690 def set_labels(self
, labels
):
1691 """Set the list of labels on the message."""
1692 self
._labels
= list(labels
)
1694 def add_label(self
, label
):
1695 """Add label to list of labels on the message."""
1696 if isinstance(label
, str):
1697 if label
not in self
._labels
:
1698 self
._labels
.append(label
)
1700 raise TypeError('label must be a string: %s' % type(label
))
1702 def remove_label(self
, label
):
1703 """Remove label from the list of labels on the message."""
1705 self
._labels
.remove(label
)
1709 def get_visible(self
):
1710 """Return a Message representation of visible headers."""
1711 return Message(self
._visible
)
1713 def set_visible(self
, visible
):
1714 """Set the Message representation of visible headers."""
1715 self
._visible
= Message(visible
)
1717 def update_visible(self
):
1718 """Update and/or sensibly generate a set of visible headers."""
1719 for header
in self
._visible
.keys():
1721 self
._visible
.replace_header(header
, self
[header
])
1723 del self
._visible
[header
]
1724 for header
in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1725 if header
in self
and header
not in self
._visible
:
1726 self
._visible
[header
] = self
[header
]
1728 def _explain_to(self
, message
):
1729 """Copy Babyl-specific state to message insofar as possible."""
1730 if isinstance(message
, MaildirMessage
):
1731 labels
= set(self
.get_labels())
1732 if 'unseen' in labels
:
1733 message
.set_subdir('cur')
1735 message
.set_subdir('cur')
1736 message
.add_flag('S')
1737 if 'forwarded' in labels
or 'resent' in labels
:
1738 message
.add_flag('P')
1739 if 'answered' in labels
:
1740 message
.add_flag('R')
1741 if 'deleted' in labels
:
1742 message
.add_flag('T')
1743 elif isinstance(message
, _mboxMMDFMessage
):
1744 labels
= set(self
.get_labels())
1745 if 'unseen' not in labels
:
1746 message
.add_flag('RO')
1748 message
.add_flag('O')
1749 if 'deleted' in labels
:
1750 message
.add_flag('D')
1751 if 'answered' in labels
:
1752 message
.add_flag('A')
1753 elif isinstance(message
, MHMessage
):
1754 labels
= set(self
.get_labels())
1755 if 'unseen' in labels
:
1756 message
.add_sequence('unseen')
1757 if 'answered' in labels
:
1758 message
.add_sequence('replied')
1759 elif isinstance(message
, BabylMessage
):
1760 message
.set_visible(self
.get_visible())
1761 for label
in self
.get_labels():
1762 message
.add_label(label
)
1763 elif isinstance(message
, Message
):
1766 raise TypeError('Cannot convert to specified type: %s' %
1770 class MMDFMessage(_mboxMMDFMessage
):
1771 """Message with MMDF-specific properties."""
1775 """A read-only wrapper of a file."""
1777 def __init__(self
, f
, pos
=None):
1778 """Initialize a _ProxyFile."""
1781 self
._pos
= f
.tell()
1785 def read(self
, size
=None):
1787 return self
._read
(size
, self
._file
.read
)
1789 def readline(self
, size
=None):
1791 return self
._read
(size
, self
._file
.readline
)
1793 def readlines(self
, sizehint
=None):
1794 """Read multiple lines."""
1798 if sizehint
is not None:
1799 sizehint
-= len(line
)
1805 """Iterate over lines."""
1807 line
= self
.readline()
1813 """Return the position."""
1816 def seek(self
, offset
, whence
=0):
1817 """Change position."""
1819 self
._file
.seek(self
._pos
)
1820 self
._file
.seek(offset
, whence
)
1821 self
._pos
= self
._file
.tell()
1824 """Close the file."""
1827 def _read(self
, size
, read_method
):
1828 """Read size bytes using read_method."""
1831 self
._file
.seek(self
._pos
)
1832 result
= read_method(size
)
1833 self
._pos
= self
._file
.tell()
1837 class _PartialFile(_ProxyFile
):
1838 """A read-only wrapper of part of a file."""
1840 def __init__(self
, f
, start
=None, stop
=None):
1841 """Initialize a _PartialFile."""
1842 _ProxyFile
.__init
__(self
, f
, start
)
1847 """Return the position with respect to start."""
1848 return _ProxyFile
.tell(self
) - self
._start
1850 def seek(self
, offset
, whence
=0):
1851 """Change position, possibly with respect to start or stop."""
1853 self
._pos
= self
._start
1856 self
._pos
= self
._stop
1858 _ProxyFile
.seek(self
, offset
, whence
)
1860 def _read(self
, size
, read_method
):
1861 """Read size bytes using read_method, honoring start and stop."""
1862 remaining
= self
._stop
- self
._pos
1865 if size
is None or size
< 0 or size
> remaining
:
1867 return _ProxyFile
._read
(self
, size
, read_method
)
1870 def _lock_file(f
, dotlock
=True):
1871 """Lock file f using lockf and dot locking."""
1872 dotlock_done
= False
1876 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1877 except IOError as e
:
1878 if e
.errno
in (errno
.EAGAIN
, errno
.EACCES
):
1879 raise ExternalClashError('lockf: lock unavailable: %s' %
1885 pre_lock
= _create_temporary(f
.name
+ '.lock')
1887 except IOError as e
:
1888 if e
.errno
== errno
.EACCES
:
1889 return # Without write access, just skip dotlocking.
1893 if hasattr(os
, 'link'):
1894 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1896 os
.unlink(pre_lock
.name
)
1898 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
1900 except OSError as e
:
1901 if e
.errno
== errno
.EEXIST
or \
1902 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
1903 os
.remove(pre_lock
.name
)
1904 raise ExternalClashError('dot lock unavailable: %s' %
1910 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1912 os
.remove(f
.name
+ '.lock')
1915 def _unlock_file(f
):
1916 """Unlock file f using lockf and dot locking."""
1918 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1919 if os
.path
.exists(f
.name
+ '.lock'):
1920 os
.remove(f
.name
+ '.lock')
1922 def _create_carefully(path
):
1923 """Create a file if it doesn't exist and open for reading and writing."""
1924 fd
= os
.open(path
, os
.O_CREAT | os
.O_EXCL | os
.O_RDWR
, 0o666)
1926 return open(path
, 'r+', newline
='')
1930 def _create_temporary(path
):
1931 """Create a temp file based on path and open for reading and writing."""
1932 return _create_carefully('%s.%s.%s.%s' % (path
, int(time
.time()),
1933 socket
.gethostname(),
1937 """Ensure changes to file f are physically on disk."""
1939 if hasattr(os
, 'fsync'):
1940 os
.fsync(f
.fileno())
1943 """Close file f, ensuring all changes are physically on disk."""
1948 class Error(Exception):
1949 """Raised for module-specific errors."""
1951 class NoSuchMailboxError(Error
):
1952 """The specified mailbox does not exist and won't be created."""
1954 class NotEmptyError(Error
):
1955 """The specified mailbox is not empty and deletion was requested."""
1957 class ExternalClashError(Error
):
1958 """Another process caused an action to fail."""
1960 class FormatError(Error
):
1961 """A file appears to have an invalid format."""