3 """Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes."""
14 import email
.Generator
22 __all__
= [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
23 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
24 'BabylMessage', 'MMDFMessage', 'UnixMailbox',
25 'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
28 """A group of messages in a particular place."""
30 def __init__(self
, path
, factory
=None, create
=True):
31 """Initialize a Mailbox instance."""
32 self
._path
= os
.path
.abspath(os
.path
.expanduser(path
))
33 self
._factory
= factory
35 def add(self
, message
):
36 """Add message and return assigned key."""
37 raise NotImplementedError('Method must be implemented by subclass')
39 def remove(self
, key
):
40 """Remove the keyed message; raise KeyError if it doesn't exist."""
41 raise NotImplementedError('Method must be implemented by subclass')
43 def __delitem__(self
, key
):
46 def discard(self
, key
):
47 """If the keyed message exists, remove it."""
53 def __setitem__(self
, key
, message
):
54 """Replace the keyed message; raise KeyError if it doesn't exist."""
55 raise NotImplementedError('Method must be implemented by subclass')
57 def get(self
, key
, default
=None):
58 """Return the keyed message, or default if it doesn't exist."""
60 return self
.__getitem
__(key
)
64 def __getitem__(self
, key
):
65 """Return the keyed message; raise KeyError if it doesn't exist."""
67 return self
.get_message(key
)
69 return self
._factory
(self
.get_file(key
))
71 def get_message(self
, key
):
72 """Return a Message representation or raise a KeyError."""
73 raise NotImplementedError('Method must be implemented by subclass')
75 def get_string(self
, key
):
76 """Return a string representation or raise a KeyError."""
77 raise NotImplementedError('Method must be implemented by subclass')
79 def get_file(self
, key
):
80 """Return a file-like representation or raise a KeyError."""
81 raise NotImplementedError('Method must be implemented by subclass')
84 """Return an iterator over keys."""
85 raise NotImplementedError('Method must be implemented by subclass')
88 """Return a list of keys."""
89 return list(self
.iterkeys())
92 """Return an iterator over all messages."""
93 for key
in self
.iterkeys():
101 return self
.itervalues()
104 """Return a list of messages. Memory intensive."""
105 return list(self
.itervalues())
108 """Return an iterator over (key, message) tuples."""
109 for key
in self
.iterkeys():
117 """Return a list of (key, message) tuples. Memory intensive."""
118 return list(self
.iteritems())
120 def has_key(self
, key
):
121 """Return True if the keyed message exists, False otherwise."""
122 raise NotImplementedError('Method must be implemented by subclass')
124 def __contains__(self
, key
):
125 return self
.has_key(key
)
128 """Return a count of messages in the mailbox."""
129 raise NotImplementedError('Method must be implemented by subclass')
132 """Delete all messages."""
133 for key
in self
.iterkeys():
136 def pop(self
, key
, default
=None):
137 """Delete the keyed message and return it, or default."""
146 """Delete an arbitrary (key, message) pair and return it."""
147 for key
in self
.iterkeys():
148 return (key
, self
.pop(key
)) # This is only run once.
150 raise KeyError('No messages in mailbox')
152 def update(self
, arg
=None):
153 """Change the messages that correspond to certain keys."""
154 if hasattr(arg
, 'iteritems'):
155 source
= arg
.iteritems()
156 elif hasattr(arg
, 'items'):
161 for key
, message
in source
:
167 raise KeyError('No message with key(s)')
170 """Write any pending changes to the disk."""
171 raise NotImplementedError('Method must be implemented by subclass')
174 """Lock the mailbox."""
175 raise NotImplementedError('Method must be implemented by subclass')
178 """Unlock the mailbox if it is locked."""
179 raise NotImplementedError('Method must be implemented by subclass')
182 """Flush and close the mailbox."""
183 raise NotImplementedError('Method must be implemented by subclass')
185 def _dump_message(self
, message
, target
, mangle_from_
=False):
186 # Most files are opened in binary mode to allow predictable seeking.
187 # To get native line endings on disk, the user-friendly \n line endings
188 # used in strings and by email.Message are translated here.
189 """Dump message contents to target file."""
190 if isinstance(message
, email
.Message
.Message
):
191 buffer = StringIO
.StringIO()
192 gen
= email
.Generator
.Generator(buffer, mangle_from_
, 0)
195 target
.write(buffer.read().replace('\n', os
.linesep
))
196 elif isinstance(message
, str):
198 message
= message
.replace('\nFrom ', '\n>From ')
199 message
= message
.replace('\n', os
.linesep
)
200 target
.write(message
)
201 elif hasattr(message
, 'read'):
203 line
= message
.readline()
206 if mangle_from_
and line
.startswith('From '):
207 line
= '>From ' + line
[5:]
208 line
= line
.replace('\n', os
.linesep
)
211 raise TypeError('Invalid message type: %s' % type(message
))
214 class Maildir(Mailbox
):
215 """A qmail-style Maildir mailbox."""
219 def __init__(self
, dirname
, factory
=rfc822
.Message
, create
=True):
220 """Initialize a Maildir instance."""
221 Mailbox
.__init
__(self
, dirname
, factory
, create
)
222 if not os
.path
.exists(self
._path
):
224 os
.mkdir(self
._path
, 0700)
225 os
.mkdir(os
.path
.join(self
._path
, 'tmp'), 0700)
226 os
.mkdir(os
.path
.join(self
._path
, 'new'), 0700)
227 os
.mkdir(os
.path
.join(self
._path
, 'cur'), 0700)
229 raise NoSuchMailboxError(self
._path
)
232 def add(self
, message
):
233 """Add message and return assigned key."""
234 tmp_file
= self
._create
_tmp
()
236 self
._dump
_message
(message
, tmp_file
)
239 if isinstance(message
, MaildirMessage
):
240 subdir
= message
.get_subdir()
241 suffix
= self
.colon
+ message
.get_info()
242 if suffix
== self
.colon
:
247 uniq
= os
.path
.basename(tmp_file
.name
).split(self
.colon
)[0]
248 dest
= os
.path
.join(self
._path
, subdir
, uniq
+ suffix
)
249 os
.rename(tmp_file
.name
, dest
)
250 if isinstance(message
, MaildirMessage
):
251 os
.utime(dest
, (os
.path
.getatime(dest
), message
.get_date()))
254 def remove(self
, key
):
255 """Remove the keyed message; raise KeyError if it doesn't exist."""
256 os
.remove(os
.path
.join(self
._path
, self
._lookup
(key
)))
258 def discard(self
, key
):
259 """If the keyed message exists, remove it."""
260 # This overrides an inapplicable implementation in the superclass.
266 if e
.errno
!= errno
.ENOENT
:
269 def __setitem__(self
, key
, message
):
270 """Replace the keyed message; raise KeyError if it doesn't exist."""
271 old_subpath
= self
._lookup
(key
)
272 temp_key
= self
.add(message
)
273 temp_subpath
= self
._lookup
(temp_key
)
274 if isinstance(message
, MaildirMessage
):
275 # temp's subdir and suffix were specified by message.
276 dominant_subpath
= temp_subpath
278 # temp's subdir and suffix were defaults from add().
279 dominant_subpath
= old_subpath
280 subdir
= os
.path
.dirname(dominant_subpath
)
281 if self
.colon
in dominant_subpath
:
282 suffix
= self
.colon
+ dominant_subpath
.split(self
.colon
)[-1]
286 new_path
= os
.path
.join(self
._path
, subdir
, key
+ suffix
)
287 os
.rename(os
.path
.join(self
._path
, temp_subpath
), new_path
)
288 if isinstance(message
, MaildirMessage
):
289 os
.utime(new_path
, (os
.path
.getatime(new_path
),
292 def get_message(self
, key
):
293 """Return a Message representation or raise a KeyError."""
294 subpath
= self
._lookup
(key
)
295 f
= open(os
.path
.join(self
._path
, subpath
), 'r')
297 msg
= MaildirMessage(f
)
300 subdir
, name
= os
.path
.split(subpath
)
301 msg
.set_subdir(subdir
)
302 if self
.colon
in name
:
303 msg
.set_info(name
.split(self
.colon
)[-1])
304 msg
.set_date(os
.path
.getmtime(os
.path
.join(self
._path
, subpath
)))
307 def get_string(self
, key
):
308 """Return a string representation or raise a KeyError."""
309 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r')
315 def get_file(self
, key
):
316 """Return a file-like representation or raise a KeyError."""
317 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'rb')
321 """Return an iterator over keys."""
323 for key
in self
._toc
:
330 def has_key(self
, key
):
331 """Return True if the keyed message exists, False otherwise."""
333 return key
in self
._toc
336 """Return a count of messages in the mailbox."""
338 return len(self
._toc
)
341 """Write any pending changes to disk."""
342 return # Maildir changes are always written immediately.
345 """Lock the mailbox."""
349 """Unlock the mailbox if it is locked."""
353 """Flush and close the mailbox."""
356 def list_folders(self
):
357 """Return a list of folder names."""
359 for entry
in os
.listdir(self
._path
):
360 if len(entry
) > 1 and entry
[0] == '.' and \
361 os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
362 result
.append(entry
[1:])
365 def get_folder(self
, folder
):
366 """Return a Maildir instance for the named folder."""
367 return Maildir(os
.path
.join(self
._path
, '.' + folder
), create
=False)
369 def add_folder(self
, folder
):
370 """Create a folder and return a Maildir instance representing it."""
371 path
= os
.path
.join(self
._path
, '.' + folder
)
372 result
= Maildir(path
)
373 maildirfolder_path
= os
.path
.join(path
, 'maildirfolder')
374 if not os
.path
.exists(maildirfolder_path
):
375 os
.close(os
.open(maildirfolder_path
, os
.O_CREAT | os
.O_WRONLY
))
378 def remove_folder(self
, folder
):
379 """Delete the named folder, which must be empty."""
380 path
= os
.path
.join(self
._path
, '.' + folder
)
381 for entry
in os
.listdir(os
.path
.join(path
, 'new')) + \
382 os
.listdir(os
.path
.join(path
, 'cur')):
383 if len(entry
) < 1 or entry
[0] != '.':
384 raise NotEmptyError('Folder contains message(s): %s' % folder
)
385 for entry
in os
.listdir(path
):
386 if entry
!= 'new' and entry
!= 'cur' and entry
!= 'tmp' and \
387 os
.path
.isdir(os
.path
.join(path
, entry
)):
388 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
390 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
392 os
.remove(os
.path
.join(root
, entry
))
394 os
.rmdir(os
.path
.join(root
, entry
))
398 """Delete old files in "tmp"."""
400 for entry
in os
.listdir(os
.path
.join(self
._path
, 'tmp')):
401 path
= os
.path
.join(self
._path
, 'tmp', entry
)
402 if now
- os
.path
.getatime(path
) > 129600: # 60 * 60 * 36
405 _count
= 1 # This is used to generate unique file names.
407 def _create_tmp(self
):
408 """Create a file in the tmp subdirectory and open and return it."""
410 hostname
= socket
.gethostname()
412 hostname
= hostname
.replace('/', r
'\057')
414 hostname
= hostname
.replace(':', r
'\072')
415 uniq
= "%s.M%sP%sQ%s.%s" % (int(now
), int(now
% 1 * 1e6
), os
.getpid(),
416 Maildir
._count
, hostname
)
417 path
= os
.path
.join(self
._path
, 'tmp', uniq
)
421 if e
.errno
== errno
.ENOENT
:
423 return open(path
, 'wb+')
427 raise ExternalClashError('Name clash prevented file creation: %s' %
431 """Update table of contents mapping."""
433 for subdir
in ('new', 'cur'):
434 for entry
in os
.listdir(os
.path
.join(self
._path
, subdir
)):
435 uniq
= entry
.split(self
.colon
)[0]
436 self
._toc
[uniq
] = os
.path
.join(subdir
, entry
)
438 def _lookup(self
, key
):
439 """Use TOC to return subpath for given key, or raise a KeyError."""
441 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
442 return self
._toc
[key
]
447 return self
._toc
[key
]
449 raise KeyError('No message with key: %s' % key
)
451 # This method is for backward compatibility only.
453 """Return the next message in a one-time iteration."""
454 if not hasattr(self
, '_onetime_keys'):
455 self
._onetime
_keys
= self
.iterkeys()
458 return self
[self
._onetime
_keys
.next()]
459 except StopIteration:
465 class _singlefileMailbox(Mailbox
):
466 """A single-file mailbox."""
468 def __init__(self
, path
, factory
=None, create
=True):
469 """Initialize a single-file mailbox."""
470 Mailbox
.__init
__(self
, path
, factory
, create
)
472 f
= open(self
._path
, 'rb+')
474 if e
.errno
== errno
.ENOENT
:
476 f
= open(self
._path
, 'wb+')
478 raise NoSuchMailboxError(self
._path
)
479 elif e
.errno
== errno
.EACCES
:
480 f
= open(self
._path
, 'rb')
486 self
._pending
= False # No changes require rewriting the file.
489 def add(self
, message
):
490 """Add message and return assigned key."""
492 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
495 return self
._next
_key
- 1
497 def remove(self
, key
):
498 """Remove the keyed message; raise KeyError if it doesn't exist."""
503 def __setitem__(self
, key
, message
):
504 """Replace the keyed message; raise KeyError if it doesn't exist."""
506 self
._toc
[key
] = self
._append
_message
(message
)
510 """Return an iterator over keys."""
512 for key
in self
._toc
.keys():
515 def has_key(self
, key
):
516 """Return True if the keyed message exists, False otherwise."""
518 return key
in self
._toc
521 """Return a count of messages in the mailbox."""
523 return len(self
._toc
)
526 """Lock the mailbox."""
528 _lock_file(self
._file
)
532 """Unlock the mailbox if it is locked."""
534 _unlock_file(self
._file
)
538 """Write any pending changes to disk."""
539 if not self
._pending
:
542 new_file
= _create_temporary(self
._path
)
545 self
._pre
_mailbox
_hook
(new_file
)
546 for key
in sorted(self
._toc
.keys()):
547 start
, stop
= self
._toc
[key
]
548 self
._file
.seek(start
)
549 self
._pre
_message
_hook
(new_file
)
550 new_start
= new_file
.tell()
552 buffer = self
._file
.read(min(4096,
553 stop
- self
._file
.tell()))
556 new_file
.write(buffer)
557 new_toc
[key
] = (new_start
, new_file
.tell())
558 self
._post
_message
_hook
(new_file
)
561 os
.remove(new_file
.name
)
566 os
.rename(new_file
.name
, self
._path
)
568 if e
.errno
== errno
.EEXIST
:
569 os
.remove(self
._path
)
570 os
.rename(new_file
.name
, self
._path
)
573 self
._file
= open(self
._path
, 'rb+')
575 self
._pending
= False
577 _lock_file(new_file
, dotlock
=False)
579 def _pre_mailbox_hook(self
, f
):
580 """Called before writing the mailbox to file f."""
583 def _pre_message_hook(self
, f
):
584 """Called before writing each message to file f."""
587 def _post_message_hook(self
, f
):
588 """Called after writing each message to file f."""
592 """Flush and close the mailbox."""
598 def _lookup(self
, key
=None):
599 """Return (start, stop) or raise KeyError."""
600 if self
._toc
is None:
604 return self
._toc
[key
]
606 raise KeyError('No message with key: %s' % key
)
608 def _append_message(self
, message
):
609 """Append message to mailbox and return (start, stop) offsets."""
610 self
._file
.seek(0, 2)
611 self
._pre
_message
_hook
(self
._file
)
612 offsets
= self
._install
_message
(message
)
613 self
._post
_message
_hook
(self
._file
)
619 class _mboxMMDF(_singlefileMailbox
):
620 """An mbox or MMDF mailbox."""
624 def get_message(self
, key
):
625 """Return a Message representation or raise a KeyError."""
626 start
, stop
= self
._lookup
(key
)
627 self
._file
.seek(start
)
628 from_line
= self
._file
.readline().replace(os
.linesep
, '')
629 string
= self
._file
.read(stop
- self
._file
.tell())
630 msg
= self
._message
_factory
(string
.replace(os
.linesep
, '\n'))
631 msg
.set_from(from_line
[5:])
634 def get_string(self
, key
, from_
=False):
635 """Return a string representation or raise a KeyError."""
636 start
, stop
= self
._lookup
(key
)
637 self
._file
.seek(start
)
639 self
._file
.readline()
640 string
= self
._file
.read(stop
- self
._file
.tell())
641 return string
.replace(os
.linesep
, '\n')
643 def get_file(self
, key
, from_
=False):
644 """Return a file-like representation or raise a KeyError."""
645 start
, stop
= self
._lookup
(key
)
646 self
._file
.seek(start
)
648 self
._file
.readline()
649 return _PartialFile(self
._file
, self
._file
.tell(), stop
)
651 def _install_message(self
, message
):
652 """Format a message and blindly write to self._file."""
654 if isinstance(message
, str) and message
.startswith('From '):
655 newline
= message
.find('\n')
657 from_line
= message
[:newline
]
658 message
= message
[newline
+ 1:]
662 elif isinstance(message
, _mboxMMDFMessage
):
663 from_line
= 'From ' + message
.get_from()
664 elif isinstance(message
, email
.Message
.Message
):
665 from_line
= message
.get_unixfrom() # May be None.
666 if from_line
is None:
667 from_line
= 'From MAILER-DAEMON %s' % time
.asctime(time
.gmtime())
668 start
= self
._file
.tell()
669 self
._file
.write(from_line
+ os
.linesep
)
670 self
._dump
_message
(message
, self
._file
, self
._mangle
_from
_)
671 stop
= self
._file
.tell()
675 class mbox(_mboxMMDF
):
676 """A classic mbox mailbox."""
680 def __init__(self
, path
, factory
=None, create
=True):
681 """Initialize an mbox mailbox."""
682 self
._message
_factory
= mboxMessage
683 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
685 def _pre_message_hook(self
, f
):
686 """Called before writing each message to file f."""
690 def _generate_toc(self
):
691 """Generate key-to-(start, stop) table of contents."""
692 starts
, stops
= [], []
695 line_pos
= self
._file
.tell()
696 line
= self
._file
.readline()
697 if line
.startswith('From '):
698 if len(stops
) < len(starts
):
699 stops
.append(line_pos
- len(os
.linesep
))
700 starts
.append(line_pos
)
702 stops
.append(line_pos
)
704 self
._toc
= dict(enumerate(zip(starts
, stops
)))
705 self
._next
_key
= len(self
._toc
)
708 class MMDF(_mboxMMDF
):
709 """An MMDF mailbox."""
711 def __init__(self
, path
, factory
=None, create
=True):
712 """Initialize an MMDF mailbox."""
713 self
._message
_factory
= MMDFMessage
714 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
716 def _pre_message_hook(self
, f
):
717 """Called before writing each message to file f."""
718 f
.write('\001\001\001\001' + os
.linesep
)
720 def _post_message_hook(self
, f
):
721 """Called after writing each message to file f."""
722 f
.write(os
.linesep
+ '\001\001\001\001' + os
.linesep
)
724 def _generate_toc(self
):
725 """Generate key-to-(start, stop) table of contents."""
726 starts
, stops
= [], []
731 line
= self
._file
.readline()
732 next_pos
= self
._file
.tell()
733 if line
.startswith('\001\001\001\001' + os
.linesep
):
734 starts
.append(next_pos
)
737 line
= self
._file
.readline()
738 next_pos
= self
._file
.tell()
739 if line
== '\001\001\001\001' + os
.linesep
:
740 stops
.append(line_pos
- len(os
.linesep
))
743 stops
.append(line_pos
)
747 self
._toc
= dict(enumerate(zip(starts
, stops
)))
748 self
._next
_key
= len(self
._toc
)
754 def __init__(self
, path
, factory
=None, create
=True):
755 """Initialize an MH instance."""
756 Mailbox
.__init
__(self
, path
, factory
, create
)
757 if not os
.path
.exists(self
._path
):
759 os
.mkdir(self
._path
, 0700)
760 os
.close(os
.open(os
.path
.join(self
._path
, '.mh_sequences'),
761 os
.O_CREAT | os
.O_EXCL | os
.O_WRONLY
, 0600))
763 raise NoSuchMailboxError(self
._path
)
766 def add(self
, message
):
767 """Add message and return assigned key."""
772 new_key
= max(keys
) + 1
773 new_path
= os
.path
.join(self
._path
, str(new_key
))
774 f
= _create_carefully(new_path
)
779 self
._dump
_message
(message
, f
)
780 if isinstance(message
, MHMessage
):
781 self
._dump
_sequences
(message
, new_key
)
789 def remove(self
, key
):
790 """Remove the keyed message; raise KeyError if it doesn't exist."""
791 path
= os
.path
.join(self
._path
, str(key
))
793 f
= open(path
, 'rb+')
795 if e
.errno
== errno
.ENOENT
:
796 raise KeyError('No message with key: %s' % key
)
804 os
.remove(os
.path
.join(self
._path
, str(key
)))
811 def __setitem__(self
, key
, message
):
812 """Replace the keyed message; raise KeyError if it doesn't exist."""
813 path
= os
.path
.join(self
._path
, str(key
))
815 f
= open(path
, 'rb+')
817 if e
.errno
== errno
.ENOENT
:
818 raise KeyError('No message with key: %s' % key
)
825 os
.close(os
.open(path
, os
.O_WRONLY | os
.O_TRUNC
))
826 self
._dump
_message
(message
, f
)
827 if isinstance(message
, MHMessage
):
828 self
._dump
_sequences
(message
, key
)
835 def get_message(self
, key
):
836 """Return a Message representation or raise a KeyError."""
839 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
841 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
843 if e
.errno
== errno
.ENOENT
:
844 raise KeyError('No message with key: %s' % key
)
857 for name
, key_list
in self
.get_sequences():
859 msg
.add_sequence(name
)
862 def get_string(self
, key
):
863 """Return a string representation or raise a KeyError."""
866 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
868 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
870 if e
.errno
== errno
.ENOENT
:
871 raise KeyError('No message with key: %s' % key
)
885 def get_file(self
, key
):
886 """Return a file-like representation or raise a KeyError."""
888 f
= open(os
.path
.join(self
._path
, str(key
)), 'rb')
890 if e
.errno
== errno
.ENOENT
:
891 raise KeyError('No message with key: %s' % key
)
897 """Return an iterator over keys."""
898 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
901 def has_key(self
, key
):
902 """Return True if the keyed message exists, False otherwise."""
903 return os
.path
.exists(os
.path
.join(self
._path
, str(key
)))
906 """Return a count of messages in the mailbox."""
907 return len(list(self
.iterkeys()))
910 """Lock the mailbox."""
912 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
913 _lock_file(self
._file
)
917 """Unlock the mailbox if it is locked."""
919 _unlock_file(self
._file
)
925 """Write any pending changes to the disk."""
929 """Flush and close the mailbox."""
933 def list_folders(self
):
934 """Return a list of folder names."""
936 for entry
in os
.listdir(self
._path
):
937 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
941 def get_folder(self
, folder
):
942 """Return an MH instance for the named folder."""
943 return MH(os
.path
.join(self
._path
, folder
), create
=False)
945 def add_folder(self
, folder
):
946 """Create a folder and return an MH instance representing it."""
947 return MH(os
.path
.join(self
._path
, folder
))
949 def remove_folder(self
, folder
):
950 """Delete the named folder, which must be empty."""
951 path
= os
.path
.join(self
._path
, folder
)
952 entries
= os
.listdir(path
)
953 if entries
== ['.mh_sequences']:
954 os
.remove(os
.path
.join(path
, '.mh_sequences'))
958 raise NotEmptyError('Folder not empty: %s' % self
._path
)
961 def get_sequences(self
):
962 """Return a name-to-key-list dictionary to define each sequence."""
964 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r')
966 all_keys
= set(self
.keys())
969 name
, contents
= line
.split(':')
971 for spec
in contents
.split():
975 start
, stop
= (int(x
) for x
in spec
.split('-'))
976 keys
.update(range(start
, stop
+ 1))
977 results
[name
] = [key
for key
in sorted(keys
) \
979 if len(results
[name
]) == 0:
982 raise FormatError('Invalid sequence specification: %s' %
988 def set_sequences(self
, sequences
):
989 """Set sequences using the given name-to-key-list dictionary."""
990 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r+')
992 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
993 for name
, keys
in sequences
.iteritems():
996 f
.write('%s:' % name
)
999 for key
in sorted(set(keys
)):
1006 f
.write('%s %s' % (prev
, key
))
1008 f
.write(' %s' % key
)
1011 f
.write(str(prev
) + '\n')
1018 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1019 sequences
= self
.get_sequences()
1022 for key
in self
.iterkeys():
1024 changes
.append((key
, prev
+ 1))
1025 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
1030 if hasattr(os
, 'link'):
1031 os
.link(os
.path
.join(self
._path
, str(key
)),
1032 os
.path
.join(self
._path
, str(prev
+ 1)))
1033 os
.unlink(os
.path
.join(self
._path
, str(key
)))
1036 os
.rename(os
.path
.join(self
._path
, str(key
)),
1037 os
.path
.join(self
._path
, str(prev
+ 1)))
1044 self
._next
_key
= prev
+ 1
1045 if len(changes
) == 0:
1047 for name
, key_list
in sequences
.items():
1048 for old
, new
in changes
:
1050 key_list
[key_list
.index(old
)] = new
1051 self
.set_sequences(sequences
)
1053 def _dump_sequences(self
, message
, key
):
1054 """Inspect a new MHMessage and update sequences appropriately."""
1055 pending_sequences
= message
.get_sequences()
1056 all_sequences
= self
.get_sequences()
1057 for name
, key_list
in all_sequences
.iteritems():
1058 if name
in pending_sequences
:
1059 key_list
.append(key
)
1060 elif key
in key_list
:
1061 del key_list
[key_list
.index(key
)]
1062 for sequence
in pending_sequences
:
1063 if sequence
not in all_sequences
:
1064 all_sequences
[sequence
] = [key
]
1065 self
.set_sequences(all_sequences
)
1068 class Babyl(_singlefileMailbox
):
1069 """An Rmail-style Babyl mailbox."""
1071 _special_labels
= frozenset(('unseen', 'deleted', 'filed', 'answered',
1072 'forwarded', 'edited', 'resent'))
1074 def __init__(self
, path
, factory
=None, create
=True):
1075 """Initialize a Babyl mailbox."""
1076 _singlefileMailbox
.__init
__(self
, path
, factory
, create
)
1079 def add(self
, message
):
1080 """Add message and return assigned key."""
1081 key
= _singlefileMailbox
.add(self
, message
)
1082 if isinstance(message
, BabylMessage
):
1083 self
._labels
[key
] = message
.get_labels()
1086 def remove(self
, key
):
1087 """Remove the keyed message; raise KeyError if it doesn't exist."""
1088 _singlefileMailbox
.remove(self
, key
)
1089 if key
in self
._labels
:
1090 del self
._labels
[key
]
1092 def __setitem__(self
, key
, message
):
1093 """Replace the keyed message; raise KeyError if it doesn't exist."""
1094 _singlefileMailbox
.__setitem
__(self
, key
, message
)
1095 if isinstance(message
, BabylMessage
):
1096 self
._labels
[key
] = message
.get_labels()
1098 def get_message(self
, key
):
1099 """Return a Message representation or raise a KeyError."""
1100 start
, stop
= self
._lookup
(key
)
1101 self
._file
.seek(start
)
1102 self
._file
.readline() # Skip '1,' line specifying labels.
1103 original_headers
= StringIO
.StringIO()
1105 line
= self
._file
.readline()
1106 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1108 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1109 visible_headers
= StringIO
.StringIO()
1111 line
= self
._file
.readline()
1112 if line
== os
.linesep
or line
== '':
1114 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1115 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1117 msg
= BabylMessage(original_headers
.getvalue() + body
)
1118 msg
.set_visible(visible_headers
.getvalue())
1119 if key
in self
._labels
:
1120 msg
.set_labels(self
._labels
[key
])
1123 def get_string(self
, key
):
1124 """Return a string representation or raise a KeyError."""
1125 start
, stop
= self
._lookup
(key
)
1126 self
._file
.seek(start
)
1127 self
._file
.readline() # Skip '1,' line specifying labels.
1128 original_headers
= StringIO
.StringIO()
1130 line
= self
._file
.readline()
1131 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1133 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1135 line
= self
._file
.readline()
1136 if line
== os
.linesep
or line
== '':
1138 return original_headers
.getvalue() + \
1139 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1142 def get_file(self
, key
):
1143 """Return a file-like representation or raise a KeyError."""
1144 return StringIO
.StringIO(self
.get_string(key
).replace('\n',
1147 def get_labels(self
):
1148 """Return a list of user-defined labels in the mailbox."""
1151 for label_list
in self
._labels
.values():
1152 labels
.update(label_list
)
1153 labels
.difference_update(self
._special
_labels
)
1156 def _generate_toc(self
):
1157 """Generate key-to-(start, stop) table of contents."""
1158 starts
, stops
= [], []
1164 line
= self
._file
.readline()
1165 next_pos
= self
._file
.tell()
1166 if line
== '\037\014' + os
.linesep
:
1167 if len(stops
) < len(starts
):
1168 stops
.append(line_pos
- len(os
.linesep
))
1169 starts
.append(next_pos
)
1170 labels
= [label
.strip() for label
1171 in self
._file
.readline()[1:].split(',')
1172 if label
.strip() != '']
1173 label_lists
.append(labels
)
1174 elif line
== '\037' or line
== '\037' + os
.linesep
:
1175 if len(stops
) < len(starts
):
1176 stops
.append(line_pos
- len(os
.linesep
))
1178 stops
.append(line_pos
- len(os
.linesep
))
1180 self
._toc
= dict(enumerate(zip(starts
, stops
)))
1181 self
._labels
= dict(enumerate(label_lists
))
1182 self
._next
_key
= len(self
._toc
)
1184 def _pre_mailbox_hook(self
, f
):
1185 """Called before writing the mailbox to file f."""
1186 f
.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1187 (os
.linesep
, os
.linesep
, ','.join(self
.get_labels()),
1190 def _pre_message_hook(self
, f
):
1191 """Called before writing each message to file f."""
1192 f
.write('\014' + os
.linesep
)
1194 def _post_message_hook(self
, f
):
1195 """Called after writing each message to file f."""
1196 f
.write(os
.linesep
+ '\037')
1198 def _install_message(self
, message
):
1199 """Write message contents and return (start, stop)."""
1200 start
= self
._file
.tell()
1201 if isinstance(message
, BabylMessage
):
1204 for label
in message
.get_labels():
1205 if label
in self
._special
_labels
:
1206 special_labels
.append(label
)
1208 labels
.append(label
)
1209 self
._file
.write('1')
1210 for label
in special_labels
:
1211 self
._file
.write(', ' + label
)
1212 self
._file
.write(',,')
1213 for label
in labels
:
1214 self
._file
.write(' ' + label
+ ',')
1215 self
._file
.write(os
.linesep
)
1217 self
._file
.write('1,,' + os
.linesep
)
1218 if isinstance(message
, email
.Message
.Message
):
1219 orig_buffer
= StringIO
.StringIO()
1220 orig_generator
= email
.Generator
.Generator(orig_buffer
, False, 0)
1221 orig_generator
.flatten(message
)
1224 line
= orig_buffer
.readline()
1225 self
._file
.write(line
.replace('\n', os
.linesep
))
1226 if line
== '\n' or line
== '':
1228 self
._file
.write('*** EOOH ***' + os
.linesep
)
1229 if isinstance(message
, BabylMessage
):
1230 vis_buffer
= StringIO
.StringIO()
1231 vis_generator
= email
.Generator
.Generator(vis_buffer
, False, 0)
1232 vis_generator
.flatten(message
.get_visible())
1234 line
= vis_buffer
.readline()
1235 self
._file
.write(line
.replace('\n', os
.linesep
))
1236 if line
== '\n' or line
== '':
1241 line
= orig_buffer
.readline()
1242 self
._file
.write(line
.replace('\n', os
.linesep
))
1243 if line
== '\n' or line
== '':
1246 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
1249 self
._file
.write(buffer.replace('\n', os
.linesep
))
1250 elif isinstance(message
, str):
1251 body_start
= message
.find('\n\n') + 2
1252 if body_start
- 2 != -1:
1253 self
._file
.write(message
[:body_start
].replace('\n',
1255 self
._file
.write('*** EOOH ***' + os
.linesep
)
1256 self
._file
.write(message
[:body_start
].replace('\n',
1258 self
._file
.write(message
[body_start
:].replace('\n',
1261 self
._file
.write('*** EOOH ***' + os
.linesep
+ os
.linesep
)
1262 self
._file
.write(message
.replace('\n', os
.linesep
))
1263 elif hasattr(message
, 'readline'):
1264 original_pos
= message
.tell()
1267 line
= message
.readline()
1268 self
._file
.write(line
.replace('\n', os
.linesep
))
1269 if line
== '\n' or line
== '':
1270 self
._file
.write('*** EOOH ***' + os
.linesep
)
1273 message
.seek(original_pos
)
1277 buffer = message
.read(4096) # Buffer size is arbitrary.
1280 self
._file
.write(buffer.replace('\n', os
.linesep
))
1282 raise TypeError('Invalid message type: %s' % type(message
))
1283 stop
= self
._file
.tell()
1284 return (start
, stop
)
1287 class Message(email
.Message
.Message
):
1288 """Message with mailbox-format-specific properties."""
1290 def __init__(self
, message
=None):
1291 """Initialize a Message instance."""
1292 if isinstance(message
, email
.Message
.Message
):
1293 self
._become
_message
(copy
.deepcopy(message
))
1294 if isinstance(message
, Message
):
1295 message
._explain
_to
(self
)
1296 elif isinstance(message
, str):
1297 self
._become
_message
(email
.message_from_string(message
))
1298 elif hasattr(message
, "read"):
1299 self
._become
_message
(email
.message_from_file(message
))
1300 elif message
is None:
1301 email
.Message
.Message
.__init
__(self
)
1303 raise TypeError('Invalid message type: %s' % type(message
))
1305 def _become_message(self
, message
):
1306 """Assume the non-format-specific state of message."""
1307 for name
in ('_headers', '_unixfrom', '_payload', '_charset',
1308 'preamble', 'epilogue', 'defects', '_default_type'):
1309 self
.__dict
__[name
] = message
.__dict
__[name
]
1311 def _explain_to(self
, message
):
1312 """Copy format-specific state to message insofar as possible."""
1313 if isinstance(message
, Message
):
1314 return # There's nothing format-specific to explain.
1316 raise TypeError('Cannot convert to specified type')
1319 class MaildirMessage(Message
):
1320 """Message with Maildir-specific properties."""
1322 def __init__(self
, message
=None):
1323 """Initialize a MaildirMessage instance."""
1324 self
._subdir
= 'new'
1326 self
._date
= time
.time()
1327 Message
.__init
__(self
, message
)
1329 def get_subdir(self
):
1330 """Return 'new' or 'cur'."""
1333 def set_subdir(self
, subdir
):
1334 """Set subdir to 'new' or 'cur'."""
1335 if subdir
== 'new' or subdir
== 'cur':
1336 self
._subdir
= subdir
1338 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir
)
1340 def get_flags(self
):
1341 """Return as a string the flags that are set."""
1342 if self
._info
.startswith('2,'):
1343 return self
._info
[2:]
1347 def set_flags(self
, flags
):
1348 """Set the given flags and unset all others."""
1349 self
._info
= '2,' + ''.join(sorted(flags
))
1351 def add_flag(self
, flag
):
1352 """Set the given flag(s) without changing others."""
1353 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1355 def remove_flag(self
, flag
):
1356 """Unset the given string flag(s) without changing others."""
1357 if self
.get_flags() != '':
1358 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1361 """Return delivery date of message, in seconds since the epoch."""
1364 def set_date(self
, date
):
1365 """Set delivery date of message, in seconds since the epoch."""
1367 self
._date
= float(date
)
1369 raise TypeError("can't convert to float: %s" % date
)
1372 """Get the message's "info" as a string."""
1375 def set_info(self
, info
):
1376 """Set the message's "info" string."""
1377 if isinstance(info
, str):
1380 raise TypeError('info must be a string: %s' % type(info
))
1382 def _explain_to(self
, message
):
1383 """Copy Maildir-specific state to message insofar as possible."""
1384 if isinstance(message
, MaildirMessage
):
1385 message
.set_flags(self
.get_flags())
1386 message
.set_subdir(self
.get_subdir())
1387 message
.set_date(self
.get_date())
1388 elif isinstance(message
, _mboxMMDFMessage
):
1389 flags
= set(self
.get_flags())
1391 message
.add_flag('R')
1392 if self
.get_subdir() == 'cur':
1393 message
.add_flag('O')
1395 message
.add_flag('D')
1397 message
.add_flag('F')
1399 message
.add_flag('A')
1400 message
.set_from('MAILER-DAEMON', time
.gmtime(self
.get_date()))
1401 elif isinstance(message
, MHMessage
):
1402 flags
= set(self
.get_flags())
1403 if 'S' not in flags
:
1404 message
.add_sequence('unseen')
1406 message
.add_sequence('replied')
1408 message
.add_sequence('flagged')
1409 elif isinstance(message
, BabylMessage
):
1410 flags
= set(self
.get_flags())
1411 if 'S' not in flags
:
1412 message
.add_label('unseen')
1414 message
.add_label('deleted')
1416 message
.add_label('answered')
1418 message
.add_label('forwarded')
1419 elif isinstance(message
, Message
):
1422 raise TypeError('Cannot convert to specified type: %s' %
1426 class _mboxMMDFMessage(Message
):
1427 """Message with mbox- or MMDF-specific properties."""
1429 def __init__(self
, message
=None):
1430 """Initialize an mboxMMDFMessage instance."""
1431 self
.set_from('MAILER-DAEMON', True)
1432 if isinstance(message
, email
.Message
.Message
):
1433 unixfrom
= message
.get_unixfrom()
1434 if unixfrom
is not None and unixfrom
.startswith('From '):
1435 self
.set_from(unixfrom
[5:])
1436 Message
.__init
__(self
, message
)
1439 """Return contents of "From " line."""
1442 def set_from(self
, from_
, time_
=None):
1443 """Set "From " line, formatting and appending time_ if specified."""
1444 if time_
is not None:
1446 time_
= time
.gmtime()
1447 from_
+= ' ' + time
.asctime(time_
)
1450 def get_flags(self
):
1451 """Return as a string the flags that are set."""
1452 return self
.get('Status', '') + self
.get('X-Status', '')
1454 def set_flags(self
, flags
):
1455 """Set the given flags and unset all others."""
1457 status_flags
, xstatus_flags
= '', ''
1458 for flag
in ('R', 'O'):
1460 status_flags
+= flag
1462 for flag
in ('D', 'F', 'A'):
1464 xstatus_flags
+= flag
1466 xstatus_flags
+= ''.join(sorted(flags
))
1468 self
.replace_header('Status', status_flags
)
1470 self
.add_header('Status', status_flags
)
1472 self
.replace_header('X-Status', xstatus_flags
)
1474 self
.add_header('X-Status', xstatus_flags
)
1476 def add_flag(self
, flag
):
1477 """Set the given flag(s) without changing others."""
1478 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1480 def remove_flag(self
, flag
):
1481 """Unset the given string flag(s) without changing others."""
1482 if 'Status' in self
or 'X-Status' in self
:
1483 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1485 def _explain_to(self
, message
):
1486 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1487 if isinstance(message
, MaildirMessage
):
1488 flags
= set(self
.get_flags())
1490 message
.set_subdir('cur')
1492 message
.add_flag('F')
1494 message
.add_flag('R')
1496 message
.add_flag('S')
1498 message
.add_flag('T')
1499 del message
['status']
1500 del message
['x-status']
1501 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1503 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1504 '%a %b %d %H:%M:%S %Y')))
1505 except (ValueError, OverflowError):
1507 elif isinstance(message
, _mboxMMDFMessage
):
1508 message
.set_flags(self
.get_flags())
1509 message
.set_from(self
.get_from())
1510 elif isinstance(message
, MHMessage
):
1511 flags
= set(self
.get_flags())
1512 if 'R' not in flags
:
1513 message
.add_sequence('unseen')
1515 message
.add_sequence('replied')
1517 message
.add_sequence('flagged')
1518 del message
['status']
1519 del message
['x-status']
1520 elif isinstance(message
, BabylMessage
):
1521 flags
= set(self
.get_flags())
1522 if 'R' not in flags
:
1523 message
.add_label('unseen')
1525 message
.add_label('deleted')
1527 message
.add_label('answered')
1528 del message
['status']
1529 del message
['x-status']
1530 elif isinstance(message
, Message
):
1533 raise TypeError('Cannot convert to specified type: %s' %
1537 class mboxMessage(_mboxMMDFMessage
):
1538 """Message with mbox-specific properties."""
1541 class MHMessage(Message
):
1542 """Message with MH-specific properties."""
1544 def __init__(self
, message
=None):
1545 """Initialize an MHMessage instance."""
1546 self
._sequences
= []
1547 Message
.__init
__(self
, message
)
1549 def get_sequences(self
):
1550 """Return a list of sequences that include the message."""
1551 return self
._sequences
[:]
1553 def set_sequences(self
, sequences
):
1554 """Set the list of sequences that include the message."""
1555 self
._sequences
= list(sequences
)
1557 def add_sequence(self
, sequence
):
1558 """Add sequence to list of sequences including the message."""
1559 if isinstance(sequence
, str):
1560 if not sequence
in self
._sequences
:
1561 self
._sequences
.append(sequence
)
1563 raise TypeError('sequence must be a string: %s' % type(sequence
))
1565 def remove_sequence(self
, sequence
):
1566 """Remove sequence from the list of sequences including the message."""
1568 self
._sequences
.remove(sequence
)
1572 def _explain_to(self
, message
):
1573 """Copy MH-specific state to message insofar as possible."""
1574 if isinstance(message
, MaildirMessage
):
1575 sequences
= set(self
.get_sequences())
1576 if 'unseen' in sequences
:
1577 message
.set_subdir('cur')
1579 message
.set_subdir('cur')
1580 message
.add_flag('S')
1581 if 'flagged' in sequences
:
1582 message
.add_flag('F')
1583 if 'replied' in sequences
:
1584 message
.add_flag('R')
1585 elif isinstance(message
, _mboxMMDFMessage
):
1586 sequences
= set(self
.get_sequences())
1587 if 'unseen' not in sequences
:
1588 message
.add_flag('RO')
1590 message
.add_flag('O')
1591 if 'flagged' in sequences
:
1592 message
.add_flag('F')
1593 if 'replied' in sequences
:
1594 message
.add_flag('A')
1595 elif isinstance(message
, MHMessage
):
1596 for sequence
in self
.get_sequences():
1597 message
.add_sequence(sequence
)
1598 elif isinstance(message
, BabylMessage
):
1599 sequences
= set(self
.get_sequences())
1600 if 'unseen' in sequences
:
1601 message
.add_label('unseen')
1602 if 'replied' in sequences
:
1603 message
.add_label('answered')
1604 elif isinstance(message
, Message
):
1607 raise TypeError('Cannot convert to specified type: %s' %
1611 class BabylMessage(Message
):
1612 """Message with Babyl-specific properties."""
1614 def __init__(self
, message
=None):
1615 """Initialize an BabylMessage instance."""
1617 self
._visible
= Message()
1618 Message
.__init
__(self
, message
)
1620 def get_labels(self
):
1621 """Return a list of labels on the message."""
1622 return self
._labels
[:]
1624 def set_labels(self
, labels
):
1625 """Set the list of labels on the message."""
1626 self
._labels
= list(labels
)
1628 def add_label(self
, label
):
1629 """Add label to list of labels on the message."""
1630 if isinstance(label
, str):
1631 if label
not in self
._labels
:
1632 self
._labels
.append(label
)
1634 raise TypeError('label must be a string: %s' % type(label
))
1636 def remove_label(self
, label
):
1637 """Remove label from the list of labels on the message."""
1639 self
._labels
.remove(label
)
1643 def get_visible(self
):
1644 """Return a Message representation of visible headers."""
1645 return Message(self
._visible
)
1647 def set_visible(self
, visible
):
1648 """Set the Message representation of visible headers."""
1649 self
._visible
= Message(visible
)
1651 def update_visible(self
):
1652 """Update and/or sensibly generate a set of visible headers."""
1653 for header
in self
._visible
.keys():
1655 self
._visible
.replace_header(header
, self
[header
])
1657 del self
._visible
[header
]
1658 for header
in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1659 if header
in self
and header
not in self
._visible
:
1660 self
._visible
[header
] = self
[header
]
1662 def _explain_to(self
, message
):
1663 """Copy Babyl-specific state to message insofar as possible."""
1664 if isinstance(message
, MaildirMessage
):
1665 labels
= set(self
.get_labels())
1666 if 'unseen' in labels
:
1667 message
.set_subdir('cur')
1669 message
.set_subdir('cur')
1670 message
.add_flag('S')
1671 if 'forwarded' in labels
or 'resent' in labels
:
1672 message
.add_flag('P')
1673 if 'answered' in labels
:
1674 message
.add_flag('R')
1675 if 'deleted' in labels
:
1676 message
.add_flag('T')
1677 elif isinstance(message
, _mboxMMDFMessage
):
1678 labels
= set(self
.get_labels())
1679 if 'unseen' not in labels
:
1680 message
.add_flag('RO')
1682 message
.add_flag('O')
1683 if 'deleted' in labels
:
1684 message
.add_flag('D')
1685 if 'answered' in labels
:
1686 message
.add_flag('A')
1687 elif isinstance(message
, MHMessage
):
1688 labels
= set(self
.get_labels())
1689 if 'unseen' in labels
:
1690 message
.add_sequence('unseen')
1691 if 'answered' in labels
:
1692 message
.add_sequence('replied')
1693 elif isinstance(message
, BabylMessage
):
1694 message
.set_visible(self
.get_visible())
1695 for label
in self
.get_labels():
1696 message
.add_label(label
)
1697 elif isinstance(message
, Message
):
1700 raise TypeError('Cannot convert to specified type: %s' %
1704 class MMDFMessage(_mboxMMDFMessage
):
1705 """Message with MMDF-specific properties."""
1709 """A read-only wrapper of a file."""
1711 def __init__(self
, f
, pos
=None):
1712 """Initialize a _ProxyFile."""
1715 self
._pos
= f
.tell()
1719 def read(self
, size
=None):
1721 return self
._read
(size
, self
._file
.read
)
1723 def readline(self
, size
=None):
1725 return self
._read
(size
, self
._file
.readline
)
1727 def readlines(self
, sizehint
=None):
1728 """Read multiple lines."""
1732 if sizehint
is not None:
1733 sizehint
-= len(line
)
1739 """Iterate over lines."""
1740 return iter(self
.readline
, "")
1743 """Return the position."""
1746 def seek(self
, offset
, whence
=0):
1747 """Change position."""
1749 self
._file
.seek(self
._pos
)
1750 self
._file
.seek(offset
, whence
)
1751 self
._pos
= self
._file
.tell()
1754 """Close the file."""
1757 def _read(self
, size
, read_method
):
1758 """Read size bytes using read_method."""
1761 self
._file
.seek(self
._pos
)
1762 result
= read_method(size
)
1763 self
._pos
= self
._file
.tell()
1767 class _PartialFile(_ProxyFile
):
1768 """A read-only wrapper of part of a file."""
1770 def __init__(self
, f
, start
=None, stop
=None):
1771 """Initialize a _PartialFile."""
1772 _ProxyFile
.__init
__(self
, f
, start
)
1777 """Return the position with respect to start."""
1778 return _ProxyFile
.tell(self
) - self
._start
1780 def seek(self
, offset
, whence
=0):
1781 """Change position, possibly with respect to start or stop."""
1783 self
._pos
= self
._start
1786 self
._pos
= self
._stop
1788 _ProxyFile
.seek(self
, offset
, whence
)
1790 def _read(self
, size
, read_method
):
1791 """Read size bytes using read_method, honoring start and stop."""
1792 remaining
= self
._stop
- self
._pos
1795 if size
is None or size
< 0 or size
> remaining
:
1797 return _ProxyFile
._read
(self
, size
, read_method
)
1800 def _lock_file(f
, dotlock
=True):
1801 """Lock file f using lockf, flock, and dot locking."""
1802 dotlock_done
= False
1806 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1808 if e
.errno
== errno
.EAGAIN
:
1809 raise ExternalClashError('lockf: lock unavailable: %s' %
1814 fcntl
.flock(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1816 if e
.errno
== errno
.EWOULDBLOCK
:
1817 raise ExternalClashError('flock: lock unavailable: %s' %
1823 pre_lock
= _create_temporary(f
.name
+ '.lock')
1826 if e
.errno
== errno
.EACCES
:
1827 return # Without write access, just skip dotlocking.
1831 if hasattr(os
, 'link'):
1832 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1834 os
.unlink(pre_lock
.name
)
1836 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
1839 if e
.errno
== errno
.EEXIST
:
1840 os
.remove(pre_lock
.name
)
1841 raise ExternalClashError('dot lock unavailable: %s' %
1847 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1848 fcntl
.flock(f
, fcntl
.LOCK_UN
)
1850 os
.remove(f
.name
+ '.lock')
1853 def _unlock_file(f
):
1854 """Unlock file f using lockf, flock, and dot locking."""
1856 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1857 fcntl
.flock(f
, fcntl
.LOCK_UN
)
1858 if os
.path
.exists(f
.name
+ '.lock'):
1859 os
.remove(f
.name
+ '.lock')
1861 def _create_carefully(path
):
1862 """Create a file if it doesn't exist and open for reading and writing."""
1863 fd
= os
.open(path
, os
.O_CREAT | os
.O_EXCL | os
.O_RDWR
)
1865 return open(path
, 'rb+')
1869 def _create_temporary(path
):
1870 """Create a temp file based on path and open for reading and writing."""
1871 return _create_carefully('%s.%s.%s.%s' % (path
, int(time
.time()),
1872 socket
.gethostname(),
1876 ## Start: classes from the original module (for backward compatibility).
1878 # Note that the Maildir class, whose name is unchanged, itself offers a next()
1879 # method for backward compatibility.
1883 def __init__(self
, fp
, factory
=rfc822
.Message
):
1886 self
.factory
= factory
1889 return iter(self
.next
, None)
1893 self
.fp
.seek(self
.seekp
)
1895 self
._search
_start
()
1897 self
.seekp
= self
.fp
.tell()
1899 start
= self
.fp
.tell()
1901 self
.seekp
= stop
= self
.fp
.tell()
1904 return self
.factory(_PartialFile(self
.fp
, start
, stop
))
1906 # Recommended to use PortableUnixMailbox instead!
1907 class UnixMailbox(_Mailbox
):
1909 def _search_start(self
):
1911 pos
= self
.fp
.tell()
1912 line
= self
.fp
.readline()
1915 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1919 def _search_end(self
):
1920 self
.fp
.readline() # Throw away header line
1922 pos
= self
.fp
.tell()
1923 line
= self
.fp
.readline()
1926 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
1930 # An overridable mechanism to test for From-line-ness. You can either
1931 # specify a different regular expression or define a whole new
1932 # _isrealfromline() method. Note that this only gets called for lines
1933 # starting with the 5 characters "From ".
1936 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
1937 # the only portable, reliable way to find message delimiters in a BSD (i.e
1938 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
1939 # beginning of the file, "^From .*\n". While _fromlinepattern below seems
1940 # like a good idea, in practice, there are too many variations for more
1941 # strict parsing of the line to be completely accurate.
1943 # _strict_isrealfromline() is the old version which tries to do stricter
1944 # parsing of the From_ line. _portable_isrealfromline() simply returns
1945 # true, since it's never called if the line doesn't already start with
1948 # This algorithm, and the way it interacts with _search_start() and
1949 # _search_end() may not be completely correct, because it doesn't check
1950 # that the two characters preceding "From " are \n\n or the beginning of
1951 # the file. Fixing this would require a more extensive rewrite than is
1952 # necessary. For convenience, we've added a PortableUnixMailbox class
1953 # which uses the more lenient _fromlinepattern regular expression.
1955 _fromlinepattern
= r
"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+" \
1956 r
"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*$"
1959 def _strict_isrealfromline(self
, line
):
1960 if not self
._regexp
:
1962 self
._regexp
= re
.compile(self
._fromlinepattern
)
1963 return self
._regexp
.match(line
)
1965 def _portable_isrealfromline(self
, line
):
1968 _isrealfromline
= _strict_isrealfromline
1971 class PortableUnixMailbox(UnixMailbox
):
1972 _isrealfromline
= UnixMailbox
._portable
_isrealfromline
1975 class MmdfMailbox(_Mailbox
):
1977 def _search_start(self
):
1979 line
= self
.fp
.readline()
1982 if line
[:5] == '\001\001\001\001\n':
1985 def _search_end(self
):
1987 pos
= self
.fp
.tell()
1988 line
= self
.fp
.readline()
1991 if line
== '\001\001\001\001\n':
1998 def __init__(self
, dirname
, factory
=rfc822
.Message
):
2000 pat
= re
.compile('^[1-9][0-9]*$')
2001 self
.dirname
= dirname
2002 # the three following lines could be combined into:
2003 # list = map(long, filter(pat.match, os.listdir(self.dirname)))
2004 list = os
.listdir(self
.dirname
)
2005 list = filter(pat
.match
, list)
2006 list = map(long, list)
2008 # This only works in Python 1.6 or later;
2009 # before that str() added 'L':
2010 self
.boxes
= map(str, list)
2011 self
.boxes
.reverse()
2012 self
.factory
= factory
2015 return iter(self
.next
, None)
2020 fn
= self
.boxes
.pop()
2021 fp
= open(os
.path
.join(self
.dirname
, fn
))
2022 msg
= self
.factory(fp
)
2025 except (AttributeError, TypeError):
2030 class BabylMailbox(_Mailbox
):
2032 def _search_start(self
):
2034 line
= self
.fp
.readline()
2037 if line
== '*** EOOH ***\n':
2040 def _search_end(self
):
2042 pos
= self
.fp
.tell()
2043 line
= self
.fp
.readline()
2046 if line
== '\037\014\n' or line
== '\037':
2050 ## End: classes from the original module (for backward compatibility).
2053 class Error(Exception):
2054 """Raised for module-specific errors."""
2056 class NoSuchMailboxError(Error
):
2057 """The specified mailbox does not exist and won't be created."""
2059 class NotEmptyError(Error
):
2060 """The specified mailbox is not empty and deletion was requested."""
2062 class ExternalClashError(Error
):
2063 """Another process caused an action to fail."""
2065 class FormatError(Error
):
2066 """A file appears to have an invalid format."""