]>
git.proxmox.com Git - mirror_edk2.git/blob - AppPkg/Applications/Python/Python-2.7.2/Lib/mailbox.py
3 """Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes."""
5 # Notes for authors of new mailbox subclasses:
7 # Remember to fsync() changes to disk before closing a modified file
8 # or returning from a flush() method. See functions _sync_flush() and
20 import email
.generator
23 if sys
.platform
== 'os2emx':
24 # OS/2 EMX fcntl() not adequate
31 with warnings
.catch_warnings():
33 warnings
.filterwarnings("ignore", ".*rfc822 has been removed",
37 __all__
= [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
38 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
39 'BabylMessage', 'MMDFMessage', 'UnixMailbox',
40 'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
43 """A group of messages in a particular place."""
45 def __init__(self
, path
, factory
=None, create
=True):
46 """Initialize a Mailbox instance."""
47 self
._path
= os
.path
.abspath(os
.path
.expanduser(path
))
48 self
._factory
= factory
50 def add(self
, message
):
51 """Add message and return assigned key."""
52 raise NotImplementedError('Method must be implemented by subclass')
54 def remove(self
, key
):
55 """Remove the keyed message; raise KeyError if it doesn't exist."""
56 raise NotImplementedError('Method must be implemented by subclass')
58 def __delitem__(self
, key
):
61 def discard(self
, key
):
62 """If the keyed message exists, remove it."""
68 def __setitem__(self
, key
, message
):
69 """Replace the keyed message; raise KeyError if it doesn't exist."""
70 raise NotImplementedError('Method must be implemented by subclass')
72 def get(self
, key
, default
=None):
73 """Return the keyed message, or default if it doesn't exist."""
75 return self
.__getitem
__(key
)
79 def __getitem__(self
, key
):
80 """Return the keyed message; raise KeyError if it doesn't exist."""
82 return self
.get_message(key
)
84 return self
._factory
(self
.get_file(key
))
86 def get_message(self
, key
):
87 """Return a Message representation or raise a KeyError."""
88 raise NotImplementedError('Method must be implemented by subclass')
90 def get_string(self
, key
):
91 """Return a string representation or raise a KeyError."""
92 raise NotImplementedError('Method must be implemented by subclass')
94 def get_file(self
, key
):
95 """Return a file-like representation or raise a KeyError."""
96 raise NotImplementedError('Method must be implemented by subclass')
99 """Return an iterator over keys."""
100 raise NotImplementedError('Method must be implemented by subclass')
103 """Return a list of keys."""
104 return list(self
.iterkeys())
106 def itervalues(self
):
107 """Return an iterator over all messages."""
108 for key
in self
.iterkeys():
116 return self
.itervalues()
119 """Return a list of messages. Memory intensive."""
120 return list(self
.itervalues())
123 """Return an iterator over (key, message) tuples."""
124 for key
in self
.iterkeys():
132 """Return a list of (key, message) tuples. Memory intensive."""
133 return list(self
.iteritems())
135 def has_key(self
, key
):
136 """Return True if the keyed message exists, False otherwise."""
137 raise NotImplementedError('Method must be implemented by subclass')
139 def __contains__(self
, key
):
140 return self
.has_key(key
)
143 """Return a count of messages in the mailbox."""
144 raise NotImplementedError('Method must be implemented by subclass')
147 """Delete all messages."""
148 for key
in self
.iterkeys():
151 def pop(self
, key
, default
=None):
152 """Delete the keyed message and return it, or default."""
161 """Delete an arbitrary (key, message) pair and return it."""
162 for key
in self
.iterkeys():
163 return (key
, self
.pop(key
)) # This is only run once.
165 raise KeyError('No messages in mailbox')
167 def update(self
, arg
=None):
168 """Change the messages that correspond to certain keys."""
169 if hasattr(arg
, 'iteritems'):
170 source
= arg
.iteritems()
171 elif hasattr(arg
, 'items'):
176 for key
, message
in source
:
182 raise KeyError('No message with key(s)')
185 """Write any pending changes to the disk."""
186 raise NotImplementedError('Method must be implemented by subclass')
189 """Lock the mailbox."""
190 raise NotImplementedError('Method must be implemented by subclass')
193 """Unlock the mailbox if it is locked."""
194 raise NotImplementedError('Method must be implemented by subclass')
197 """Flush and close the mailbox."""
198 raise NotImplementedError('Method must be implemented by subclass')
200 def _dump_message(self
, message
, target
, mangle_from_
=False):
201 # Most files are opened in binary mode to allow predictable seeking.
202 # To get native line endings on disk, the user-friendly \n line endings
203 # used in strings and by email.Message are translated here.
204 """Dump message contents to target file."""
205 if isinstance(message
, email
.message
.Message
):
206 buffer = StringIO
.StringIO()
207 gen
= email
.generator
.Generator(buffer, mangle_from_
, 0)
210 target
.write(buffer.read().replace('\n', os
.linesep
))
211 elif isinstance(message
, str):
213 message
= message
.replace('\nFrom ', '\n>From ')
214 message
= message
.replace('\n', os
.linesep
)
215 target
.write(message
)
216 elif hasattr(message
, 'read'):
218 line
= message
.readline()
221 if mangle_from_
and line
.startswith('From '):
222 line
= '>From ' + line
[5:]
223 line
= line
.replace('\n', os
.linesep
)
226 raise TypeError('Invalid message type: %s' % type(message
))
229 class Maildir(Mailbox
):
230 """A qmail-style Maildir mailbox."""
234 def __init__(self
, dirname
, factory
=rfc822
.Message
, create
=True):
235 """Initialize a Maildir instance."""
236 Mailbox
.__init
__(self
, dirname
, factory
, create
)
238 'tmp': os
.path
.join(self
._path
, 'tmp'),
239 'new': os
.path
.join(self
._path
, 'new'),
240 'cur': os
.path
.join(self
._path
, 'cur'),
242 if not os
.path
.exists(self
._path
):
244 os
.mkdir(self
._path
, 0700)
245 for path
in self
._paths
.values():
246 os
.mkdir(path
, 0o700)
248 raise NoSuchMailboxError(self
._path
)
250 self
._toc
_mtimes
= {}
251 for subdir
in ('cur', 'new'):
252 self
._toc
_mtimes
[subdir
] = os
.path
.getmtime(self
._paths
[subdir
])
253 self
._last
_read
= time
.time() # Records last time we read cur/new
254 self
._skewfactor
= 0.1 # Adjust if os/fs clocks are skewing
256 def add(self
, message
):
257 """Add message and return assigned key."""
258 tmp_file
= self
._create
_tmp
()
260 self
._dump
_message
(message
, tmp_file
)
261 except BaseException
:
263 os
.remove(tmp_file
.name
)
265 _sync_close(tmp_file
)
266 if isinstance(message
, MaildirMessage
):
267 subdir
= message
.get_subdir()
268 suffix
= self
.colon
+ message
.get_info()
269 if suffix
== self
.colon
:
274 uniq
= os
.path
.basename(tmp_file
.name
).split(self
.colon
)[0]
275 dest
= os
.path
.join(self
._path
, subdir
, uniq
+ suffix
)
277 if hasattr(os
, 'link'):
278 os
.link(tmp_file
.name
, dest
)
279 os
.remove(tmp_file
.name
)
281 os
.rename(tmp_file
.name
, dest
)
283 os
.remove(tmp_file
.name
)
284 if e
.errno
== errno
.EEXIST
:
285 raise ExternalClashError('Name clash with existing message: %s'
289 if isinstance(message
, MaildirMessage
):
290 os
.utime(dest
, (os
.path
.getatime(dest
), message
.get_date()))
293 def remove(self
, key
):
294 """Remove the keyed message; raise KeyError if it doesn't exist."""
295 os
.remove(os
.path
.join(self
._path
, self
._lookup
(key
)))
297 def discard(self
, key
):
298 """If the keyed message exists, remove it."""
299 # This overrides an inapplicable implementation in the superclass.
305 if e
.errno
!= errno
.ENOENT
:
308 def __setitem__(self
, key
, message
):
309 """Replace the keyed message; raise KeyError if it doesn't exist."""
310 old_subpath
= self
._lookup
(key
)
311 temp_key
= self
.add(message
)
312 temp_subpath
= self
._lookup
(temp_key
)
313 if isinstance(message
, MaildirMessage
):
314 # temp's subdir and suffix were specified by message.
315 dominant_subpath
= temp_subpath
317 # temp's subdir and suffix were defaults from add().
318 dominant_subpath
= old_subpath
319 subdir
= os
.path
.dirname(dominant_subpath
)
320 if self
.colon
in dominant_subpath
:
321 suffix
= self
.colon
+ dominant_subpath
.split(self
.colon
)[-1]
325 new_path
= os
.path
.join(self
._path
, subdir
, key
+ suffix
)
326 os
.rename(os
.path
.join(self
._path
, temp_subpath
), new_path
)
327 if isinstance(message
, MaildirMessage
):
328 os
.utime(new_path
, (os
.path
.getatime(new_path
),
331 def get_message(self
, key
):
332 """Return a Message representation or raise a KeyError."""
333 subpath
= self
._lookup
(key
)
334 f
= open(os
.path
.join(self
._path
, subpath
), 'r')
337 msg
= self
._factory
(f
)
339 msg
= MaildirMessage(f
)
342 subdir
, name
= os
.path
.split(subpath
)
343 msg
.set_subdir(subdir
)
344 if self
.colon
in name
:
345 msg
.set_info(name
.split(self
.colon
)[-1])
346 msg
.set_date(os
.path
.getmtime(os
.path
.join(self
._path
, subpath
)))
349 def get_string(self
, key
):
350 """Return a string representation or raise a KeyError."""
351 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'r')
357 def get_file(self
, key
):
358 """Return a file-like representation or raise a KeyError."""
359 f
= open(os
.path
.join(self
._path
, self
._lookup
(key
)), 'rb')
363 """Return an iterator over keys."""
365 for key
in self
._toc
:
372 def has_key(self
, key
):
373 """Return True if the keyed message exists, False otherwise."""
375 return key
in self
._toc
378 """Return a count of messages in the mailbox."""
380 return len(self
._toc
)
383 """Write any pending changes to disk."""
384 # Maildir changes are always written immediately, so there's nothing
389 """Lock the mailbox."""
393 """Unlock the mailbox if it is locked."""
397 """Flush and close the mailbox."""
400 def list_folders(self
):
401 """Return a list of folder names."""
403 for entry
in os
.listdir(self
._path
):
404 if len(entry
) > 1 and entry
[0] == '.' and \
405 os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
406 result
.append(entry
[1:])
409 def get_folder(self
, folder
):
410 """Return a Maildir instance for the named folder."""
411 return Maildir(os
.path
.join(self
._path
, '.' + folder
),
412 factory
=self
._factory
,
415 def add_folder(self
, folder
):
416 """Create a folder and return a Maildir instance representing it."""
417 path
= os
.path
.join(self
._path
, '.' + folder
)
418 result
= Maildir(path
, factory
=self
._factory
)
419 maildirfolder_path
= os
.path
.join(path
, 'maildirfolder')
420 if not os
.path
.exists(maildirfolder_path
):
421 os
.close(os
.open(maildirfolder_path
, os
.O_CREAT | os
.O_WRONLY
,
425 def remove_folder(self
, folder
):
426 """Delete the named folder, which must be empty."""
427 path
= os
.path
.join(self
._path
, '.' + folder
)
428 for entry
in os
.listdir(os
.path
.join(path
, 'new')) + \
429 os
.listdir(os
.path
.join(path
, 'cur')):
430 if len(entry
) < 1 or entry
[0] != '.':
431 raise NotEmptyError('Folder contains message(s): %s' % folder
)
432 for entry
in os
.listdir(path
):
433 if entry
!= 'new' and entry
!= 'cur' and entry
!= 'tmp' and \
434 os
.path
.isdir(os
.path
.join(path
, entry
)):
435 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
437 for root
, dirs
, files
in os
.walk(path
, topdown
=False):
439 os
.remove(os
.path
.join(root
, entry
))
441 os
.rmdir(os
.path
.join(root
, entry
))
445 """Delete old files in "tmp"."""
447 for entry
in os
.listdir(os
.path
.join(self
._path
, 'tmp')):
448 path
= os
.path
.join(self
._path
, 'tmp', entry
)
449 if now
- os
.path
.getatime(path
) > 129600: # 60 * 60 * 36
452 _count
= 1 # This is used to generate unique file names.
454 def _create_tmp(self
):
455 """Create a file in the tmp subdirectory and open and return it."""
457 hostname
= socket
.gethostname()
459 hostname
= hostname
.replace('/', r
'\057')
461 hostname
= hostname
.replace(':', r
'\072')
462 uniq
= "%s.M%sP%sQ%s.%s" % (int(now
), int(now
% 1 * 1e6
), os
.getpid(),
463 Maildir
._count
, hostname
)
464 path
= os
.path
.join(self
._path
, 'tmp', uniq
)
468 if e
.errno
== errno
.ENOENT
:
471 return _create_carefully(path
)
473 if e
.errno
!= errno
.EEXIST
:
478 # Fall through to here if stat succeeded or open raised EEXIST.
479 raise ExternalClashError('Name clash prevented file creation: %s' %
483 """Update table of contents mapping."""
484 # If it has been less than two seconds since the last _refresh() call,
485 # we have to unconditionally re-read the mailbox just in case it has
486 # been modified, because os.path.mtime() has a 2 sec resolution in the
487 # most common worst case (FAT) and a 1 sec resolution typically. This
488 # results in a few unnecessary re-reads when _refresh() is called
489 # multiple times in that interval, but once the clock ticks over, we
490 # will only re-read as needed. Because the filesystem might be being
491 # served by an independent system with its own clock, we record and
492 # compare with the mtimes from the filesystem. Because the other
493 # system's clock might be skewing relative to our clock, we add an
494 # extra delta to our wait. The default is one tenth second, but is an
495 # instance variable and so can be adjusted if dealing with a
496 # particularly skewed or irregular system.
497 if time
.time() - self
._last
_read
> 2 + self
._skewfactor
:
499 for subdir
in self
._toc
_mtimes
:
500 mtime
= os
.path
.getmtime(self
._paths
[subdir
])
501 if mtime
> self
._toc
_mtimes
[subdir
]:
503 self
._toc
_mtimes
[subdir
] = mtime
508 for subdir
in self
._toc
_mtimes
:
509 path
= self
._paths
[subdir
]
510 for entry
in os
.listdir(path
):
511 p
= os
.path
.join(path
, entry
)
514 uniq
= entry
.split(self
.colon
)[0]
515 self
._toc
[uniq
] = os
.path
.join(subdir
, entry
)
516 self
._last
_read
= time
.time()
518 def _lookup(self
, key
):
519 """Use TOC to return subpath for given key, or raise a KeyError."""
521 if os
.path
.exists(os
.path
.join(self
._path
, self
._toc
[key
])):
522 return self
._toc
[key
]
527 return self
._toc
[key
]
529 raise KeyError('No message with key: %s' % key
)
531 # This method is for backward compatibility only.
533 """Return the next message in a one-time iteration."""
534 if not hasattr(self
, '_onetime_keys'):
535 self
._onetime
_keys
= self
.iterkeys()
538 return self
[self
._onetime
_keys
.next()]
539 except StopIteration:
545 class _singlefileMailbox(Mailbox
):
546 """A single-file mailbox."""
548 def __init__(self
, path
, factory
=None, create
=True):
549 """Initialize a single-file mailbox."""
550 Mailbox
.__init
__(self
, path
, factory
, create
)
552 f
= open(self
._path
, 'rb+')
554 if e
.errno
== errno
.ENOENT
:
556 f
= open(self
._path
, 'wb+')
558 raise NoSuchMailboxError(self
._path
)
559 elif e
.errno
in (errno
.EACCES
, errno
.EROFS
):
560 f
= open(self
._path
, 'rb')
566 self
._pending
= False # No changes require rewriting the file.
568 self
._file
_length
= None # Used to record mailbox size
570 def add(self
, message
):
571 """Add message and return assigned key."""
573 self
._toc
[self
._next
_key
] = self
._append
_message
(message
)
576 return self
._next
_key
- 1
578 def remove(self
, key
):
579 """Remove the keyed message; raise KeyError if it doesn't exist."""
584 def __setitem__(self
, key
, message
):
585 """Replace the keyed message; raise KeyError if it doesn't exist."""
587 self
._toc
[key
] = self
._append
_message
(message
)
591 """Return an iterator over keys."""
593 for key
in self
._toc
.keys():
596 def has_key(self
, key
):
597 """Return True if the keyed message exists, False otherwise."""
599 return key
in self
._toc
602 """Return a count of messages in the mailbox."""
604 return len(self
._toc
)
607 """Lock the mailbox."""
609 _lock_file(self
._file
)
613 """Unlock the mailbox if it is locked."""
615 _unlock_file(self
._file
)
619 """Write any pending changes to disk."""
620 if not self
._pending
:
623 # In order to be writing anything out at all, self._toc must
624 # already have been generated (and presumably has been modified
625 # by adding or deleting an item).
626 assert self
._toc
is not None
628 # Check length of self._file; if it's changed, some other process
629 # has modified the mailbox since we scanned it.
630 self
._file
.seek(0, 2)
631 cur_len
= self
._file
.tell()
632 if cur_len
!= self
._file
_length
:
633 raise ExternalClashError('Size of mailbox file changed '
634 '(expected %i, found %i)' %
635 (self
._file
_length
, cur_len
))
637 new_file
= _create_temporary(self
._path
)
640 self
._pre
_mailbox
_hook
(new_file
)
641 for key
in sorted(self
._toc
.keys()):
642 start
, stop
= self
._toc
[key
]
643 self
._file
.seek(start
)
644 self
._pre
_message
_hook
(new_file
)
645 new_start
= new_file
.tell()
647 buffer = self
._file
.read(min(4096,
648 stop
- self
._file
.tell()))
651 new_file
.write(buffer)
652 new_toc
[key
] = (new_start
, new_file
.tell())
653 self
._post
_message
_hook
(new_file
)
656 os
.remove(new_file
.name
)
658 _sync_close(new_file
)
659 # self._file is about to get replaced, so no need to sync.
662 os
.rename(new_file
.name
, self
._path
)
664 if e
.errno
== errno
.EEXIST
or \
665 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
666 os
.remove(self
._path
)
667 os
.rename(new_file
.name
, self
._path
)
670 self
._file
= open(self
._path
, 'rb+')
672 self
._pending
= False
674 _lock_file(self
._file
, dotlock
=False)
676 def _pre_mailbox_hook(self
, f
):
677 """Called before writing the mailbox to file f."""
680 def _pre_message_hook(self
, f
):
681 """Called before writing each message to file f."""
684 def _post_message_hook(self
, f
):
685 """Called after writing each message to file f."""
689 """Flush and close the mailbox."""
693 self
._file
.close() # Sync has been done by self.flush() above.
695 def _lookup(self
, key
=None):
696 """Return (start, stop) or raise KeyError."""
697 if self
._toc
is None:
701 return self
._toc
[key
]
703 raise KeyError('No message with key: %s' % key
)
705 def _append_message(self
, message
):
706 """Append message to mailbox and return (start, stop) offsets."""
707 self
._file
.seek(0, 2)
708 before
= self
._file
.tell()
710 self
._pre
_message
_hook
(self
._file
)
711 offsets
= self
._install
_message
(message
)
712 self
._post
_message
_hook
(self
._file
)
713 except BaseException
:
714 self
._file
.truncate(before
)
717 self
._file
_length
= self
._file
.tell() # Record current length of mailbox
722 class _mboxMMDF(_singlefileMailbox
):
723 """An mbox or MMDF mailbox."""
727 def get_message(self
, key
):
728 """Return a Message representation or raise a KeyError."""
729 start
, stop
= self
._lookup
(key
)
730 self
._file
.seek(start
)
731 from_line
= self
._file
.readline().replace(os
.linesep
, '')
732 string
= self
._file
.read(stop
- self
._file
.tell())
733 msg
= self
._message
_factory
(string
.replace(os
.linesep
, '\n'))
734 msg
.set_from(from_line
[5:])
737 def get_string(self
, key
, from_
=False):
738 """Return a string representation or raise a KeyError."""
739 start
, stop
= self
._lookup
(key
)
740 self
._file
.seek(start
)
742 self
._file
.readline()
743 string
= self
._file
.read(stop
- self
._file
.tell())
744 return string
.replace(os
.linesep
, '\n')
746 def get_file(self
, key
, from_
=False):
747 """Return a file-like representation or raise a KeyError."""
748 start
, stop
= self
._lookup
(key
)
749 self
._file
.seek(start
)
751 self
._file
.readline()
752 return _PartialFile(self
._file
, self
._file
.tell(), stop
)
754 def _install_message(self
, message
):
755 """Format a message and blindly write to self._file."""
757 if isinstance(message
, str) and message
.startswith('From '):
758 newline
= message
.find('\n')
760 from_line
= message
[:newline
]
761 message
= message
[newline
+ 1:]
765 elif isinstance(message
, _mboxMMDFMessage
):
766 from_line
= 'From ' + message
.get_from()
767 elif isinstance(message
, email
.message
.Message
):
768 from_line
= message
.get_unixfrom() # May be None.
769 if from_line
is None:
770 from_line
= 'From MAILER-DAEMON %s' % time
.asctime(time
.gmtime())
771 start
= self
._file
.tell()
772 self
._file
.write(from_line
+ os
.linesep
)
773 self
._dump
_message
(message
, self
._file
, self
._mangle
_from
_)
774 stop
= self
._file
.tell()
778 class mbox(_mboxMMDF
):
779 """A classic mbox mailbox."""
783 def __init__(self
, path
, factory
=None, create
=True):
784 """Initialize an mbox mailbox."""
785 self
._message
_factory
= mboxMessage
786 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
788 def _pre_message_hook(self
, f
):
789 """Called before writing each message to file f."""
793 def _generate_toc(self
):
794 """Generate key-to-(start, stop) table of contents."""
795 starts
, stops
= [], []
798 line_pos
= self
._file
.tell()
799 line
= self
._file
.readline()
800 if line
.startswith('From '):
801 if len(stops
) < len(starts
):
802 stops
.append(line_pos
- len(os
.linesep
))
803 starts
.append(line_pos
)
805 stops
.append(line_pos
)
807 self
._toc
= dict(enumerate(zip(starts
, stops
)))
808 self
._next
_key
= len(self
._toc
)
809 self
._file
_length
= self
._file
.tell()
812 class MMDF(_mboxMMDF
):
813 """An MMDF mailbox."""
815 def __init__(self
, path
, factory
=None, create
=True):
816 """Initialize an MMDF mailbox."""
817 self
._message
_factory
= MMDFMessage
818 _mboxMMDF
.__init
__(self
, path
, factory
, create
)
820 def _pre_message_hook(self
, f
):
821 """Called before writing each message to file f."""
822 f
.write('\001\001\001\001' + os
.linesep
)
824 def _post_message_hook(self
, f
):
825 """Called after writing each message to file f."""
826 f
.write(os
.linesep
+ '\001\001\001\001' + os
.linesep
)
828 def _generate_toc(self
):
829 """Generate key-to-(start, stop) table of contents."""
830 starts
, stops
= [], []
835 line
= self
._file
.readline()
836 next_pos
= self
._file
.tell()
837 if line
.startswith('\001\001\001\001' + os
.linesep
):
838 starts
.append(next_pos
)
841 line
= self
._file
.readline()
842 next_pos
= self
._file
.tell()
843 if line
== '\001\001\001\001' + os
.linesep
:
844 stops
.append(line_pos
- len(os
.linesep
))
847 stops
.append(line_pos
)
851 self
._toc
= dict(enumerate(zip(starts
, stops
)))
852 self
._next
_key
= len(self
._toc
)
853 self
._file
.seek(0, 2)
854 self
._file
_length
= self
._file
.tell()
860 def __init__(self
, path
, factory
=None, create
=True):
861 """Initialize an MH instance."""
862 Mailbox
.__init
__(self
, path
, factory
, create
)
863 if not os
.path
.exists(self
._path
):
865 os
.mkdir(self
._path
, 0700)
866 os
.close(os
.open(os
.path
.join(self
._path
, '.mh_sequences'),
867 os
.O_CREAT | os
.O_EXCL | os
.O_WRONLY
, 0600))
869 raise NoSuchMailboxError(self
._path
)
872 def add(self
, message
):
873 """Add message and return assigned key."""
878 new_key
= max(keys
) + 1
879 new_path
= os
.path
.join(self
._path
, str(new_key
))
880 f
= _create_carefully(new_path
)
887 self
._dump
_message
(message
, f
)
888 except BaseException
:
889 # Unlock and close so it can be deleted on Windows
896 if isinstance(message
, MHMessage
):
897 self
._dump
_sequences
(message
, new_key
)
906 def remove(self
, key
):
907 """Remove the keyed message; raise KeyError if it doesn't exist."""
908 path
= os
.path
.join(self
._path
, str(key
))
910 f
= open(path
, 'rb+')
912 if e
.errno
== errno
.ENOENT
:
913 raise KeyError('No message with key: %s' % key
)
920 def __setitem__(self
, key
, message
):
921 """Replace the keyed message; raise KeyError if it doesn't exist."""
922 path
= os
.path
.join(self
._path
, str(key
))
924 f
= open(path
, 'rb+')
926 if e
.errno
== errno
.ENOENT
:
927 raise KeyError('No message with key: %s' % key
)
934 os
.close(os
.open(path
, os
.O_WRONLY | os
.O_TRUNC
))
935 self
._dump
_message
(message
, f
)
936 if isinstance(message
, MHMessage
):
937 self
._dump
_sequences
(message
, key
)
944 def get_message(self
, key
):
945 """Return a Message representation or raise a KeyError."""
948 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
950 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
952 if e
.errno
== errno
.ENOENT
:
953 raise KeyError('No message with key: %s' % key
)
966 for name
, key_list
in self
.get_sequences().iteritems():
968 msg
.add_sequence(name
)
971 def get_string(self
, key
):
972 """Return a string representation or raise a KeyError."""
975 f
= open(os
.path
.join(self
._path
, str(key
)), 'r+')
977 f
= open(os
.path
.join(self
._path
, str(key
)), 'r')
979 if e
.errno
== errno
.ENOENT
:
980 raise KeyError('No message with key: %s' % key
)
994 def get_file(self
, key
):
995 """Return a file-like representation or raise a KeyError."""
997 f
= open(os
.path
.join(self
._path
, str(key
)), 'rb')
999 if e
.errno
== errno
.ENOENT
:
1000 raise KeyError('No message with key: %s' % key
)
1003 return _ProxyFile(f
)
1006 """Return an iterator over keys."""
1007 return iter(sorted(int(entry
) for entry
in os
.listdir(self
._path
)
1008 if entry
.isdigit()))
1010 def has_key(self
, key
):
1011 """Return True if the keyed message exists, False otherwise."""
1012 return os
.path
.exists(os
.path
.join(self
._path
, str(key
)))
1015 """Return a count of messages in the mailbox."""
1016 return len(list(self
.iterkeys()))
1019 """Lock the mailbox."""
1020 if not self
._locked
:
1021 self
._file
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'rb+')
1022 _lock_file(self
._file
)
1026 """Unlock the mailbox if it is locked."""
1028 _unlock_file(self
._file
)
1029 _sync_close(self
._file
)
1031 self
._locked
= False
1034 """Write any pending changes to the disk."""
1038 """Flush and close the mailbox."""
1042 def list_folders(self
):
1043 """Return a list of folder names."""
1045 for entry
in os
.listdir(self
._path
):
1046 if os
.path
.isdir(os
.path
.join(self
._path
, entry
)):
1047 result
.append(entry
)
1050 def get_folder(self
, folder
):
1051 """Return an MH instance for the named folder."""
1052 return MH(os
.path
.join(self
._path
, folder
),
1053 factory
=self
._factory
, create
=False)
1055 def add_folder(self
, folder
):
1056 """Create a folder and return an MH instance representing it."""
1057 return MH(os
.path
.join(self
._path
, folder
),
1058 factory
=self
._factory
)
1060 def remove_folder(self
, folder
):
1061 """Delete the named folder, which must be empty."""
1062 path
= os
.path
.join(self
._path
, folder
)
1063 entries
= os
.listdir(path
)
1064 if entries
== ['.mh_sequences']:
1065 os
.remove(os
.path
.join(path
, '.mh_sequences'))
1069 raise NotEmptyError('Folder not empty: %s' % self
._path
)
1072 def get_sequences(self
):
1073 """Return a name-to-key-list dictionary to define each sequence."""
1075 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r')
1077 all_keys
= set(self
.keys())
1080 name
, contents
= line
.split(':')
1082 for spec
in contents
.split():
1086 start
, stop
= (int(x
) for x
in spec
.split('-'))
1087 keys
.update(range(start
, stop
+ 1))
1088 results
[name
] = [key
for key
in sorted(keys
) \
1090 if len(results
[name
]) == 0:
1093 raise FormatError('Invalid sequence specification: %s' %
1099 def set_sequences(self
, sequences
):
1100 """Set sequences using the given name-to-key-list dictionary."""
1101 f
= open(os
.path
.join(self
._path
, '.mh_sequences'), 'r+')
1103 os
.close(os
.open(f
.name
, os
.O_WRONLY | os
.O_TRUNC
))
1104 for name
, keys
in sequences
.iteritems():
1107 f
.write('%s:' % name
)
1110 for key
in sorted(set(keys
)):
1117 f
.write('%s %s' % (prev
, key
))
1119 f
.write(' %s' % key
)
1122 f
.write(str(prev
) + '\n')
1129 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1130 sequences
= self
.get_sequences()
1133 for key
in self
.iterkeys():
1135 changes
.append((key
, prev
+ 1))
1136 if hasattr(os
, 'link'):
1137 os
.link(os
.path
.join(self
._path
, str(key
)),
1138 os
.path
.join(self
._path
, str(prev
+ 1)))
1139 os
.unlink(os
.path
.join(self
._path
, str(key
)))
1141 os
.rename(os
.path
.join(self
._path
, str(key
)),
1142 os
.path
.join(self
._path
, str(prev
+ 1)))
1144 self
._next
_key
= prev
+ 1
1145 if len(changes
) == 0:
1147 for name
, key_list
in sequences
.items():
1148 for old
, new
in changes
:
1150 key_list
[key_list
.index(old
)] = new
1151 self
.set_sequences(sequences
)
1153 def _dump_sequences(self
, message
, key
):
1154 """Inspect a new MHMessage and update sequences appropriately."""
1155 pending_sequences
= message
.get_sequences()
1156 all_sequences
= self
.get_sequences()
1157 for name
, key_list
in all_sequences
.iteritems():
1158 if name
in pending_sequences
:
1159 key_list
.append(key
)
1160 elif key
in key_list
:
1161 del key_list
[key_list
.index(key
)]
1162 for sequence
in pending_sequences
:
1163 if sequence
not in all_sequences
:
1164 all_sequences
[sequence
] = [key
]
1165 self
.set_sequences(all_sequences
)
1168 class Babyl(_singlefileMailbox
):
1169 """An Rmail-style Babyl mailbox."""
1171 _special_labels
= frozenset(('unseen', 'deleted', 'filed', 'answered',
1172 'forwarded', 'edited', 'resent'))
1174 def __init__(self
, path
, factory
=None, create
=True):
1175 """Initialize a Babyl mailbox."""
1176 _singlefileMailbox
.__init
__(self
, path
, factory
, create
)
1179 def add(self
, message
):
1180 """Add message and return assigned key."""
1181 key
= _singlefileMailbox
.add(self
, message
)
1182 if isinstance(message
, BabylMessage
):
1183 self
._labels
[key
] = message
.get_labels()
1186 def remove(self
, key
):
1187 """Remove the keyed message; raise KeyError if it doesn't exist."""
1188 _singlefileMailbox
.remove(self
, key
)
1189 if key
in self
._labels
:
1190 del self
._labels
[key
]
1192 def __setitem__(self
, key
, message
):
1193 """Replace the keyed message; raise KeyError if it doesn't exist."""
1194 _singlefileMailbox
.__setitem
__(self
, key
, message
)
1195 if isinstance(message
, BabylMessage
):
1196 self
._labels
[key
] = message
.get_labels()
1198 def get_message(self
, key
):
1199 """Return a Message representation or raise a KeyError."""
1200 start
, stop
= self
._lookup
(key
)
1201 self
._file
.seek(start
)
1202 self
._file
.readline() # Skip '1,' line specifying labels.
1203 original_headers
= StringIO
.StringIO()
1205 line
= self
._file
.readline()
1206 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1208 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1209 visible_headers
= StringIO
.StringIO()
1211 line
= self
._file
.readline()
1212 if line
== os
.linesep
or line
== '':
1214 visible_headers
.write(line
.replace(os
.linesep
, '\n'))
1215 body
= self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1217 msg
= BabylMessage(original_headers
.getvalue() + body
)
1218 msg
.set_visible(visible_headers
.getvalue())
1219 if key
in self
._labels
:
1220 msg
.set_labels(self
._labels
[key
])
1223 def get_string(self
, key
):
1224 """Return a string representation or raise a KeyError."""
1225 start
, stop
= self
._lookup
(key
)
1226 self
._file
.seek(start
)
1227 self
._file
.readline() # Skip '1,' line specifying labels.
1228 original_headers
= StringIO
.StringIO()
1230 line
= self
._file
.readline()
1231 if line
== '*** EOOH ***' + os
.linesep
or line
== '':
1233 original_headers
.write(line
.replace(os
.linesep
, '\n'))
1235 line
= self
._file
.readline()
1236 if line
== os
.linesep
or line
== '':
1238 return original_headers
.getvalue() + \
1239 self
._file
.read(stop
- self
._file
.tell()).replace(os
.linesep
,
1242 def get_file(self
, key
):
1243 """Return a file-like representation or raise a KeyError."""
1244 return StringIO
.StringIO(self
.get_string(key
).replace('\n',
1247 def get_labels(self
):
1248 """Return a list of user-defined labels in the mailbox."""
1251 for label_list
in self
._labels
.values():
1252 labels
.update(label_list
)
1253 labels
.difference_update(self
._special
_labels
)
1256 def _generate_toc(self
):
1257 """Generate key-to-(start, stop) table of contents."""
1258 starts
, stops
= [], []
1264 line
= self
._file
.readline()
1265 next_pos
= self
._file
.tell()
1266 if line
== '\037\014' + os
.linesep
:
1267 if len(stops
) < len(starts
):
1268 stops
.append(line_pos
- len(os
.linesep
))
1269 starts
.append(next_pos
)
1270 labels
= [label
.strip() for label
1271 in self
._file
.readline()[1:].split(',')
1272 if label
.strip() != '']
1273 label_lists
.append(labels
)
1274 elif line
== '\037' or line
== '\037' + os
.linesep
:
1275 if len(stops
) < len(starts
):
1276 stops
.append(line_pos
- len(os
.linesep
))
1278 stops
.append(line_pos
- len(os
.linesep
))
1280 self
._toc
= dict(enumerate(zip(starts
, stops
)))
1281 self
._labels
= dict(enumerate(label_lists
))
1282 self
._next
_key
= len(self
._toc
)
1283 self
._file
.seek(0, 2)
1284 self
._file
_length
= self
._file
.tell()
1286 def _pre_mailbox_hook(self
, f
):
1287 """Called before writing the mailbox to file f."""
1288 f
.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1289 (os
.linesep
, os
.linesep
, ','.join(self
.get_labels()),
1292 def _pre_message_hook(self
, f
):
1293 """Called before writing each message to file f."""
1294 f
.write('\014' + os
.linesep
)
1296 def _post_message_hook(self
, f
):
1297 """Called after writing each message to file f."""
1298 f
.write(os
.linesep
+ '\037')
1300 def _install_message(self
, message
):
1301 """Write message contents and return (start, stop)."""
1302 start
= self
._file
.tell()
1303 if isinstance(message
, BabylMessage
):
1306 for label
in message
.get_labels():
1307 if label
in self
._special
_labels
:
1308 special_labels
.append(label
)
1310 labels
.append(label
)
1311 self
._file
.write('1')
1312 for label
in special_labels
:
1313 self
._file
.write(', ' + label
)
1314 self
._file
.write(',,')
1315 for label
in labels
:
1316 self
._file
.write(' ' + label
+ ',')
1317 self
._file
.write(os
.linesep
)
1319 self
._file
.write('1,,' + os
.linesep
)
1320 if isinstance(message
, email
.message
.Message
):
1321 orig_buffer
= StringIO
.StringIO()
1322 orig_generator
= email
.generator
.Generator(orig_buffer
, False, 0)
1323 orig_generator
.flatten(message
)
1326 line
= orig_buffer
.readline()
1327 self
._file
.write(line
.replace('\n', os
.linesep
))
1328 if line
== '\n' or line
== '':
1330 self
._file
.write('*** EOOH ***' + os
.linesep
)
1331 if isinstance(message
, BabylMessage
):
1332 vis_buffer
= StringIO
.StringIO()
1333 vis_generator
= email
.generator
.Generator(vis_buffer
, False, 0)
1334 vis_generator
.flatten(message
.get_visible())
1336 line
= vis_buffer
.readline()
1337 self
._file
.write(line
.replace('\n', os
.linesep
))
1338 if line
== '\n' or line
== '':
1343 line
= orig_buffer
.readline()
1344 self
._file
.write(line
.replace('\n', os
.linesep
))
1345 if line
== '\n' or line
== '':
1348 buffer = orig_buffer
.read(4096) # Buffer size is arbitrary.
1351 self
._file
.write(buffer.replace('\n', os
.linesep
))
1352 elif isinstance(message
, str):
1353 body_start
= message
.find('\n\n') + 2
1354 if body_start
- 2 != -1:
1355 self
._file
.write(message
[:body_start
].replace('\n',
1357 self
._file
.write('*** EOOH ***' + os
.linesep
)
1358 self
._file
.write(message
[:body_start
].replace('\n',
1360 self
._file
.write(message
[body_start
:].replace('\n',
1363 self
._file
.write('*** EOOH ***' + os
.linesep
+ os
.linesep
)
1364 self
._file
.write(message
.replace('\n', os
.linesep
))
1365 elif hasattr(message
, 'readline'):
1366 original_pos
= message
.tell()
1369 line
= message
.readline()
1370 self
._file
.write(line
.replace('\n', os
.linesep
))
1371 if line
== '\n' or line
== '':
1372 self
._file
.write('*** EOOH ***' + os
.linesep
)
1375 message
.seek(original_pos
)
1379 buffer = message
.read(4096) # Buffer size is arbitrary.
1382 self
._file
.write(buffer.replace('\n', os
.linesep
))
1384 raise TypeError('Invalid message type: %s' % type(message
))
1385 stop
= self
._file
.tell()
1386 return (start
, stop
)
1389 class Message(email
.message
.Message
):
1390 """Message with mailbox-format-specific properties."""
1392 def __init__(self
, message
=None):
1393 """Initialize a Message instance."""
1394 if isinstance(message
, email
.message
.Message
):
1395 self
._become
_message
(copy
.deepcopy(message
))
1396 if isinstance(message
, Message
):
1397 message
._explain
_to
(self
)
1398 elif isinstance(message
, str):
1399 self
._become
_message
(email
.message_from_string(message
))
1400 elif hasattr(message
, "read"):
1401 self
._become
_message
(email
.message_from_file(message
))
1402 elif message
is None:
1403 email
.message
.Message
.__init
__(self
)
1405 raise TypeError('Invalid message type: %s' % type(message
))
1407 def _become_message(self
, message
):
1408 """Assume the non-format-specific state of message."""
1409 for name
in ('_headers', '_unixfrom', '_payload', '_charset',
1410 'preamble', 'epilogue', 'defects', '_default_type'):
1411 self
.__dict
__[name
] = message
.__dict
__[name
]
1413 def _explain_to(self
, message
):
1414 """Copy format-specific state to message insofar as possible."""
1415 if isinstance(message
, Message
):
1416 return # There's nothing format-specific to explain.
1418 raise TypeError('Cannot convert to specified type')
1421 class MaildirMessage(Message
):
1422 """Message with Maildir-specific properties."""
1424 def __init__(self
, message
=None):
1425 """Initialize a MaildirMessage instance."""
1426 self
._subdir
= 'new'
1428 self
._date
= time
.time()
1429 Message
.__init
__(self
, message
)
1431 def get_subdir(self
):
1432 """Return 'new' or 'cur'."""
1435 def set_subdir(self
, subdir
):
1436 """Set subdir to 'new' or 'cur'."""
1437 if subdir
== 'new' or subdir
== 'cur':
1438 self
._subdir
= subdir
1440 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir
)
1442 def get_flags(self
):
1443 """Return as a string the flags that are set."""
1444 if self
._info
.startswith('2,'):
1445 return self
._info
[2:]
1449 def set_flags(self
, flags
):
1450 """Set the given flags and unset all others."""
1451 self
._info
= '2,' + ''.join(sorted(flags
))
1453 def add_flag(self
, flag
):
1454 """Set the given flag(s) without changing others."""
1455 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1457 def remove_flag(self
, flag
):
1458 """Unset the given string flag(s) without changing others."""
1459 if self
.get_flags() != '':
1460 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1463 """Return delivery date of message, in seconds since the epoch."""
1466 def set_date(self
, date
):
1467 """Set delivery date of message, in seconds since the epoch."""
1469 self
._date
= float(date
)
1471 raise TypeError("can't convert to float: %s" % date
)
1474 """Get the message's "info" as a string."""
1477 def set_info(self
, info
):
1478 """Set the message's "info" string."""
1479 if isinstance(info
, str):
1482 raise TypeError('info must be a string: %s' % type(info
))
1484 def _explain_to(self
, message
):
1485 """Copy Maildir-specific state to message insofar as possible."""
1486 if isinstance(message
, MaildirMessage
):
1487 message
.set_flags(self
.get_flags())
1488 message
.set_subdir(self
.get_subdir())
1489 message
.set_date(self
.get_date())
1490 elif isinstance(message
, _mboxMMDFMessage
):
1491 flags
= set(self
.get_flags())
1493 message
.add_flag('R')
1494 if self
.get_subdir() == 'cur':
1495 message
.add_flag('O')
1497 message
.add_flag('D')
1499 message
.add_flag('F')
1501 message
.add_flag('A')
1502 message
.set_from('MAILER-DAEMON', time
.gmtime(self
.get_date()))
1503 elif isinstance(message
, MHMessage
):
1504 flags
= set(self
.get_flags())
1505 if 'S' not in flags
:
1506 message
.add_sequence('unseen')
1508 message
.add_sequence('replied')
1510 message
.add_sequence('flagged')
1511 elif isinstance(message
, BabylMessage
):
1512 flags
= set(self
.get_flags())
1513 if 'S' not in flags
:
1514 message
.add_label('unseen')
1516 message
.add_label('deleted')
1518 message
.add_label('answered')
1520 message
.add_label('forwarded')
1521 elif isinstance(message
, Message
):
1524 raise TypeError('Cannot convert to specified type: %s' %
1528 class _mboxMMDFMessage(Message
):
1529 """Message with mbox- or MMDF-specific properties."""
1531 def __init__(self
, message
=None):
1532 """Initialize an mboxMMDFMessage instance."""
1533 self
.set_from('MAILER-DAEMON', True)
1534 if isinstance(message
, email
.message
.Message
):
1535 unixfrom
= message
.get_unixfrom()
1536 if unixfrom
is not None and unixfrom
.startswith('From '):
1537 self
.set_from(unixfrom
[5:])
1538 Message
.__init
__(self
, message
)
1541 """Return contents of "From " line."""
1544 def set_from(self
, from_
, time_
=None):
1545 """Set "From " line, formatting and appending time_ if specified."""
1546 if time_
is not None:
1548 time_
= time
.gmtime()
1549 from_
+= ' ' + time
.asctime(time_
)
1552 def get_flags(self
):
1553 """Return as a string the flags that are set."""
1554 return self
.get('Status', '') + self
.get('X-Status', '')
1556 def set_flags(self
, flags
):
1557 """Set the given flags and unset all others."""
1559 status_flags
, xstatus_flags
= '', ''
1560 for flag
in ('R', 'O'):
1562 status_flags
+= flag
1564 for flag
in ('D', 'F', 'A'):
1566 xstatus_flags
+= flag
1568 xstatus_flags
+= ''.join(sorted(flags
))
1570 self
.replace_header('Status', status_flags
)
1572 self
.add_header('Status', status_flags
)
1574 self
.replace_header('X-Status', xstatus_flags
)
1576 self
.add_header('X-Status', xstatus_flags
)
1578 def add_flag(self
, flag
):
1579 """Set the given flag(s) without changing others."""
1580 self
.set_flags(''.join(set(self
.get_flags()) |
set(flag
)))
1582 def remove_flag(self
, flag
):
1583 """Unset the given string flag(s) without changing others."""
1584 if 'Status' in self
or 'X-Status' in self
:
1585 self
.set_flags(''.join(set(self
.get_flags()) - set(flag
)))
1587 def _explain_to(self
, message
):
1588 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1589 if isinstance(message
, MaildirMessage
):
1590 flags
= set(self
.get_flags())
1592 message
.set_subdir('cur')
1594 message
.add_flag('F')
1596 message
.add_flag('R')
1598 message
.add_flag('S')
1600 message
.add_flag('T')
1601 del message
['status']
1602 del message
['x-status']
1603 maybe_date
= ' '.join(self
.get_from().split()[-5:])
1605 message
.set_date(calendar
.timegm(time
.strptime(maybe_date
,
1606 '%a %b %d %H:%M:%S %Y')))
1607 except (ValueError, OverflowError):
1609 elif isinstance(message
, _mboxMMDFMessage
):
1610 message
.set_flags(self
.get_flags())
1611 message
.set_from(self
.get_from())
1612 elif isinstance(message
, MHMessage
):
1613 flags
= set(self
.get_flags())
1614 if 'R' not in flags
:
1615 message
.add_sequence('unseen')
1617 message
.add_sequence('replied')
1619 message
.add_sequence('flagged')
1620 del message
['status']
1621 del message
['x-status']
1622 elif isinstance(message
, BabylMessage
):
1623 flags
= set(self
.get_flags())
1624 if 'R' not in flags
:
1625 message
.add_label('unseen')
1627 message
.add_label('deleted')
1629 message
.add_label('answered')
1630 del message
['status']
1631 del message
['x-status']
1632 elif isinstance(message
, Message
):
1635 raise TypeError('Cannot convert to specified type: %s' %
1639 class mboxMessage(_mboxMMDFMessage
):
1640 """Message with mbox-specific properties."""
1643 class MHMessage(Message
):
1644 """Message with MH-specific properties."""
1646 def __init__(self
, message
=None):
1647 """Initialize an MHMessage instance."""
1648 self
._sequences
= []
1649 Message
.__init
__(self
, message
)
1651 def get_sequences(self
):
1652 """Return a list of sequences that include the message."""
1653 return self
._sequences
[:]
1655 def set_sequences(self
, sequences
):
1656 """Set the list of sequences that include the message."""
1657 self
._sequences
= list(sequences
)
1659 def add_sequence(self
, sequence
):
1660 """Add sequence to list of sequences including the message."""
1661 if isinstance(sequence
, str):
1662 if not sequence
in self
._sequences
:
1663 self
._sequences
.append(sequence
)
1665 raise TypeError('sequence must be a string: %s' % type(sequence
))
1667 def remove_sequence(self
, sequence
):
1668 """Remove sequence from the list of sequences including the message."""
1670 self
._sequences
.remove(sequence
)
1674 def _explain_to(self
, message
):
1675 """Copy MH-specific state to message insofar as possible."""
1676 if isinstance(message
, MaildirMessage
):
1677 sequences
= set(self
.get_sequences())
1678 if 'unseen' in sequences
:
1679 message
.set_subdir('cur')
1681 message
.set_subdir('cur')
1682 message
.add_flag('S')
1683 if 'flagged' in sequences
:
1684 message
.add_flag('F')
1685 if 'replied' in sequences
:
1686 message
.add_flag('R')
1687 elif isinstance(message
, _mboxMMDFMessage
):
1688 sequences
= set(self
.get_sequences())
1689 if 'unseen' not in sequences
:
1690 message
.add_flag('RO')
1692 message
.add_flag('O')
1693 if 'flagged' in sequences
:
1694 message
.add_flag('F')
1695 if 'replied' in sequences
:
1696 message
.add_flag('A')
1697 elif isinstance(message
, MHMessage
):
1698 for sequence
in self
.get_sequences():
1699 message
.add_sequence(sequence
)
1700 elif isinstance(message
, BabylMessage
):
1701 sequences
= set(self
.get_sequences())
1702 if 'unseen' in sequences
:
1703 message
.add_label('unseen')
1704 if 'replied' in sequences
:
1705 message
.add_label('answered')
1706 elif isinstance(message
, Message
):
1709 raise TypeError('Cannot convert to specified type: %s' %
1713 class BabylMessage(Message
):
1714 """Message with Babyl-specific properties."""
1716 def __init__(self
, message
=None):
1717 """Initialize an BabylMessage instance."""
1719 self
._visible
= Message()
1720 Message
.__init
__(self
, message
)
1722 def get_labels(self
):
1723 """Return a list of labels on the message."""
1724 return self
._labels
[:]
1726 def set_labels(self
, labels
):
1727 """Set the list of labels on the message."""
1728 self
._labels
= list(labels
)
1730 def add_label(self
, label
):
1731 """Add label to list of labels on the message."""
1732 if isinstance(label
, str):
1733 if label
not in self
._labels
:
1734 self
._labels
.append(label
)
1736 raise TypeError('label must be a string: %s' % type(label
))
1738 def remove_label(self
, label
):
1739 """Remove label from the list of labels on the message."""
1741 self
._labels
.remove(label
)
1745 def get_visible(self
):
1746 """Return a Message representation of visible headers."""
1747 return Message(self
._visible
)
1749 def set_visible(self
, visible
):
1750 """Set the Message representation of visible headers."""
1751 self
._visible
= Message(visible
)
1753 def update_visible(self
):
1754 """Update and/or sensibly generate a set of visible headers."""
1755 for header
in self
._visible
.keys():
1757 self
._visible
.replace_header(header
, self
[header
])
1759 del self
._visible
[header
]
1760 for header
in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1761 if header
in self
and header
not in self
._visible
:
1762 self
._visible
[header
] = self
[header
]
1764 def _explain_to(self
, message
):
1765 """Copy Babyl-specific state to message insofar as possible."""
1766 if isinstance(message
, MaildirMessage
):
1767 labels
= set(self
.get_labels())
1768 if 'unseen' in labels
:
1769 message
.set_subdir('cur')
1771 message
.set_subdir('cur')
1772 message
.add_flag('S')
1773 if 'forwarded' in labels
or 'resent' in labels
:
1774 message
.add_flag('P')
1775 if 'answered' in labels
:
1776 message
.add_flag('R')
1777 if 'deleted' in labels
:
1778 message
.add_flag('T')
1779 elif isinstance(message
, _mboxMMDFMessage
):
1780 labels
= set(self
.get_labels())
1781 if 'unseen' not in labels
:
1782 message
.add_flag('RO')
1784 message
.add_flag('O')
1785 if 'deleted' in labels
:
1786 message
.add_flag('D')
1787 if 'answered' in labels
:
1788 message
.add_flag('A')
1789 elif isinstance(message
, MHMessage
):
1790 labels
= set(self
.get_labels())
1791 if 'unseen' in labels
:
1792 message
.add_sequence('unseen')
1793 if 'answered' in labels
:
1794 message
.add_sequence('replied')
1795 elif isinstance(message
, BabylMessage
):
1796 message
.set_visible(self
.get_visible())
1797 for label
in self
.get_labels():
1798 message
.add_label(label
)
1799 elif isinstance(message
, Message
):
1802 raise TypeError('Cannot convert to specified type: %s' %
1806 class MMDFMessage(_mboxMMDFMessage
):
1807 """Message with MMDF-specific properties."""
1811 """A read-only wrapper of a file."""
1813 def __init__(self
, f
, pos
=None):
1814 """Initialize a _ProxyFile."""
1817 self
._pos
= f
.tell()
1821 def read(self
, size
=None):
1823 return self
._read
(size
, self
._file
.read
)
1825 def readline(self
, size
=None):
1827 return self
._read
(size
, self
._file
.readline
)
1829 def readlines(self
, sizehint
=None):
1830 """Read multiple lines."""
1834 if sizehint
is not None:
1835 sizehint
-= len(line
)
1841 """Iterate over lines."""
1842 return iter(self
.readline
, "")
1845 """Return the position."""
1848 def seek(self
, offset
, whence
=0):
1849 """Change position."""
1851 self
._file
.seek(self
._pos
)
1852 self
._file
.seek(offset
, whence
)
1853 self
._pos
= self
._file
.tell()
1856 """Close the file."""
1859 def _read(self
, size
, read_method
):
1860 """Read size bytes using read_method."""
1863 self
._file
.seek(self
._pos
)
1864 result
= read_method(size
)
1865 self
._pos
= self
._file
.tell()
1869 class _PartialFile(_ProxyFile
):
1870 """A read-only wrapper of part of a file."""
1872 def __init__(self
, f
, start
=None, stop
=None):
1873 """Initialize a _PartialFile."""
1874 _ProxyFile
.__init
__(self
, f
, start
)
1879 """Return the position with respect to start."""
1880 return _ProxyFile
.tell(self
) - self
._start
1882 def seek(self
, offset
, whence
=0):
1883 """Change position, possibly with respect to start or stop."""
1885 self
._pos
= self
._start
1888 self
._pos
= self
._stop
1890 _ProxyFile
.seek(self
, offset
, whence
)
1892 def _read(self
, size
, read_method
):
1893 """Read size bytes using read_method, honoring start and stop."""
1894 remaining
= self
._stop
- self
._pos
1897 if size
is None or size
< 0 or size
> remaining
:
1899 return _ProxyFile
._read
(self
, size
, read_method
)
1902 def _lock_file(f
, dotlock
=True):
1903 """Lock file f using lockf and dot locking."""
1904 dotlock_done
= False
1908 fcntl
.lockf(f
, fcntl
.LOCK_EX | fcntl
.LOCK_NB
)
1910 if e
.errno
in (errno
.EAGAIN
, errno
.EACCES
, errno
.EROFS
):
1911 raise ExternalClashError('lockf: lock unavailable: %s' %
1917 pre_lock
= _create_temporary(f
.name
+ '.lock')
1920 if e
.errno
in (errno
.EACCES
, errno
.EROFS
):
1921 return # Without write access, just skip dotlocking.
1925 if hasattr(os
, 'link'):
1926 os
.link(pre_lock
.name
, f
.name
+ '.lock')
1928 os
.unlink(pre_lock
.name
)
1930 os
.rename(pre_lock
.name
, f
.name
+ '.lock')
1933 if e
.errno
== errno
.EEXIST
or \
1934 (os
.name
== 'os2' and e
.errno
== errno
.EACCES
):
1935 os
.remove(pre_lock
.name
)
1936 raise ExternalClashError('dot lock unavailable: %s' %
1942 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1944 os
.remove(f
.name
+ '.lock')
1947 def _unlock_file(f
):
1948 """Unlock file f using lockf and dot locking."""
1950 fcntl
.lockf(f
, fcntl
.LOCK_UN
)
1951 if os
.path
.exists(f
.name
+ '.lock'):
1952 os
.remove(f
.name
+ '.lock')
1954 def _create_carefully(path
):
1955 """Create a file if it doesn't exist and open for reading and writing."""
1956 fd
= os
.open(path
, os
.O_CREAT | os
.O_EXCL | os
.O_RDWR
, 0666)
1958 return open(path
, 'rb+')
1962 def _create_temporary(path
):
1963 """Create a temp file based on path and open for reading and writing."""
1964 return _create_carefully('%s.%s.%s.%s' % (path
, int(time
.time()),
1965 socket
.gethostname(),
1969 """Ensure changes to file f are physically on disk."""
1971 if hasattr(os
, 'fsync'):
1972 os
.fsync(f
.fileno())
1975 """Close file f, ensuring all changes are physically on disk."""
1979 ## Start: classes from the original module (for backward compatibility).
1981 # Note that the Maildir class, whose name is unchanged, itself offers a next()
1982 # method for backward compatibility.
1986 def __init__(self
, fp
, factory
=rfc822
.Message
):
1989 self
.factory
= factory
1992 return iter(self
.next
, None)
1996 self
.fp
.seek(self
.seekp
)
1998 self
._search
_start
()
2000 self
.seekp
= self
.fp
.tell()
2002 start
= self
.fp
.tell()
2004 self
.seekp
= stop
= self
.fp
.tell()
2007 return self
.factory(_PartialFile(self
.fp
, start
, stop
))
2009 # Recommended to use PortableUnixMailbox instead!
2010 class UnixMailbox(_Mailbox
):
2012 def _search_start(self
):
2014 pos
= self
.fp
.tell()
2015 line
= self
.fp
.readline()
2018 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
2022 def _search_end(self
):
2023 self
.fp
.readline() # Throw away header line
2025 pos
= self
.fp
.tell()
2026 line
= self
.fp
.readline()
2029 if line
[:5] == 'From ' and self
._isrealfromline
(line
):
2033 # An overridable mechanism to test for From-line-ness. You can either
2034 # specify a different regular expression or define a whole new
2035 # _isrealfromline() method. Note that this only gets called for lines
2036 # starting with the 5 characters "From ".
2039 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
2040 # the only portable, reliable way to find message delimiters in a BSD (i.e
2041 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
2042 # beginning of the file, "^From .*\n". While _fromlinepattern below seems
2043 # like a good idea, in practice, there are too many variations for more
2044 # strict parsing of the line to be completely accurate.
2046 # _strict_isrealfromline() is the old version which tries to do stricter
2047 # parsing of the From_ line. _portable_isrealfromline() simply returns
2048 # true, since it's never called if the line doesn't already start with
2051 # This algorithm, and the way it interacts with _search_start() and
2052 # _search_end() may not be completely correct, because it doesn't check
2053 # that the two characters preceding "From " are \n\n or the beginning of
2054 # the file. Fixing this would require a more extensive rewrite than is
2055 # necessary. For convenience, we've added a PortableUnixMailbox class
2056 # which does no checking of the format of the 'From' line.
2058 _fromlinepattern
= (r
"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
2059 r
"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
2064 def _strict_isrealfromline(self
, line
):
2065 if not self
._regexp
:
2067 self
._regexp
= re
.compile(self
._fromlinepattern
)
2068 return self
._regexp
.match(line
)
2070 def _portable_isrealfromline(self
, line
):
2073 _isrealfromline
= _strict_isrealfromline
2076 class PortableUnixMailbox(UnixMailbox
):
2077 _isrealfromline
= UnixMailbox
._portable
_isrealfromline
2080 class MmdfMailbox(_Mailbox
):
2082 def _search_start(self
):
2084 line
= self
.fp
.readline()
2087 if line
[:5] == '\001\001\001\001\n':
2090 def _search_end(self
):
2092 pos
= self
.fp
.tell()
2093 line
= self
.fp
.readline()
2096 if line
== '\001\001\001\001\n':
2103 def __init__(self
, dirname
, factory
=rfc822
.Message
):
2105 pat
= re
.compile('^[1-9][0-9]*$')
2106 self
.dirname
= dirname
2107 # the three following lines could be combined into:
2108 # list = map(long, filter(pat.match, os.listdir(self.dirname)))
2109 list = os
.listdir(self
.dirname
)
2110 list = filter(pat
.match
, list)
2111 list = map(long, list)
2113 # This only works in Python 1.6 or later;
2114 # before that str() added 'L':
2115 self
.boxes
= map(str, list)
2116 self
.boxes
.reverse()
2117 self
.factory
= factory
2120 return iter(self
.next
, None)
2125 fn
= self
.boxes
.pop()
2126 fp
= open(os
.path
.join(self
.dirname
, fn
))
2127 msg
= self
.factory(fp
)
2130 except (AttributeError, TypeError):
2135 class BabylMailbox(_Mailbox
):
2137 def _search_start(self
):
2139 line
= self
.fp
.readline()
2142 if line
== '*** EOOH ***\n':
2145 def _search_end(self
):
2147 pos
= self
.fp
.tell()
2148 line
= self
.fp
.readline()
2151 if line
== '\037\014\n' or line
== '\037':
2155 ## End: classes from the original module (for backward compatibility).
2158 class Error(Exception):
2159 """Raised for module-specific errors."""
2161 class NoSuchMailboxError(Error
):
2162 """The specified mailbox does not exist and won't be created."""
2164 class NotEmptyError(Error
):
2165 """The specified mailbox is not empty and deletion was requested."""
2167 class ExternalClashError(Error
):
2168 """Another process caused an action to fail."""
2170 class FormatError(Error
):
2171 """A file appears to have an invalid format."""