3 """Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes."""
14 import email
.Generator
18 if sys
.platform
== 'os2emx':
19 # OS/2 EMX fcntl() not adequate
25 __all__
= [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
26 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
27 'BabylMessage', 'MMDFMessage', 'UnixMailbox',
28 'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
31 """A group of messages in a particular place."""
33 def __init__(self
, path
, factory
=None, create
=True):
34 """Initialize a Mailbox instance."""
35 self
._path
= os
.path
.abspath(os
.path
.expanduser(path
))
36 self
._factory
= factory
38 def add(self
, message
):
39 """Add message and return assigned key."""
40 raise NotImplementedError('Method must be implemented by subclass')
42 def remove(self
, key
):
43 """Remove the keyed message; raise KeyError if it doesn't exist."""
44 raise NotImplementedError('Method must be implemented by subclass')
46 def __delitem__(self
, key
):
49 def discard(self
, key
):
50 """If the keyed message exists, remove it."""
56 def __setitem__(self
, key
, message
):
57 """Replace the keyed message; raise KeyError if it doesn't exist."""
58 raise NotImplementedError('Method must be implemented by subclass')
60 def get(self
, key
, default
=None):
61 """Return the keyed message, or default if it doesn't exist."""
63 return self
.__getitem
__(key
)
67 def __getitem__(self
, key
):
68 """Return the keyed message; raise KeyError if it doesn't exist."""
70 return self
.get_message(key
)
72 return self
._factory
(self
.get_file(key
))
74 def get_message(self
, key
):
75 """Return a Message representation or raise a KeyError."""
76 raise NotImplementedError('Method must be implemented by subclass')
78 def get_string(self
, key
):
79 """Return a string representation or raise a KeyError."""
80 raise NotImplementedError('Method must be implemented by subclass')
82 def get_file(self
, key
):
83 """Return a file-like representation or raise a KeyError."""
84 raise NotImplementedError('Method must be implemented by subclass')
87 """Return an iterator over keys."""
88 raise NotImplementedError('Method must be implemented by subclass')
91 """Return a list of keys."""
92 return list(self
.iterkeys())
95 """Return an iterator over all messages."""
96 for key
in self
.iterkeys():
104 return self
.itervalues()
107 """Return a list of messages. Memory intensive."""
108 return list(self
.itervalues())
111 """Return an iterator over (key, message) tuples."""
112 for key
in self
.iterkeys():
120 """Return a list of (key, message) tuples. Memory intensive."""
121 return list(self
.iteritems())
123 def has_key(self
, key
):
124 """Return True if the keyed message exists, False otherwise."""
125 raise NotImplementedError('Method must be implemented by subclass')
127 def __contains__(self
, key
):
128 return self
.has_key(key
)
131 """Return a count of messages in the mailbox."""
132 raise NotImplementedError('Method must be implemented by subclass')
135 """Delete all messages."""
136 for key
in self
.iterkeys():
139 def pop(self
, key
, default
=None):
140 """Delete the keyed message and return it, or default."""
149 """Delete an arbitrary (key, message) pair and return it."""
150 for key
in self
.iterkeys():
151 return (key
, self
.pop(key
)) # This is only run once.
153 raise KeyError('No messages in mailbox')
155 def update(self
, arg
=None):
156 """Change the messages that correspond to certain keys."""
157 if hasattr(arg
, 'iteritems'):
158 source
= arg
.iteritems()
159 elif hasattr(arg
, 'items'):
164 for key
, message
in source
:
170 raise KeyError('No message with key(s)')
173 """Write any pending changes to the disk."""
174 raise NotImplementedError('Method must be implemented by subclass')
177 """Lock the mailbox."""
178 raise NotImplementedError('Method must be implemented by subclass')
181 """Unlock the mailbox if it is locked."""
182 raise NotImplementedError('Method must be implemented by subclass')
185 """Flush and close the mailbox."""
186 raise NotImplementedError('Method must be implemented by subclass')
188 def _dump_message(self
, message
, target
, mangle_from_
=False):
189 # Most files are opened in binary mode to allow predictable seeking.
190 # To get native line endings on disk, the user-friendly \n line endings
191 # used in strings and by email.Message are translated here.
192 """Dump message contents to target file."""
193 if isinstance(message
, email
.Message
.Message
):
194 buffer = StringIO
.StringIO()
195 gen
= email
.Generator
.Generator(buffer, mangle_from_
, 0)
198 target
.write(buffer.read().replace('\n', os
.linesep
))
199 elif isinstance(message
, str):
201 message
= message
.replace('\nFrom ', '\n>From ')
202 message
= message
.replace('\n', os
.linesep
)
203 target
.write(message
)
204 elif hasattr(message
, 'read'):
206 line
= message
.readline()
209 if mangle_from_
and line
.startswith('From '):
210 line
= '>From ' + line
[5:]
211 line
= line
.replace('\n', os
.linesep
)
214 raise TypeError('Invalid message type: %s' % type(message
))
217 class Maildir(Mailbox
):
218 """A qmail-style Maildir mailbox."""
222 def __init__(self
, dirname
, factory
=rfc822
.Message
, create
=True):
223 """Initialize a Maildir instance."""
224 Mailbox
.__init
__(self
, dirname
, factory
, create
)
225 if not os
.path
.exists(self
._path
):
227 os
.mkdir(self
._path
, 0700)
228 os
.mkdir(os
.path
.join(self
._path
, 'tmp'), 0700)
229 os
.mkdir(os
.path
.join(self
._path
, 'new'), 0700)
230 os
.mkdir(os
.path
.join(self
._path
, 'cur'), 0700)
232 raise NoSuchMailboxError(self
._path
)
235 def add(self
, message
):
236 """Add message and return assigned key."""
237 tmp_file
= self
._create
_tmp
()
239 self
._dump
_message
(message
, tmp_file
)
242 if isinstance(message
, MaildirMessage
):
243 subdir
= message
.get_subdir()
244 suffix
= self
.colon
+ message
.get_info()
245 if suffix
== self
.colon
:
250 uniq
= os
.path
.basename(tmp_file
.name
).split(self
.colon
)[0]
251 dest
= os
.path
.join(self
._path
, subdir
, uniq
+ suffix
)
252 os
.rename(tmp_file
.name
, dest
)
253 if isinstance(message
, MaildirMessage
):
254 os
.utime(dest
, (os
.path
.getatime(dest
), message
.get_date()))
257 def remove(self
, key
):
258 """Remove the keyed message; raise KeyError if it doesn't exist."""
259 os
.remove(os
.path
.join(self
._path
, self
._lookup
(key
)))
261 def discard(self
, key
):
262 """If the keyed message exists, remove it."""
263 # This overrides an inapplicable implementation in the superclass.
269 if e
.errno
!= errno
.ENOENT
:
272 def __setitem__(self
, key
, message
):
273 """Replace the keyed message; raise KeyError if it doesn't exist."""
274 old_subpath
= self
._lookup
(key
)
275 temp_key
= self
.add(message
)
276 temp_subpath
= self
._lookup
(temp_key
)
277 if isinstance(message
, MaildirMessage
):
278 # temp's subdir and suffix were specified by message.
279 dominant_subpath
= temp_subpath
281 # temp's subdir and suffix were defaults from add().
282 dominant_subpath
= old_subpath
283 subdir
= os
.path
.dirname(dominant_subpath
)
284 if self
.colon
in dominant_subpath
:
285 suffix
= self
.colon
+ dominant_subpath
.split(self
.colon
)[-1]
289 new_path
= os
.path
.join(self
._path
, subdir
, key
+ suffix
)
290 os
.rename(os
.path
.join(self
._path
, temp_subpath
), new_path
)
291 if isinstance(message
, MaildirMessage
):
292 os
.utime(new_path
, (os
.path
.getatime(new_path
),
295 def get_message(self
, key
):
296 """Return a Message representation or raise a KeyError."""
297 subpath
= self
._lookup
(key
)
298 f
= open(os
.path
.join(self
._path
, subpath
), 'r')
300 msg
= MaildirMessage(f
)
303 subdir
, name
= os
.path
.split(subpath
)
304 msg
.set_subdir(subdir
)
305 if self
.colon
in name
:
306 msg
.set_info(name
.split(self
.colon
)[-1])
307 msg
.set_date(os
.path
.getmtime(os
.path
.join(self
._path
, subpath
)))
310 def get_string(self
, key
):
311 """Return a string representation or raise a KeyError."""
312 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r')
318 def get_file(self
, key
):
319 """Return a file-like representation or raise a KeyError."""
320 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'rb')
324 """Return an iterator over keys."""
326 for key
in self
._toc
:
333 def has_key(self
, key
):
334 """Return True if the keyed message exists, False otherwise."""
336 return key
in self
._toc
339 """Return a count of messages in the mailbox."""
341 return len(self
._toc
)
344 """Write any pending changes to disk."""
345 return # Maildir changes are always written immediately.
348 """Lock the mailbox."""
352 """Unlock the mailbox if it is locked."""
356 """Flush and close the mailbox."""
359 def list_folders(self
):
360 """Return a list of folder names."""
362 for entry
in os
.listdir(self
._path
):
363 if len(entry
) > 1 and entry
[0] == '.' and \
364 os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
365 result
.append(entry
[1:])
368 def get_folder(self
, folder
):
369 """Return a Maildir instance for the named folder."""
370 return Maildir(os
.path
.join(self
._path
, '.' + folder
), create
=False)
372 def add_folder(self
, folder
):
373 """Create a folder and return a Maildir instance representing it."""
374 path
= os
.path
.join(self
._path
, '.' + folder
)
375 result
= Maildir(path
)
376 maildirfolder_path
= os
.path
.join(path
, 'maildirfolder')
377 if not os
.path
.exists(maildirfolder_path
):
378 os
.close(os
.open(maildirfolder_path
, os
.O_CREAT | os
.O_WRONLY
))
381 def remove_folder(self
, folder
):
382 """Delete the named folder, which must be empty."""
383 path
= os
.path
.join(self
._path
, '.' + folder
)
384 for entry
in os
.listdir(os
.path
.join(path
, 'new')) + \
385 os
.listdir(os
.path
.join(path
, 'cur')):
386 if len(entry
) < 1 or entry
[0] != '.':
387 raise NotEmptyError('Folder contains message(s): %s' % folder
)
388 for entry
in os
.listdir(path
):
389 if entry
!= 'new' and entry
!= 'cur' and entry
!= 'tmp' and \
390 os
.path
.isdir(os
.path
.join(path
, entry
)):
391 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
393 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
395 os
.remove(os
.path
.join(root
, entry
))
397 os
.rmdir(os
.path
.join(root
, entry
))
401 """Delete old files in "tmp"."""
403 for entry
in os
.listdir(os
.path
.join(self
._path
, 'tmp')):
404 path
= os
.path
.join(self
._path
, 'tmp', entry
)
405 if now
- os
.path
.getatime(path
) > 129600: # 60 * 60 * 36
408 _count
= 1 # This is used to generate unique file names.
410 def _create_tmp(self
):
411 """Create a file in the tmp subdirectory and open and return it."""
413 hostname
= socket
.gethostname()
415 hostname
= hostname
.replace('/', r
'\057')
417 hostname
= hostname
.replace(':', r
'\072')
418 uniq
= "%s.M%sP%sQ%s.%s" % (int(now
), int(now
% 1 * 1e6
), os
.getpid(),
419 Maildir
._count
, hostname
)
420 path
= os
.path
.join(self
._path
, 'tmp', uniq
)
424 if e
.errno
== errno
.ENOENT
:
426 return open(path
, 'wb+')
430 raise ExternalClashError('Name clash prevented file creation: %s' %
434 """Update table of contents mapping."""
436 for subdir
in ('new', 'cur'):
437 for entry
in os
.listdir(os
.path
.join(self
._path
, subdir
)):
438 uniq
= entry
.split(self
.colon
)[0]
439 self
._toc
[uniq
] = os
.path
.join(subdir
, entry
)
441 def _lookup(self
, key
):
442 """Use TOC to return subpath for given key, or raise a KeyError."""
444 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
445 return self
._toc
[key
]
450 return self
._toc
[key
]
452 raise KeyError('No message with key: %s' % key
)
454 # This method is for backward compatibility only.
456 """Return the next message in a one-time iteration."""
457 if not hasattr(self
, '_onetime_keys'):
458 self
._onetime
_keys
= self
.iterkeys()
461 return self
[self
._onetime
_keys
.next()]
462 except StopIteration:
468 class _singlefileMailbox(Mailbox
):
469 """A single-file mailbox."""
471 def __init__(self
, path
, factory
=None, create
=True):
472 """Initialize a single-file mailbox."""
473 Mailbox
.__init
__(self
, path
, factory
, create
)
475 f
= open(self
._path
, 'rb+')
477 if e
.errno
== errno
.ENOENT
:
479 f
= open(self
._path
, 'wb+')
481 raise NoSuchMailboxError(self
._path
)
482 elif e
.errno
== errno
.EACCES
:
483 f
= open(self
._path
, 'rb')
489 self
._pending
= False # No changes require rewriting the file.
492 def add(self
, message
):
493 """Add message and return assigned key."""
495 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
498 return self
._next
_key
- 1
500 def remove(self
, key
):
501 """Remove the keyed message; raise KeyError if it doesn't exist."""
506 def __setitem__(self
, key
, message
):
507 """Replace the keyed message; raise KeyError if it doesn't exist."""
509 self
._toc
[key
] = self
._append
_message
(message
)
513 """Return an iterator over keys."""
515 for key
in self
._toc
.keys():
518 def has_key(self
, key
):
519 """Return True if the keyed message exists, False otherwise."""
521 return key
in self
._toc
524 """Return a count of messages in the mailbox."""
526 return len(self
._toc
)
529 """Lock the mailbox."""
531 _lock_file(self
._file
)
535 """Unlock the mailbox if it is locked."""
537 _unlock_file(self
._file
)
541 """Write any pending changes to disk."""
542 if not self
._pending
:
545 new_file
= _create_temporary(self
._path
)
548 self
._pre
_mailbox
_hook
(new_file
)
549 for key
in sorted(self
._toc
.keys()):
550 start
, stop
= self
._toc
[key
]
551 self
._file
.seek(start
)
552 self
._pre
_message
_hook
(new_file
)
553 new_start
= new_file
.tell()
555 buffer = self
._file
.read(min(4096,
556 stop
- self
._file
.tell()))
559 new_file
.write(buffer)
560 new_toc
[key
] = (new_start
, new_file
.tell())
561 self
._post
_message
_hook
(new_file
)
564 os
.remove(new_file
.name
)
569 os
.rename(new_file
.name
, self
._path
)
571 if e
.errno
== errno
.EEXIST
or \
572 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
573 os
.remove(self
._path
)
574 os
.rename(new_file
.name
, self
._path
)
577 self
._file
= open(self
._path
, 'rb+')
579 self
._pending
= False
581 _lock_file(new_file
, dotlock
=False)
583 def _pre_mailbox_hook(self
, f
):
584 """Called before writing the mailbox to file f."""
587 def _pre_message_hook(self
, f
):
588 """Called before writing each message to file f."""
591 def _post_message_hook(self
, f
):
592 """Called after writing each message to file f."""
596 """Flush and close the mailbox."""
602 def _lookup(self
, key
=None):
603 """Return (start, stop) or raise KeyError."""
604 if self
._toc
is None:
608 return self
._toc
[key
]
610 raise KeyError('No message with key: %s' % key
)
612 def _append_message(self
, message
):
613 """Append message to mailbox and return (start, stop) offsets."""
614 self
._file
.seek(0, 2)
615 self
._pre
_message
_hook
(self
._file
)
616 offsets
= self
._install
_message
(message
)
617 self
._post
_message
_hook
(self
._file
)
623 class _mboxMMDF(_singlefileMailbox
):
624 """An mbox or MMDF mailbox."""
628 def get_message(self
, key
):
629 """Return a Message representation or raise a KeyError."""
630 start
, stop
= self
._lookup
(key
)
631 self
._file
.seek(start
)
632 from_line
= self
._file
.readline().replace(os
.linesep
, '')
633 string
= self
._file
.read(stop
- self
._file
.tell())
634 msg
= self
._message
_factory
(string
.replace(os
.linesep
, '\n'))
635 msg
.set_from(from_line
[5:])
638 def get_string(self
, key
, from_
=False):
639 """Return a string representation or raise a KeyError."""
640 start
, stop
= self
._lookup
(key
)
641 self
._file
.seek(start
)
643 self
._file
.readline()
644 string
= self
._file
.read(stop
- self
._file
.tell())
645 return string
.replace(os
.linesep
, '\n')
647 def get_file(self
, key
, from_
=False):
648 """Return a file-like representation or raise a KeyError."""
649 start
, stop
= self
._lookup
(key
)
650 self
._file
.seek(start
)
652 self
._file
.readline()
653 return _PartialFile(self
._file
, self
._file
.tell(), stop
)
655 def _install_message(self
, message
):
656 """Format a message and blindly write to self._file."""
658 if isinstance(message
, str) and message
.startswith('From '):
659 newline
= message
.find('\n')
661 from_line
= message
[:newline
]
662 message
= message
[newline
+ 1:]
666 elif isinstance(message
, _mboxMMDFMessage
):
667 from_line
= 'From ' + message
.get_from()
668 elif isinstance(message
, email
.Message
.Message
):
669 from_line
= message
.get_unixfrom() # May be None.
670 if from_line
is None:
671 from_line
= 'From MAILER-DAEMON %s' % time
.asctime(time
.gmtime())
672 start
= self
._file
.tell()
673 self
._file
.write(from_line
+ os
.linesep
)
674 self
._dump
_message
(message
, self
._file
, self
._mangle
_from
_)
675 stop
= self
._file
.tell()
679 class mbox(_mboxMMDF
):
680 """A classic mbox mailbox."""
684 def __init__(self
, path
, factory
=None, create
=True):
685 """Initialize an mbox mailbox."""
686 self
._message
_factory
= mboxMessage
687 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
689 def _pre_message_hook(self
, f
):
690 """Called before writing each message to file f."""
694 def _generate_toc(self
):
695 """Generate key-to-(start, stop) table of contents."""
696 starts
, stops
= [], []
699 line_pos
= self
._file
.tell()
700 line
= self
._file
.readline()
701 if line
.startswith('From '):
702 if len(stops
) < len(starts
):
703 stops
.append(line_pos
- len(os
.linesep
))
704 starts
.append(line_pos
)
706 stops
.append(line_pos
)
708 self
._toc
= dict(enumerate(zip(starts
, stops
)))
709 self
._next
_key
= len(self
._toc
)
712 class MMDF(_mboxMMDF
):
713 """An MMDF mailbox."""
715 def __init__(self
, path
, factory
=None, create
=True):
716 """Initialize an MMDF mailbox."""
717 self
._message
_factory
= MMDFMessage
718 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
720 def _pre_message_hook(self
, f
):
721 """Called before writing each message to file f."""
722 f
.write('\001\001\001\001' + os
.linesep
)
724 def _post_message_hook(self
, f
):
725 """Called after writing each message to file f."""
726 f
.write(os
.linesep
+ '\001\001\001\001' + os
.linesep
)
728 def _generate_toc(self
):
729 """Generate key-to-(start, stop) table of contents."""
730 starts
, stops
= [], []
735 line
= self
._file
.readline()
736 next_pos
= self
._file
.tell()
737 if line
.startswith('\001\001\001\001' + os
.linesep
):
738 starts
.append(next_pos
)
741 line
= self
._file
.readline()
742 next_pos
= self
._file
.tell()
743 if line
== '\001\001\001\001' + os
.linesep
:
744 stops
.append(line_pos
- len(os
.linesep
))
747 stops
.append(line_pos
)
751 self
._toc
= dict(enumerate(zip(starts
, stops
)))
752 self
._next
_key
= len(self
._toc
)
758 def __init__(self
, path
, factory
=None, create
=True):
759 """Initialize an MH instance."""
760 Mailbox
.__init
__(self
, path
, factory
, create
)
761 if not os
.path
.exists(self
._path
):
763 os
.mkdir(self
._path
, 0700)
764 os
.close(os
.open(os
.path
.join(self
._path
, '.mh_sequences'),
765 os
.O_CREAT | os
.O_EXCL | os
.O_WRONLY
, 0600))
767 raise NoSuchMailboxError(self
._path
)
770 def add(self
, message
):
771 """Add message and return assigned key."""
776 new_key
= max(keys
) + 1
777 new_path
= os
.path
.join(self
._path
, str(new_key
))
778 f
= _create_carefully(new_path
)
783 self
._dump
_message
(message
, f
)
784 if isinstance(message
, MHMessage
):
785 self
._dump
_sequences
(message
, new_key
)
793 def remove(self
, key
):
794 """Remove the keyed message; raise KeyError if it doesn't exist."""
795 path
= os
.path
.join(self
._path
, str(key
))
797 f
= open(path
, 'rb+')
799 if e
.errno
== errno
.ENOENT
:
800 raise KeyError('No message with key: %s' % key
)
808 os
.remove(os
.path
.join(self
._path
, str(key
)))
815 def __setitem__(self
, key
, message
):
816 """Replace the keyed message; raise KeyError if it doesn't exist."""
817 path
= os
.path
.join(self
._path
, str(key
))
819 f
= open(path
, 'rb+')
821 if e
.errno
== errno
.ENOENT
:
822 raise KeyError('No message with key: %s' % key
)
829 os
.close(os
.open(path
, os
.O_WRONLY | os
.O_TRUNC
))
830 self
._dump
_message
(message
, f
)
831 if isinstance(message
, MHMessage
):
832 self
._dump
_sequences
(message
, key
)
839 def get_message(self
, key
):
840 """Return a Message representation or raise a KeyError."""
843 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
845 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
847 if e
.errno
== errno
.ENOENT
:
848 raise KeyError('No message with key: %s' % key
)
861 for name
, key_list
in self
.get_sequences():
863 msg
.add_sequence(name
)
866 def get_string(self
, key
):
867 """Return a string representation or raise a KeyError."""
870 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
872 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
874 if e
.errno
== errno
.ENOENT
:
875 raise KeyError('No message with key: %s' % key
)
889 def get_file(self
, key
):
890 """Return a file-like representation or raise a KeyError."""
892 f
= open(os
.path
.join(self
._path
, str(key
)), 'rb')
894 if e
.errno
== errno
.ENOENT
:
895 raise KeyError('No message with key: %s' % key
)
901 """Return an iterator over keys."""
902 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
905 def has_key(self
, key
):
906 """Return True if the keyed message exists, False otherwise."""
907 return os
.path
.exists(os
.path
.join(self
._path
, str(key
)))
910 """Return a count of messages in the mailbox."""
911 return len(list(self
.iterkeys()))
914 """Lock the mailbox."""
916 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
917 _lock_file(self
._file
)
921 """Unlock the mailbox if it is locked."""
923 _unlock_file(self
._file
)
929 """Write any pending changes to the disk."""
933 """Flush and close the mailbox."""
937 def list_folders(self
):
938 """Return a list of folder names."""
940 for entry
in os
.listdir(self
._path
):
941 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
945 def get_folder(self
, folder
):
946 """Return an MH instance for the named folder."""
947 return MH(os
.path
.join(self
._path
, folder
), create
=False)
949 def add_folder(self
, folder
):
950 """Create a folder and return an MH instance representing it."""
951 return MH(os
.path
.join(self
._path
, folder
))
953 def remove_folder(self
, folder
):
954 """Delete the named folder, which must be empty."""
955 path
= os
.path
.join(self
._path
, folder
)
956 entries
= os
.listdir(path
)
957 if entries
== ['.mh_sequences']:
958 os
.remove(os
.path
.join(path
, '.mh_sequences'))
962 raise NotEmptyError('Folder not empty: %s' % self
._path
)
965 def get_sequences(self
):
966 """Return a name-to-key-list dictionary to define each sequence."""
968 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r')
970 all_keys
= set(self
.keys())
973 name
, contents
= line
.split(':')
975 for spec
in contents
.split():
979 start
, stop
= (int(x
) for x
in spec
.split('-'))
980 keys
.update(range(start
, stop
+ 1))
981 results
[name
] = [key
for key
in sorted(keys
) \
983 if len(results
[name
]) == 0:
986 raise FormatError('Invalid sequence specification: %s' %
992 def set_sequences(self
, sequences
):
993 """Set sequences using the given name-to-key-list dictionary."""
994 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r+')
996 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
997 for name
, keys
in sequences
.iteritems():
1000 f
.write('%s:' % name
)
1003 for key
in sorted(set(keys
)):
1010 f
.write('%s %s' % (prev
, key
))
1012 f
.write(' %s' % key
)
1015 f
.write(str(prev
) + '\n')
1022 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1023 sequences
= self
.get_sequences()
1026 for key
in self
.iterkeys():
1028 changes
.append((key
, prev
+ 1))
1029 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
1034 if hasattr(os
, 'link'):
1035 os
.link(os
.path
.join(self
._path
, str(key
)),
1036 os
.path
.join(self
._path
, str(prev
+ 1)))
1037 if sys
.platform
== 'os2emx':
1038 # cannot unlink an open file on OS/2
1040 os
.unlink(os
.path
.join(self
._path
, str(key
)))
1043 os
.rename(os
.path
.join(self
._path
, str(key
)),
1044 os
.path
.join(self
._path
, str(prev
+ 1)))
1051 self
._next
_key
= prev
+ 1
1052 if len(changes
) == 0:
1054 for name
, key_list
in sequences
.items():
1055 for old
, new
in changes
:
1057 key_list
[key_list
.index(old
)] = new
1058 self
.set_sequences(sequences
)
1060 def _dump_sequences(self
, message
, key
):
1061 """Inspect a new MHMessage and update sequences appropriately."""
1062 pending_sequences
= message
.get_sequences()
1063 all_sequences
= self
.get_sequences()
1064 for name
, key_list
in all_sequences
.iteritems():
1065 if name
in pending_sequences
:
1066 key_list
.append(key
)
1067 elif key
in key_list
:
1068 del key_list
[key_list
.index(key
)]
1069 for sequence
in pending_sequences
:
1070 if sequence
not in all_sequences
:
1071 all_sequences
[sequence
] = [key
]
1072 self
.set_sequences(all_sequences
)
1075 class Babyl(_singlefileMailbox
):
1076 """An Rmail-style Babyl mailbox."""
1078 _special_labels
= frozenset(('unseen', 'deleted', 'filed', 'answered',
1079 'forwarded', 'edited', 'resent'))
1081 def __init__(self
, path
, factory
=None, create
=True):
1082 """Initialize a Babyl mailbox."""
1083 _singlefileMailbox
.__init
__(self
, path
, factory
, create
)
1086 def add(self
, message
):
1087 """Add message and return assigned key."""
1088 key
= _singlefileMailbox
.add(self
, message
)
1089 if isinstance(message
, BabylMessage
):
1090 self
._labels
[key
] = message
.get_labels()
1093 def remove(self
, key
):
1094 """Remove the keyed message; raise KeyError if it doesn't exist."""
1095 _singlefileMailbox
.remove(self
, key
)
1096 if key
in self
._labels
:
1097 del self
._labels
[key
]
1099 def __setitem__(self
, key
, message
):
1100 """Replace the keyed message; raise KeyError if it doesn't exist."""
1101 _singlefileMailbox
.__setitem
__(self
, key
, message
)
1102 if isinstance(message
, BabylMessage
):
1103 self
._labels
[key
] = message
.get_labels()
1105 def get_message(self
, key
):
1106 """Return a Message representation or raise a KeyError."""
1107 start
, stop
= self
._lookup
(key
)
1108 self
._file
.seek(start
)
1109 self
._file
.readline() # Skip '1,' line specifying labels.
1110 original_headers
= StringIO
.StringIO()
1112 line
= self
._file
.readline()
1113 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1115 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1116 visible_headers
= StringIO
.StringIO()
1118 line
= self
._file
.readline()
1119 if line
== os
.linesep
or line
== '':
1121 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1122 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1124 msg
= BabylMessage(original_headers
.getvalue() + body
)
1125 msg
.set_visible(visible_headers
.getvalue())
1126 if key
in self
._labels
:
1127 msg
.set_labels(self
._labels
[key
])
1130 def get_string(self
, key
):
1131 """Return a string representation or raise a KeyError."""
1132 start
, stop
= self
._lookup
(key
)
1133 self
._file
.seek(start
)
1134 self
._file
.readline() # Skip '1,' line specifying labels.
1135 original_headers
= StringIO
.StringIO()
1137 line
= self
._file
.readline()
1138 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1140 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1142 line
= self
._file
.readline()
1143 if line
== os
.linesep
or line
== '':
1145 return original_headers
.getvalue() + \
1146 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1149 def get_file(self
, key
):
1150 """Return a file-like representation or raise a KeyError."""
1151 return StringIO
.StringIO(self
.get_string(key
).replace('\n',
1154 def get_labels(self
):
1155 """Return a list of user-defined labels in the mailbox."""
1158 for label_list
in self
._labels
.values():
1159 labels
.update(label_list
)
1160 labels
.difference_update(self
._special
_labels
)
1163 def _generate_toc(self
):
1164 """Generate key-to-(start, stop) table of contents."""
1165 starts
, stops
= [], []
1171 line
= self
._file
.readline()
1172 next_pos
= self
._file
.tell()
1173 if line
== '\037\014' + os
.linesep
:
1174 if len(stops
) < len(starts
):
1175 stops
.append(line_pos
- len(os
.linesep
))
1176 starts
.append(next_pos
)
1177 labels
= [label
.strip() for label
1178 in self
._file
.readline()[1:].split(',')
1179 if label
.strip() != '']
1180 label_lists
.append(labels
)
1181 elif line
== '\037' or line
== '\037' + os
.linesep
:
1182 if len(stops
) < len(starts
):
1183 stops
.append(line_pos
- len(os
.linesep
))
1185 stops
.append(line_pos
- len(os
.linesep
))
1187 self
._toc
= dict(enumerate(zip(starts
, stops
)))
1188 self
._labels
= dict(enumerate(label_lists
))
1189 self
._next
_key
= len(self
._toc
)
1191 def _pre_mailbox_hook(self
, f
):
1192 """Called before writing the mailbox to file f."""
1193 f
.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1194 (os
.linesep
, os
.linesep
, ','.join(self
.get_labels()),
1197 def _pre_message_hook(self
, f
):
1198 """Called before writing each message to file f."""
1199 f
.write('\014' + os
.linesep
)
1201 def _post_message_hook(self
, f
):
1202 """Called after writing each message to file f."""
1203 f
.write(os
.linesep
+ '\037')
1205 def _install_message(self
, message
):
1206 """Write message contents and return (start, stop)."""
1207 start
= self
._file
.tell()
1208 if isinstance(message
, BabylMessage
):
1211 for label
in message
.get_labels():
1212 if label
in self
._special
_labels
:
1213 special_labels
.append(label
)
1215 labels
.append(label
)
1216 self
._file
.write('1')
1217 for label
in special_labels
:
1218 self
._file
.write(', ' + label
)
1219 self
._file
.write(',,')
1220 for label
in labels
:
1221 self
._file
.write(' ' + label
+ ',')
1222 self
._file
.write(os
.linesep
)
1224 self
._file
.write('1,,' + os
.linesep
)
1225 if isinstance(message
, email
.Message
.Message
):
1226 orig_buffer
= StringIO
.StringIO()
1227 orig_generator
= email
.Generator
.Generator(orig_buffer
, False, 0)
1228 orig_generator
.flatten(message
)
1231 line
= orig_buffer
.readline()
1232 self
._file
.write(line
.replace('\n', os
.linesep
))
1233 if line
== '\n' or line
== '':
1235 self
._file
.write('*** EOOH ***' + os
.linesep
)
1236 if isinstance(message
, BabylMessage
):
1237 vis_buffer
= StringIO
.StringIO()
1238 vis_generator
= email
.Generator
.Generator(vis_buffer
, False, 0)
1239 vis_generator
.flatten(message
.get_visible())
1241 line
= vis_buffer
.readline()
1242 self
._file
.write(line
.replace('\n', os
.linesep
))
1243 if line
== '\n' or line
== '':
1248 line
= orig_buffer
.readline()
1249 self
._file
.write(line
.replace('\n', os
.linesep
))
1250 if line
== '\n' or line
== '':
1253 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
1256 self
._file
.write(buffer.replace('\n', os
.linesep
))
1257 elif isinstance(message
, str):
1258 body_start
= message
.find('\n\n') + 2
1259 if body_start
- 2 != -1:
1260 self
._file
.write(message
[:body_start
].replace('\n',
1262 self
._file
.write('*** EOOH ***' + os
.linesep
)
1263 self
._file
.write(message
[:body_start
].replace('\n',
1265 self
._file
.write(message
[body_start
:].replace('\n',
1268 self
._file
.write('*** EOOH ***' + os
.linesep
+ os
.linesep
)
1269 self
._file
.write(message
.replace('\n', os
.linesep
))
1270 elif hasattr(message
, 'readline'):
1271 original_pos
= message
.tell()
1274 line
= message
.readline()
1275 self
._file
.write(line
.replace('\n', os
.linesep
))
1276 if line
== '\n' or line
== '':
1277 self
._file
.write('*** EOOH ***' + os
.linesep
)
1280 message
.seek(original_pos
)
1284 buffer = message
.read(4096) # Buffer size is arbitrary.
1287 self
._file
.write(buffer.replace('\n', os
.linesep
))
1289 raise TypeError('Invalid message type: %s' % type(message
))
1290 stop
= self
._file
.tell()
1291 return (start
, stop
)
1294 class Message(email
.Message
.Message
):
1295 """Message with mailbox-format-specific properties."""
1297 def __init__(self
, message
=None):
1298 """Initialize a Message instance."""
1299 if isinstance(message
, email
.Message
.Message
):
1300 self
._become
_message
(copy
.deepcopy(message
))
1301 if isinstance(message
, Message
):
1302 message
._explain
_to
(self
)
1303 elif isinstance(message
, str):
1304 self
._become
_message
(email
.message_from_string(message
))
1305 elif hasattr(message
, "read"):
1306 self
._become
_message
(email
.message_from_file(message
))
1307 elif message
is None:
1308 email
.Message
.Message
.__init
__(self
)
1310 raise TypeError('Invalid message type: %s' % type(message
))
1312 def _become_message(self
, message
):
1313 """Assume the non-format-specific state of message."""
1314 for name
in ('_headers', '_unixfrom', '_payload', '_charset',
1315 'preamble', 'epilogue', 'defects', '_default_type'):
1316 self
.__dict
__[name
] = message
.__dict
__[name
]
1318 def _explain_to(self
, message
):
1319 """Copy format-specific state to message insofar as possible."""
1320 if isinstance(message
, Message
):
1321 return # There's nothing format-specific to explain.
1323 raise TypeError('Cannot convert to specified type')
1326 class MaildirMessage(Message
):
1327 """Message with Maildir-specific properties."""
1329 def __init__(self
, message
=None):
1330 """Initialize a MaildirMessage instance."""
1331 self
._subdir
= 'new'
1333 self
._date
= time
.time()
1334 Message
.__init
__(self
, message
)
1336 def get_subdir(self
):
1337 """Return 'new' or 'cur'."""
1340 def set_subdir(self
, subdir
):
1341 """Set subdir to 'new' or 'cur'."""
1342 if subdir
== 'new' or subdir
== 'cur':
1343 self
._subdir
= subdir
1345 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir
)
1347 def get_flags(self
):
1348 """Return as a string the flags that are set."""
1349 if self
._info
.startswith('2,'):
1350 return self
._info
[2:]
1354 def set_flags(self
, flags
):
1355 """Set the given flags and unset all others."""
1356 self
._info
= '2,' + ''.join(sorted(flags
))
1358 def add_flag(self
, flag
):
1359 """Set the given flag(s) without changing others."""
1360 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1362 def remove_flag(self
, flag
):
1363 """Unset the given string flag(s) without changing others."""
1364 if self
.get_flags() != '':
1365 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1368 """Return delivery date of message, in seconds since the epoch."""
1371 def set_date(self
, date
):
1372 """Set delivery date of message, in seconds since the epoch."""
1374 self
._date
= float(date
)
1376 raise TypeError("can't convert to float: %s" % date
)
1379 """Get the message's "info" as a string."""
1382 def set_info(self
, info
):
1383 """Set the message's "info" string."""
1384 if isinstance(info
, str):
1387 raise TypeError('info must be a string: %s' % type(info
))
1389 def _explain_to(self
, message
):
1390 """Copy Maildir-specific state to message insofar as possible."""
1391 if isinstance(message
, MaildirMessage
):
1392 message
.set_flags(self
.get_flags())
1393 message
.set_subdir(self
.get_subdir())
1394 message
.set_date(self
.get_date())
1395 elif isinstance(message
, _mboxMMDFMessage
):
1396 flags
= set(self
.get_flags())
1398 message
.add_flag('R')
1399 if self
.get_subdir() == 'cur':
1400 message
.add_flag('O')
1402 message
.add_flag('D')
1404 message
.add_flag('F')
1406 message
.add_flag('A')
1407 message
.set_from('MAILER-DAEMON', time
.gmtime(self
.get_date()))
1408 elif isinstance(message
, MHMessage
):
1409 flags
= set(self
.get_flags())
1410 if 'S' not in flags
:
1411 message
.add_sequence('unseen')
1413 message
.add_sequence('replied')
1415 message
.add_sequence('flagged')
1416 elif isinstance(message
, BabylMessage
):
1417 flags
= set(self
.get_flags())
1418 if 'S' not in flags
:
1419 message
.add_label('unseen')
1421 message
.add_label('deleted')
1423 message
.add_label('answered')
1425 message
.add_label('forwarded')
1426 elif isinstance(message
, Message
):
1429 raise TypeError('Cannot convert to specified type: %s' %
1433 class _mboxMMDFMessage(Message
):
1434 """Message with mbox- or MMDF-specific properties."""
1436 def __init__(self
, message
=None):
1437 """Initialize an mboxMMDFMessage instance."""
1438 self
.set_from('MAILER-DAEMON', True)
1439 if isinstance(message
, email
.Message
.Message
):
1440 unixfrom
= message
.get_unixfrom()
1441 if unixfrom
is not None and unixfrom
.startswith('From '):
1442 self
.set_from(unixfrom
[5:])
1443 Message
.__init
__(self
, message
)
1446 """Return contents of "From " line."""
1449 def set_from(self
, from_
, time_
=None):
1450 """Set "From " line, formatting and appending time_ if specified."""
1451 if time_
is not None:
1453 time_
= time
.gmtime()
1454 from_
+= ' ' + time
.asctime(time_
)
1457 def get_flags(self
):
1458 """Return as a string the flags that are set."""
1459 return self
.get('Status', '') + self
.get('X-Status', '')
1461 def set_flags(self
, flags
):
1462 """Set the given flags and unset all others."""
1464 status_flags
, xstatus_flags
= '', ''
1465 for flag
in ('R', 'O'):
1467 status_flags
+= flag
1469 for flag
in ('D', 'F', 'A'):
1471 xstatus_flags
+= flag
1473 xstatus_flags
+= ''.join(sorted(flags
))
1475 self
.replace_header('Status', status_flags
)
1477 self
.add_header('Status', status_flags
)
1479 self
.replace_header('X-Status', xstatus_flags
)
1481 self
.add_header('X-Status', xstatus_flags
)
1483 def add_flag(self
, flag
):
1484 """Set the given flag(s) without changing others."""
1485 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1487 def remove_flag(self
, flag
):
1488 """Unset the given string flag(s) without changing others."""
1489 if 'Status' in self
or 'X-Status' in self
:
1490 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1492 def _explain_to(self
, message
):
1493 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1494 if isinstance(message
, MaildirMessage
):
1495 flags
= set(self
.get_flags())
1497 message
.set_subdir('cur')
1499 message
.add_flag('F')
1501 message
.add_flag('R')
1503 message
.add_flag('S')
1505 message
.add_flag('T')
1506 del message
['status']
1507 del message
['x-status']
1508 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1510 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1511 '%a %b %d %H:%M:%S %Y')))
1512 except (ValueError, OverflowError):
1514 elif isinstance(message
, _mboxMMDFMessage
):
1515 message
.set_flags(self
.get_flags())
1516 message
.set_from(self
.get_from())
1517 elif isinstance(message
, MHMessage
):
1518 flags
= set(self
.get_flags())
1519 if 'R' not in flags
:
1520 message
.add_sequence('unseen')
1522 message
.add_sequence('replied')
1524 message
.add_sequence('flagged')
1525 del message
['status']
1526 del message
['x-status']
1527 elif isinstance(message
, BabylMessage
):
1528 flags
= set(self
.get_flags())
1529 if 'R' not in flags
:
1530 message
.add_label('unseen')
1532 message
.add_label('deleted')
1534 message
.add_label('answered')
1535 del message
['status']
1536 del message
['x-status']
1537 elif isinstance(message
, Message
):
1540 raise TypeError('Cannot convert to specified type: %s' %
1544 class mboxMessage(_mboxMMDFMessage
):
1545 """Message with mbox-specific properties."""
1548 class MHMessage(Message
):
1549 """Message with MH-specific properties."""
1551 def __init__(self
, message
=None):
1552 """Initialize an MHMessage instance."""
1553 self
._sequences
= []
1554 Message
.__init
__(self
, message
)
1556 def get_sequences(self
):
1557 """Return a list of sequences that include the message."""
1558 return self
._sequences
[:]
1560 def set_sequences(self
, sequences
):
1561 """Set the list of sequences that include the message."""
1562 self
._sequences
= list(sequences
)
1564 def add_sequence(self
, sequence
):
1565 """Add sequence to list of sequences including the message."""
1566 if isinstance(sequence
, str):
1567 if not sequence
in self
._sequences
:
1568 self
._sequences
.append(sequence
)
1570 raise TypeError('sequence must be a string: %s' % type(sequence
))
1572 def remove_sequence(self
, sequence
):
1573 """Remove sequence from the list of sequences including the message."""
1575 self
._sequences
.remove(sequence
)
1579 def _explain_to(self
, message
):
1580 """Copy MH-specific state to message insofar as possible."""
1581 if isinstance(message
, MaildirMessage
):
1582 sequences
= set(self
.get_sequences())
1583 if 'unseen' in sequences
:
1584 message
.set_subdir('cur')
1586 message
.set_subdir('cur')
1587 message
.add_flag('S')
1588 if 'flagged' in sequences
:
1589 message
.add_flag('F')
1590 if 'replied' in sequences
:
1591 message
.add_flag('R')
1592 elif isinstance(message
, _mboxMMDFMessage
):
1593 sequences
= set(self
.get_sequences())
1594 if 'unseen' not in sequences
:
1595 message
.add_flag('RO')
1597 message
.add_flag('O')
1598 if 'flagged' in sequences
:
1599 message
.add_flag('F')
1600 if 'replied' in sequences
:
1601 message
.add_flag('A')
1602 elif isinstance(message
, MHMessage
):
1603 for sequence
in self
.get_sequences():
1604 message
.add_sequence(sequence
)
1605 elif isinstance(message
, BabylMessage
):
1606 sequences
= set(self
.get_sequences())
1607 if 'unseen' in sequences
:
1608 message
.add_label('unseen')
1609 if 'replied' in sequences
:
1610 message
.add_label('answered')
1611 elif isinstance(message
, Message
):
1614 raise TypeError('Cannot convert to specified type: %s' %
1618 class BabylMessage(Message
):
1619 """Message with Babyl-specific properties."""
1621 def __init__(self
, message
=None):
1622 """Initialize an BabylMessage instance."""
1624 self
._visible
= Message()
1625 Message
.__init
__(self
, message
)
1627 def get_labels(self
):
1628 """Return a list of labels on the message."""
1629 return self
._labels
[:]
1631 def set_labels(self
, labels
):
1632 """Set the list of labels on the message."""
1633 self
._labels
= list(labels
)
1635 def add_label(self
, label
):
1636 """Add label to list of labels on the message."""
1637 if isinstance(label
, str):
1638 if label
not in self
._labels
:
1639 self
._labels
.append(label
)
1641 raise TypeError('label must be a string: %s' % type(label
))
1643 def remove_label(self
, label
):
1644 """Remove label from the list of labels on the message."""
1646 self
._labels
.remove(label
)
1650 def get_visible(self
):
1651 """Return a Message representation of visible headers."""
1652 return Message(self
._visible
)
1654 def set_visible(self
, visible
):
1655 """Set the Message representation of visible headers."""
1656 self
._visible
= Message(visible
)
1658 def update_visible(self
):
1659 """Update and/or sensibly generate a set of visible headers."""
1660 for header
in self
._visible
.keys():
1662 self
._visible
.replace_header(header
, self
[header
])
1664 del self
._visible
[header
]
1665 for header
in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1666 if header
in self
and header
not in self
._visible
:
1667 self
._visible
[header
] = self
[header
]
1669 def _explain_to(self
, message
):
1670 """Copy Babyl-specific state to message insofar as possible."""
1671 if isinstance(message
, MaildirMessage
):
1672 labels
= set(self
.get_labels())
1673 if 'unseen' in labels
:
1674 message
.set_subdir('cur')
1676 message
.set_subdir('cur')
1677 message
.add_flag('S')
1678 if 'forwarded' in labels
or 'resent' in labels
:
1679 message
.add_flag('P')
1680 if 'answered' in labels
:
1681 message
.add_flag('R')
1682 if 'deleted' in labels
:
1683 message
.add_flag('T')
1684 elif isinstance(message
, _mboxMMDFMessage
):
1685 labels
= set(self
.get_labels())
1686 if 'unseen' not in labels
:
1687 message
.add_flag('RO')
1689 message
.add_flag('O')
1690 if 'deleted' in labels
:
1691 message
.add_flag('D')
1692 if 'answered' in labels
:
1693 message
.add_flag('A')
1694 elif isinstance(message
, MHMessage
):
1695 labels
= set(self
.get_labels())
1696 if 'unseen' in labels
:
1697 message
.add_sequence('unseen')
1698 if 'answered' in labels
:
1699 message
.add_sequence('replied')
1700 elif isinstance(message
, BabylMessage
):
1701 message
.set_visible(self
.get_visible())
1702 for label
in self
.get_labels():
1703 message
.add_label(label
)
1704 elif isinstance(message
, Message
):
1707 raise TypeError('Cannot convert to specified type: %s' %
1711 class MMDFMessage(_mboxMMDFMessage
):
1712 """Message with MMDF-specific properties."""
1716 """A read-only wrapper of a file."""
1718 def __init__(self
, f
, pos
=None):
1719 """Initialize a _ProxyFile."""
1722 self
._pos
= f
.tell()
1726 def read(self
, size
=None):
1728 return self
._read
(size
, self
._file
.read
)
1730 def readline(self
, size
=None):
1732 return self
._read
(size
, self
._file
.readline
)
1734 def readlines(self
, sizehint
=None):
1735 """Read multiple lines."""
1739 if sizehint
is not None:
1740 sizehint
-= len(line
)
1746 """Iterate over lines."""
1747 return iter(self
.readline
, "")
1750 """Return the position."""
1753 def seek(self
, offset
, whence
=0):
1754 """Change position."""
1756 self
._file
.seek(self
._pos
)
1757 self
._file
.seek(offset
, whence
)
1758 self
._pos
= self
._file
.tell()
1761 """Close the file."""
1764 def _read(self
, size
, read_method
):
1765 """Read size bytes using read_method."""
1768 self
._file
.seek(self
._pos
)
1769 result
= read_method(size
)
1770 self
._pos
= self
._file
.tell()
1774 class _PartialFile(_ProxyFile
):
1775 """A read-only wrapper of part of a file."""
1777 def __init__(self
, f
, start
=None, stop
=None):
1778 """Initialize a _PartialFile."""
1779 _ProxyFile
.__init
__(self
, f
, start
)
1784 """Return the position with respect to start."""
1785 return _ProxyFile
.tell(self
) - self
._start
1787 def seek(self
, offset
, whence
=0):
1788 """Change position, possibly with respect to start or stop."""
1790 self
._pos
= self
._start
1793 self
._pos
= self
._stop
1795 _ProxyFile
.seek(self
, offset
, whence
)
1797 def _read(self
, size
, read_method
):
1798 """Read size bytes using read_method, honoring start and stop."""
1799 remaining
= self
._stop
- self
._pos
1802 if size
is None or size
< 0 or size
> remaining
:
1804 return _ProxyFile
._read
(self
, size
, read_method
)
1807 def _lock_file(f
, dotlock
=True):
1808 """Lock file f using lockf and dot locking."""
1809 dotlock_done
= False
1813 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1815 if e
.errno
in (errno
.EAGAIN
, errno
.EACCES
):
1816 raise ExternalClashError('lockf: lock unavailable: %s' %
1822 pre_lock
= _create_temporary(f
.name
+ '.lock')
1825 if e
.errno
== errno
.EACCES
:
1826 return # Without write access, just skip dotlocking.
1830 if hasattr(os
, 'link'):
1831 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1833 os
.unlink(pre_lock
.name
)
1835 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
1838 if e
.errno
== errno
.EEXIST
or \
1839 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
1840 os
.remove(pre_lock
.name
)
1841 raise ExternalClashError('dot lock unavailable: %s' %
1847 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1849 os
.remove(f
.name
+ '.lock')
1852 def _unlock_file(f
):
1853 """Unlock file f using lockf and dot locking."""
1855 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1856 if os
.path
.exists(f
.name
+ '.lock'):
1857 os
.remove(f
.name
+ '.lock')
1859 def _create_carefully(path
):
1860 """Create a file if it doesn't exist and open for reading and writing."""
1861 fd
= os
.open(path
, os
.O_CREAT | os
.O_EXCL | os
.O_RDWR
)
1863 return open(path
, 'rb+')
1867 def _create_temporary(path
):
1868 """Create a temp file based on path and open for reading and writing."""
1869 return _create_carefully('%s.%s.%s.%s' % (path
, int(time
.time()),
1870 socket
.gethostname(),
1874 ## Start: classes from the original module (for backward compatibility).
1876 # Note that the Maildir class, whose name is unchanged, itself offers a next()
1877 # method for backward compatibility.
1881 def __init__(self
, fp
, factory
=rfc822
.Message
):
1884 self
.factory
= factory
1887 return iter(self
.next
, None)
1891 self
.fp
.seek(self
.seekp
)
1893 self
._search
_start
()
1895 self
.seekp
= self
.fp
.tell()
1897 start
= self
.fp
.tell()
1899 self
.seekp
= stop
= self
.fp
.tell()
1902 return self
.factory(_PartialFile(self
.fp
, start
, stop
))
1904 # Recommended to use PortableUnixMailbox instead!
1905 class UnixMailbox(_Mailbox
):
1907 def _search_start(self
):
1909 pos
= self
.fp
.tell()
1910 line
= self
.fp
.readline()
1913 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1917 def _search_end(self
):
1918 self
.fp
.readline() # Throw away header line
1920 pos
= self
.fp
.tell()
1921 line
= self
.fp
.readline()
1924 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1928 # An overridable mechanism to test for From-line-ness. You can either
1929 # specify a different regular expression or define a whole new
1930 # _isrealfromline() method. Note that this only gets called for lines
1931 # starting with the 5 characters "From ".
1934 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
1935 # the only portable, reliable way to find message delimiters in a BSD (i.e
1936 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
1937 # beginning of the file, "^From .*\n". While _fromlinepattern below seems
1938 # like a good idea, in practice, there are too many variations for more
1939 # strict parsing of the line to be completely accurate.
1941 # _strict_isrealfromline() is the old version which tries to do stricter
1942 # parsing of the From_ line. _portable_isrealfromline() simply returns
1943 # true, since it's never called if the line doesn't already start with
1946 # This algorithm, and the way it interacts with _search_start() and
1947 # _search_end() may not be completely correct, because it doesn't check
1948 # that the two characters preceding "From " are \n\n or the beginning of
1949 # the file. Fixing this would require a more extensive rewrite than is
1950 # necessary. For convenience, we've added a PortableUnixMailbox class
1951 # which uses the more lenient _fromlinepattern regular expression.
1953 _fromlinepattern
= r
"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+" \
1954 r
"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*$"
1957 def _strict_isrealfromline(self
, line
):
1958 if not self
._regexp
:
1960 self
._regexp
= re
.compile(self
._fromlinepattern
)
1961 return self
._regexp
.match(line
)
1963 def _portable_isrealfromline(self
, line
):
1966 _isrealfromline
= _strict_isrealfromline
1969 class PortableUnixMailbox(UnixMailbox
):
1970 _isrealfromline
= UnixMailbox
._portable
_isrealfromline
1973 class MmdfMailbox(_Mailbox
):
1975 def _search_start(self
):
1977 line
= self
.fp
.readline()
1980 if line
[:5] == '\001\001\001\001\n':
1983 def _search_end(self
):
1985 pos
= self
.fp
.tell()
1986 line
= self
.fp
.readline()
1989 if line
== '\001\001\001\001\n':
1996 def __init__(self
, dirname
, factory
=rfc822
.Message
):
1998 pat
= re
.compile('^[1-9][0-9]*$')
1999 self
.dirname
= dirname
2000 # the three following lines could be combined into:
2001 # list = map(long, filter(pat.match, os.listdir(self.dirname)))
2002 list = os
.listdir(self
.dirname
)
2003 list = filter(pat
.match
, list)
2004 list = map(long, list)
2006 # This only works in Python 1.6 or later;
2007 # before that str() added 'L':
2008 self
.boxes
= map(str, list)
2009 self
.boxes
.reverse()
2010 self
.factory
= factory
2013 return iter(self
.next
, None)
2018 fn
= self
.boxes
.pop()
2019 fp
= open(os
.path
.join(self
.dirname
, fn
))
2020 msg
= self
.factory(fp
)
2023 except (AttributeError, TypeError):
2028 class BabylMailbox(_Mailbox
):
2030 def _search_start(self
):
2032 line
= self
.fp
.readline()
2035 if line
== '*** EOOH ***\n':
2038 def _search_end(self
):
2040 pos
= self
.fp
.tell()
2041 line
= self
.fp
.readline()
2044 if line
== '\037\014\n' or line
== '\037':
2048 ## End: classes from the original module (for backward compatibility).
2051 class Error(Exception):
2052 """Raised for module-specific errors."""
2054 class NoSuchMailboxError(Error
):
2055 """The specified mailbox does not exist and won't be created."""
2057 class NotEmptyError(Error
):
2058 """The specified mailbox is not empty and deletion was requested."""
2060 class ExternalClashError(Error
):
2061 """Another process caused an action to fail."""
2063 class FormatError(Error
):
2064 """A file appears to have an invalid format."""