UserString.MutableString has been removed in Python 3.0.
[python.git] / Lib / mailbox.py
blobe3e9cbb3422707d5f313634b8393182c0d666828
1 #! /usr/bin/env python
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
9 # _sync_close().
11 import sys
12 import os
13 import time
14 import calendar
15 import socket
16 import errno
17 import copy
18 import email
19 import email.message
20 import email.generator
21 import rfc822
22 import StringIO
23 try:
24 if sys.platform == 'os2emx':
25 # OS/2 EMX fcntl() not adequate
26 raise ImportError
27 import fcntl
28 except ImportError:
29 fcntl = None
31 __all__ = [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
32 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
33 'BabylMessage', 'MMDFMessage', 'UnixMailbox',
34 'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ]
36 class Mailbox:
37 """A group of messages in a particular place."""
39 def __init__(self, path, factory=None, create=True):
40 """Initialize a Mailbox instance."""
41 self._path = os.path.abspath(os.path.expanduser(path))
42 self._factory = factory
44 def add(self, message):
45 """Add message and return assigned key."""
46 raise NotImplementedError('Method must be implemented by subclass')
48 def remove(self, key):
49 """Remove the keyed message; raise KeyError if it doesn't exist."""
50 raise NotImplementedError('Method must be implemented by subclass')
52 def __delitem__(self, key):
53 self.remove(key)
55 def discard(self, key):
56 """If the keyed message exists, remove it."""
57 try:
58 self.remove(key)
59 except KeyError:
60 pass
62 def __setitem__(self, key, message):
63 """Replace the keyed message; raise KeyError if it doesn't exist."""
64 raise NotImplementedError('Method must be implemented by subclass')
66 def get(self, key, default=None):
67 """Return the keyed message, or default if it doesn't exist."""
68 try:
69 return self.__getitem__(key)
70 except KeyError:
71 return default
73 def __getitem__(self, key):
74 """Return the keyed message; raise KeyError if it doesn't exist."""
75 if not self._factory:
76 return self.get_message(key)
77 else:
78 return self._factory(self.get_file(key))
80 def get_message(self, key):
81 """Return a Message representation or raise a KeyError."""
82 raise NotImplementedError('Method must be implemented by subclass')
84 def get_string(self, key):
85 """Return a string representation or raise a KeyError."""
86 raise NotImplementedError('Method must be implemented by subclass')
88 def get_file(self, key):
89 """Return a file-like representation or raise a KeyError."""
90 raise NotImplementedError('Method must be implemented by subclass')
92 def iterkeys(self):
93 """Return an iterator over keys."""
94 raise NotImplementedError('Method must be implemented by subclass')
96 def keys(self):
97 """Return a list of keys."""
98 return list(self.iterkeys())
100 def itervalues(self):
101 """Return an iterator over all messages."""
102 for key in self.iterkeys():
103 try:
104 value = self[key]
105 except KeyError:
106 continue
107 yield value
109 def __iter__(self):
110 return self.itervalues()
112 def values(self):
113 """Return a list of messages. Memory intensive."""
114 return list(self.itervalues())
116 def iteritems(self):
117 """Return an iterator over (key, message) tuples."""
118 for key in self.iterkeys():
119 try:
120 value = self[key]
121 except KeyError:
122 continue
123 yield (key, value)
125 def items(self):
126 """Return a list of (key, message) tuples. Memory intensive."""
127 return list(self.iteritems())
129 def has_key(self, key):
130 """Return True if the keyed message exists, False otherwise."""
131 raise NotImplementedError('Method must be implemented by subclass')
133 def __contains__(self, key):
134 return self.has_key(key)
136 def __len__(self):
137 """Return a count of messages in the mailbox."""
138 raise NotImplementedError('Method must be implemented by subclass')
140 def clear(self):
141 """Delete all messages."""
142 for key in self.iterkeys():
143 self.discard(key)
145 def pop(self, key, default=None):
146 """Delete the keyed message and return it, or default."""
147 try:
148 result = self[key]
149 except KeyError:
150 return default
151 self.discard(key)
152 return result
154 def popitem(self):
155 """Delete an arbitrary (key, message) pair and return it."""
156 for key in self.iterkeys():
157 return (key, self.pop(key)) # This is only run once.
158 else:
159 raise KeyError('No messages in mailbox')
161 def update(self, arg=None):
162 """Change the messages that correspond to certain keys."""
163 if hasattr(arg, 'iteritems'):
164 source = arg.iteritems()
165 elif hasattr(arg, 'items'):
166 source = arg.items()
167 else:
168 source = arg
169 bad_key = False
170 for key, message in source:
171 try:
172 self[key] = message
173 except KeyError:
174 bad_key = True
175 if bad_key:
176 raise KeyError('No message with key(s)')
178 def flush(self):
179 """Write any pending changes to the disk."""
180 raise NotImplementedError('Method must be implemented by subclass')
182 def lock(self):
183 """Lock the mailbox."""
184 raise NotImplementedError('Method must be implemented by subclass')
186 def unlock(self):
187 """Unlock the mailbox if it is locked."""
188 raise NotImplementedError('Method must be implemented by subclass')
190 def close(self):
191 """Flush and close the mailbox."""
192 raise NotImplementedError('Method must be implemented by subclass')
194 def _dump_message(self, message, target, mangle_from_=False):
195 # Most files are opened in binary mode to allow predictable seeking.
196 # To get native line endings on disk, the user-friendly \n line endings
197 # used in strings and by email.Message are translated here.
198 """Dump message contents to target file."""
199 if isinstance(message, email.message.Message):
200 buffer = StringIO.StringIO()
201 gen = email.generator.Generator(buffer, mangle_from_, 0)
202 gen.flatten(message)
203 buffer.seek(0)
204 target.write(buffer.read().replace('\n', os.linesep))
205 elif isinstance(message, str):
206 if mangle_from_:
207 message = message.replace('\nFrom ', '\n>From ')
208 message = message.replace('\n', os.linesep)
209 target.write(message)
210 elif hasattr(message, 'read'):
211 while True:
212 line = message.readline()
213 if line == '':
214 break
215 if mangle_from_ and line.startswith('From '):
216 line = '>From ' + line[5:]
217 line = line.replace('\n', os.linesep)
218 target.write(line)
219 else:
220 raise TypeError('Invalid message type: %s' % type(message))
223 class Maildir(Mailbox):
224 """A qmail-style Maildir mailbox."""
226 colon = ':'
228 def __init__(self, dirname, factory=rfc822.Message, create=True):
229 """Initialize a Maildir instance."""
230 Mailbox.__init__(self, dirname, factory, create)
231 if not os.path.exists(self._path):
232 if create:
233 os.mkdir(self._path, 0700)
234 os.mkdir(os.path.join(self._path, 'tmp'), 0700)
235 os.mkdir(os.path.join(self._path, 'new'), 0700)
236 os.mkdir(os.path.join(self._path, 'cur'), 0700)
237 else:
238 raise NoSuchMailboxError(self._path)
239 self._toc = {}
241 def add(self, message):
242 """Add message and return assigned key."""
243 tmp_file = self._create_tmp()
244 try:
245 self._dump_message(message, tmp_file)
246 finally:
247 _sync_close(tmp_file)
248 if isinstance(message, MaildirMessage):
249 subdir = message.get_subdir()
250 suffix = self.colon + message.get_info()
251 if suffix == self.colon:
252 suffix = ''
253 else:
254 subdir = 'new'
255 suffix = ''
256 uniq = os.path.basename(tmp_file.name).split(self.colon)[0]
257 dest = os.path.join(self._path, subdir, uniq + suffix)
258 try:
259 if hasattr(os, 'link'):
260 os.link(tmp_file.name, dest)
261 os.remove(tmp_file.name)
262 else:
263 os.rename(tmp_file.name, dest)
264 except OSError, e:
265 os.remove(tmp_file.name)
266 if e.errno == errno.EEXIST:
267 raise ExternalClashError('Name clash with existing message: %s'
268 % dest)
269 else:
270 raise
271 if isinstance(message, MaildirMessage):
272 os.utime(dest, (os.path.getatime(dest), message.get_date()))
273 return uniq
275 def remove(self, key):
276 """Remove the keyed message; raise KeyError if it doesn't exist."""
277 os.remove(os.path.join(self._path, self._lookup(key)))
279 def discard(self, key):
280 """If the keyed message exists, remove it."""
281 # This overrides an inapplicable implementation in the superclass.
282 try:
283 self.remove(key)
284 except KeyError:
285 pass
286 except OSError, e:
287 if e.errno != errno.ENOENT:
288 raise
290 def __setitem__(self, key, message):
291 """Replace the keyed message; raise KeyError if it doesn't exist."""
292 old_subpath = self._lookup(key)
293 temp_key = self.add(message)
294 temp_subpath = self._lookup(temp_key)
295 if isinstance(message, MaildirMessage):
296 # temp's subdir and suffix were specified by message.
297 dominant_subpath = temp_subpath
298 else:
299 # temp's subdir and suffix were defaults from add().
300 dominant_subpath = old_subpath
301 subdir = os.path.dirname(dominant_subpath)
302 if self.colon in dominant_subpath:
303 suffix = self.colon + dominant_subpath.split(self.colon)[-1]
304 else:
305 suffix = ''
306 self.discard(key)
307 new_path = os.path.join(self._path, subdir, key + suffix)
308 os.rename(os.path.join(self._path, temp_subpath), new_path)
309 if isinstance(message, MaildirMessage):
310 os.utime(new_path, (os.path.getatime(new_path),
311 message.get_date()))
313 def get_message(self, key):
314 """Return a Message representation or raise a KeyError."""
315 subpath = self._lookup(key)
316 f = open(os.path.join(self._path, subpath), 'r')
317 try:
318 if self._factory:
319 msg = self._factory(f)
320 else:
321 msg = MaildirMessage(f)
322 finally:
323 f.close()
324 subdir, name = os.path.split(subpath)
325 msg.set_subdir(subdir)
326 if self.colon in name:
327 msg.set_info(name.split(self.colon)[-1])
328 msg.set_date(os.path.getmtime(os.path.join(self._path, subpath)))
329 return msg
331 def get_string(self, key):
332 """Return a string representation or raise a KeyError."""
333 f = open(os.path.join(self._path, self._lookup(key)), 'r')
334 try:
335 return f.read()
336 finally:
337 f.close()
339 def get_file(self, key):
340 """Return a file-like representation or raise a KeyError."""
341 f = open(os.path.join(self._path, self._lookup(key)), 'rb')
342 return _ProxyFile(f)
344 def iterkeys(self):
345 """Return an iterator over keys."""
346 self._refresh()
347 for key in self._toc:
348 try:
349 self._lookup(key)
350 except KeyError:
351 continue
352 yield key
354 def has_key(self, key):
355 """Return True if the keyed message exists, False otherwise."""
356 self._refresh()
357 return key in self._toc
359 def __len__(self):
360 """Return a count of messages in the mailbox."""
361 self._refresh()
362 return len(self._toc)
364 def flush(self):
365 """Write any pending changes to disk."""
366 return # Maildir changes are always written immediately.
368 def lock(self):
369 """Lock the mailbox."""
370 return
372 def unlock(self):
373 """Unlock the mailbox if it is locked."""
374 return
376 def close(self):
377 """Flush and close the mailbox."""
378 return
380 def list_folders(self):
381 """Return a list of folder names."""
382 result = []
383 for entry in os.listdir(self._path):
384 if len(entry) > 1 and entry[0] == '.' and \
385 os.path.isdir(os.path.join(self._path, entry)):
386 result.append(entry[1:])
387 return result
389 def get_folder(self, folder):
390 """Return a Maildir instance for the named folder."""
391 return Maildir(os.path.join(self._path, '.' + folder),
392 factory=self._factory,
393 create=False)
395 def add_folder(self, folder):
396 """Create a folder and return a Maildir instance representing it."""
397 path = os.path.join(self._path, '.' + folder)
398 result = Maildir(path, factory=self._factory)
399 maildirfolder_path = os.path.join(path, 'maildirfolder')
400 if not os.path.exists(maildirfolder_path):
401 os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY))
402 return result
404 def remove_folder(self, folder):
405 """Delete the named folder, which must be empty."""
406 path = os.path.join(self._path, '.' + folder)
407 for entry in os.listdir(os.path.join(path, 'new')) + \
408 os.listdir(os.path.join(path, 'cur')):
409 if len(entry) < 1 or entry[0] != '.':
410 raise NotEmptyError('Folder contains message(s): %s' % folder)
411 for entry in os.listdir(path):
412 if entry != 'new' and entry != 'cur' and entry != 'tmp' and \
413 os.path.isdir(os.path.join(path, entry)):
414 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
415 (folder, entry))
416 for root, dirs, files in os.walk(path, topdown=False):
417 for entry in files:
418 os.remove(os.path.join(root, entry))
419 for entry in dirs:
420 os.rmdir(os.path.join(root, entry))
421 os.rmdir(path)
423 def clean(self):
424 """Delete old files in "tmp"."""
425 now = time.time()
426 for entry in os.listdir(os.path.join(self._path, 'tmp')):
427 path = os.path.join(self._path, 'tmp', entry)
428 if now - os.path.getatime(path) > 129600: # 60 * 60 * 36
429 os.remove(path)
431 _count = 1 # This is used to generate unique file names.
433 def _create_tmp(self):
434 """Create a file in the tmp subdirectory and open and return it."""
435 now = time.time()
436 hostname = socket.gethostname()
437 if '/' in hostname:
438 hostname = hostname.replace('/', r'\057')
439 if ':' in hostname:
440 hostname = hostname.replace(':', r'\072')
441 uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(),
442 Maildir._count, hostname)
443 path = os.path.join(self._path, 'tmp', uniq)
444 try:
445 os.stat(path)
446 except OSError, e:
447 if e.errno == errno.ENOENT:
448 Maildir._count += 1
449 try:
450 return _create_carefully(path)
451 except OSError, e:
452 if e.errno != errno.EEXIST:
453 raise
454 else:
455 raise
457 # Fall through to here if stat succeeded or open raised EEXIST.
458 raise ExternalClashError('Name clash prevented file creation: %s' %
459 path)
461 def _refresh(self):
462 """Update table of contents mapping."""
463 self._toc = {}
464 for subdir in ('new', 'cur'):
465 subdir_path = os.path.join(self._path, subdir)
466 for entry in os.listdir(subdir_path):
467 p = os.path.join(subdir_path, entry)
468 if os.path.isdir(p):
469 continue
470 uniq = entry.split(self.colon)[0]
471 self._toc[uniq] = os.path.join(subdir, entry)
473 def _lookup(self, key):
474 """Use TOC to return subpath for given key, or raise a KeyError."""
475 try:
476 if os.path.exists(os.path.join(self._path, self._toc[key])):
477 return self._toc[key]
478 except KeyError:
479 pass
480 self._refresh()
481 try:
482 return self._toc[key]
483 except KeyError:
484 raise KeyError('No message with key: %s' % key)
486 # This method is for backward compatibility only.
487 def next(self):
488 """Return the next message in a one-time iteration."""
489 if not hasattr(self, '_onetime_keys'):
490 self._onetime_keys = self.iterkeys()
491 while True:
492 try:
493 return self[self._onetime_keys.next()]
494 except StopIteration:
495 return None
496 except KeyError:
497 continue
500 class _singlefileMailbox(Mailbox):
501 """A single-file mailbox."""
503 def __init__(self, path, factory=None, create=True):
504 """Initialize a single-file mailbox."""
505 Mailbox.__init__(self, path, factory, create)
506 try:
507 f = open(self._path, 'rb+')
508 except IOError, e:
509 if e.errno == errno.ENOENT:
510 if create:
511 f = open(self._path, 'wb+')
512 else:
513 raise NoSuchMailboxError(self._path)
514 elif e.errno == errno.EACCES:
515 f = open(self._path, 'rb')
516 else:
517 raise
518 self._file = f
519 self._toc = None
520 self._next_key = 0
521 self._pending = False # No changes require rewriting the file.
522 self._locked = False
523 self._file_length = None # Used to record mailbox size
525 def add(self, message):
526 """Add message and return assigned key."""
527 self._lookup()
528 self._toc[self._next_key] = self._append_message(message)
529 self._next_key += 1
530 self._pending = True
531 return self._next_key - 1
533 def remove(self, key):
534 """Remove the keyed message; raise KeyError if it doesn't exist."""
535 self._lookup(key)
536 del self._toc[key]
537 self._pending = True
539 def __setitem__(self, key, message):
540 """Replace the keyed message; raise KeyError if it doesn't exist."""
541 self._lookup(key)
542 self._toc[key] = self._append_message(message)
543 self._pending = True
545 def iterkeys(self):
546 """Return an iterator over keys."""
547 self._lookup()
548 for key in self._toc.keys():
549 yield key
551 def has_key(self, key):
552 """Return True if the keyed message exists, False otherwise."""
553 self._lookup()
554 return key in self._toc
556 def __len__(self):
557 """Return a count of messages in the mailbox."""
558 self._lookup()
559 return len(self._toc)
561 def lock(self):
562 """Lock the mailbox."""
563 if not self._locked:
564 _lock_file(self._file)
565 self._locked = True
567 def unlock(self):
568 """Unlock the mailbox if it is locked."""
569 if self._locked:
570 _unlock_file(self._file)
571 self._locked = False
573 def flush(self):
574 """Write any pending changes to disk."""
575 if not self._pending:
576 return
578 # In order to be writing anything out at all, self._toc must
579 # already have been generated (and presumably has been modified
580 # by adding or deleting an item).
581 assert self._toc is not None
583 # Check length of self._file; if it's changed, some other process
584 # has modified the mailbox since we scanned it.
585 self._file.seek(0, 2)
586 cur_len = self._file.tell()
587 if cur_len != self._file_length:
588 raise ExternalClashError('Size of mailbox file changed '
589 '(expected %i, found %i)' %
590 (self._file_length, cur_len))
592 new_file = _create_temporary(self._path)
593 try:
594 new_toc = {}
595 self._pre_mailbox_hook(new_file)
596 for key in sorted(self._toc.keys()):
597 start, stop = self._toc[key]
598 self._file.seek(start)
599 self._pre_message_hook(new_file)
600 new_start = new_file.tell()
601 while True:
602 buffer = self._file.read(min(4096,
603 stop - self._file.tell()))
604 if buffer == '':
605 break
606 new_file.write(buffer)
607 new_toc[key] = (new_start, new_file.tell())
608 self._post_message_hook(new_file)
609 except:
610 new_file.close()
611 os.remove(new_file.name)
612 raise
613 _sync_close(new_file)
614 # self._file is about to get replaced, so no need to sync.
615 self._file.close()
616 try:
617 os.rename(new_file.name, self._path)
618 except OSError, e:
619 if e.errno == errno.EEXIST or \
620 (os.name == 'os2' and e.errno == errno.EACCES):
621 os.remove(self._path)
622 os.rename(new_file.name, self._path)
623 else:
624 raise
625 self._file = open(self._path, 'rb+')
626 self._toc = new_toc
627 self._pending = False
628 if self._locked:
629 _lock_file(self._file, dotlock=False)
631 def _pre_mailbox_hook(self, f):
632 """Called before writing the mailbox to file f."""
633 return
635 def _pre_message_hook(self, f):
636 """Called before writing each message to file f."""
637 return
639 def _post_message_hook(self, f):
640 """Called after writing each message to file f."""
641 return
643 def close(self):
644 """Flush and close the mailbox."""
645 self.flush()
646 if self._locked:
647 self.unlock()
648 self._file.close() # Sync has been done by self.flush() above.
650 def _lookup(self, key=None):
651 """Return (start, stop) or raise KeyError."""
652 if self._toc is None:
653 self._generate_toc()
654 if key is not None:
655 try:
656 return self._toc[key]
657 except KeyError:
658 raise KeyError('No message with key: %s' % key)
660 def _append_message(self, message):
661 """Append message to mailbox and return (start, stop) offsets."""
662 self._file.seek(0, 2)
663 self._pre_message_hook(self._file)
664 offsets = self._install_message(message)
665 self._post_message_hook(self._file)
666 self._file.flush()
667 self._file_length = self._file.tell() # Record current length of mailbox
668 return offsets
672 class _mboxMMDF(_singlefileMailbox):
673 """An mbox or MMDF mailbox."""
675 _mangle_from_ = True
677 def get_message(self, key):
678 """Return a Message representation or raise a KeyError."""
679 start, stop = self._lookup(key)
680 self._file.seek(start)
681 from_line = self._file.readline().replace(os.linesep, '')
682 string = self._file.read(stop - self._file.tell())
683 msg = self._message_factory(string.replace(os.linesep, '\n'))
684 msg.set_from(from_line[5:])
685 return msg
687 def get_string(self, key, from_=False):
688 """Return a string representation or raise a KeyError."""
689 start, stop = self._lookup(key)
690 self._file.seek(start)
691 if not from_:
692 self._file.readline()
693 string = self._file.read(stop - self._file.tell())
694 return string.replace(os.linesep, '\n')
696 def get_file(self, key, from_=False):
697 """Return a file-like representation or raise a KeyError."""
698 start, stop = self._lookup(key)
699 self._file.seek(start)
700 if not from_:
701 self._file.readline()
702 return _PartialFile(self._file, self._file.tell(), stop)
704 def _install_message(self, message):
705 """Format a message and blindly write to self._file."""
706 from_line = None
707 if isinstance(message, str) and message.startswith('From '):
708 newline = message.find('\n')
709 if newline != -1:
710 from_line = message[:newline]
711 message = message[newline + 1:]
712 else:
713 from_line = message
714 message = ''
715 elif isinstance(message, _mboxMMDFMessage):
716 from_line = 'From ' + message.get_from()
717 elif isinstance(message, email.message.Message):
718 from_line = message.get_unixfrom() # May be None.
719 if from_line is None:
720 from_line = 'From MAILER-DAEMON %s' % time.asctime(time.gmtime())
721 start = self._file.tell()
722 self._file.write(from_line + os.linesep)
723 self._dump_message(message, self._file, self._mangle_from_)
724 stop = self._file.tell()
725 return (start, stop)
728 class mbox(_mboxMMDF):
729 """A classic mbox mailbox."""
731 _mangle_from_ = True
733 def __init__(self, path, factory=None, create=True):
734 """Initialize an mbox mailbox."""
735 self._message_factory = mboxMessage
736 _mboxMMDF.__init__(self, path, factory, create)
738 def _pre_message_hook(self, f):
739 """Called before writing each message to file f."""
740 if f.tell() != 0:
741 f.write(os.linesep)
743 def _generate_toc(self):
744 """Generate key-to-(start, stop) table of contents."""
745 starts, stops = [], []
746 self._file.seek(0)
747 while True:
748 line_pos = self._file.tell()
749 line = self._file.readline()
750 if line.startswith('From '):
751 if len(stops) < len(starts):
752 stops.append(line_pos - len(os.linesep))
753 starts.append(line_pos)
754 elif line == '':
755 stops.append(line_pos)
756 break
757 self._toc = dict(enumerate(zip(starts, stops)))
758 self._next_key = len(self._toc)
759 self._file_length = self._file.tell()
762 class MMDF(_mboxMMDF):
763 """An MMDF mailbox."""
765 def __init__(self, path, factory=None, create=True):
766 """Initialize an MMDF mailbox."""
767 self._message_factory = MMDFMessage
768 _mboxMMDF.__init__(self, path, factory, create)
770 def _pre_message_hook(self, f):
771 """Called before writing each message to file f."""
772 f.write('\001\001\001\001' + os.linesep)
774 def _post_message_hook(self, f):
775 """Called after writing each message to file f."""
776 f.write(os.linesep + '\001\001\001\001' + os.linesep)
778 def _generate_toc(self):
779 """Generate key-to-(start, stop) table of contents."""
780 starts, stops = [], []
781 self._file.seek(0)
782 next_pos = 0
783 while True:
784 line_pos = next_pos
785 line = self._file.readline()
786 next_pos = self._file.tell()
787 if line.startswith('\001\001\001\001' + os.linesep):
788 starts.append(next_pos)
789 while True:
790 line_pos = next_pos
791 line = self._file.readline()
792 next_pos = self._file.tell()
793 if line == '\001\001\001\001' + os.linesep:
794 stops.append(line_pos - len(os.linesep))
795 break
796 elif line == '':
797 stops.append(line_pos)
798 break
799 elif line == '':
800 break
801 self._toc = dict(enumerate(zip(starts, stops)))
802 self._next_key = len(self._toc)
803 self._file.seek(0, 2)
804 self._file_length = self._file.tell()
807 class MH(Mailbox):
808 """An MH mailbox."""
810 def __init__(self, path, factory=None, create=True):
811 """Initialize an MH instance."""
812 Mailbox.__init__(self, path, factory, create)
813 if not os.path.exists(self._path):
814 if create:
815 os.mkdir(self._path, 0700)
816 os.close(os.open(os.path.join(self._path, '.mh_sequences'),
817 os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0600))
818 else:
819 raise NoSuchMailboxError(self._path)
820 self._locked = False
822 def add(self, message):
823 """Add message and return assigned key."""
824 keys = self.keys()
825 if len(keys) == 0:
826 new_key = 1
827 else:
828 new_key = max(keys) + 1
829 new_path = os.path.join(self._path, str(new_key))
830 f = _create_carefully(new_path)
831 try:
832 if self._locked:
833 _lock_file(f)
834 try:
835 self._dump_message(message, f)
836 if isinstance(message, MHMessage):
837 self._dump_sequences(message, new_key)
838 finally:
839 if self._locked:
840 _unlock_file(f)
841 finally:
842 _sync_close(f)
843 return new_key
845 def remove(self, key):
846 """Remove the keyed message; raise KeyError if it doesn't exist."""
847 path = os.path.join(self._path, str(key))
848 try:
849 f = open(path, 'rb+')
850 except IOError, e:
851 if e.errno == errno.ENOENT:
852 raise KeyError('No message with key: %s' % key)
853 else:
854 raise
855 try:
856 if self._locked:
857 _lock_file(f)
858 try:
859 f.close()
860 os.remove(os.path.join(self._path, str(key)))
861 finally:
862 if self._locked:
863 _unlock_file(f)
864 finally:
865 f.close()
867 def __setitem__(self, key, message):
868 """Replace the keyed message; raise KeyError if it doesn't exist."""
869 path = os.path.join(self._path, str(key))
870 try:
871 f = open(path, 'rb+')
872 except IOError, e:
873 if e.errno == errno.ENOENT:
874 raise KeyError('No message with key: %s' % key)
875 else:
876 raise
877 try:
878 if self._locked:
879 _lock_file(f)
880 try:
881 os.close(os.open(path, os.O_WRONLY | os.O_TRUNC))
882 self._dump_message(message, f)
883 if isinstance(message, MHMessage):
884 self._dump_sequences(message, key)
885 finally:
886 if self._locked:
887 _unlock_file(f)
888 finally:
889 _sync_close(f)
891 def get_message(self, key):
892 """Return a Message representation or raise a KeyError."""
893 try:
894 if self._locked:
895 f = open(os.path.join(self._path, str(key)), 'r+')
896 else:
897 f = open(os.path.join(self._path, str(key)), 'r')
898 except IOError, e:
899 if e.errno == errno.ENOENT:
900 raise KeyError('No message with key: %s' % key)
901 else:
902 raise
903 try:
904 if self._locked:
905 _lock_file(f)
906 try:
907 msg = MHMessage(f)
908 finally:
909 if self._locked:
910 _unlock_file(f)
911 finally:
912 f.close()
913 for name, key_list in self.get_sequences():
914 if key in key_list:
915 msg.add_sequence(name)
916 return msg
918 def get_string(self, key):
919 """Return a string representation or raise a KeyError."""
920 try:
921 if self._locked:
922 f = open(os.path.join(self._path, str(key)), 'r+')
923 else:
924 f = open(os.path.join(self._path, str(key)), 'r')
925 except IOError, e:
926 if e.errno == errno.ENOENT:
927 raise KeyError('No message with key: %s' % key)
928 else:
929 raise
930 try:
931 if self._locked:
932 _lock_file(f)
933 try:
934 return f.read()
935 finally:
936 if self._locked:
937 _unlock_file(f)
938 finally:
939 f.close()
941 def get_file(self, key):
942 """Return a file-like representation or raise a KeyError."""
943 try:
944 f = open(os.path.join(self._path, str(key)), 'rb')
945 except IOError, e:
946 if e.errno == errno.ENOENT:
947 raise KeyError('No message with key: %s' % key)
948 else:
949 raise
950 return _ProxyFile(f)
952 def iterkeys(self):
953 """Return an iterator over keys."""
954 return iter(sorted(int(entry) for entry in os.listdir(self._path)
955 if entry.isdigit()))
957 def has_key(self, key):
958 """Return True if the keyed message exists, False otherwise."""
959 return os.path.exists(os.path.join(self._path, str(key)))
961 def __len__(self):
962 """Return a count of messages in the mailbox."""
963 return len(list(self.iterkeys()))
965 def lock(self):
966 """Lock the mailbox."""
967 if not self._locked:
968 self._file = open(os.path.join(self._path, '.mh_sequences'), 'rb+')
969 _lock_file(self._file)
970 self._locked = True
972 def unlock(self):
973 """Unlock the mailbox if it is locked."""
974 if self._locked:
975 _unlock_file(self._file)
976 _sync_close(self._file)
977 del self._file
978 self._locked = False
980 def flush(self):
981 """Write any pending changes to the disk."""
982 return
984 def close(self):
985 """Flush and close the mailbox."""
986 if self._locked:
987 self.unlock()
989 def list_folders(self):
990 """Return a list of folder names."""
991 result = []
992 for entry in os.listdir(self._path):
993 if os.path.isdir(os.path.join(self._path, entry)):
994 result.append(entry)
995 return result
997 def get_folder(self, folder):
998 """Return an MH instance for the named folder."""
999 return MH(os.path.join(self._path, folder),
1000 factory=self._factory, create=False)
1002 def add_folder(self, folder):
1003 """Create a folder and return an MH instance representing it."""
1004 return MH(os.path.join(self._path, folder),
1005 factory=self._factory)
1007 def remove_folder(self, folder):
1008 """Delete the named folder, which must be empty."""
1009 path = os.path.join(self._path, folder)
1010 entries = os.listdir(path)
1011 if entries == ['.mh_sequences']:
1012 os.remove(os.path.join(path, '.mh_sequences'))
1013 elif entries == []:
1014 pass
1015 else:
1016 raise NotEmptyError('Folder not empty: %s' % self._path)
1017 os.rmdir(path)
1019 def get_sequences(self):
1020 """Return a name-to-key-list dictionary to define each sequence."""
1021 results = {}
1022 f = open(os.path.join(self._path, '.mh_sequences'), 'r')
1023 try:
1024 all_keys = set(self.keys())
1025 for line in f:
1026 try:
1027 name, contents = line.split(':')
1028 keys = set()
1029 for spec in contents.split():
1030 if spec.isdigit():
1031 keys.add(int(spec))
1032 else:
1033 start, stop = (int(x) for x in spec.split('-'))
1034 keys.update(range(start, stop + 1))
1035 results[name] = [key for key in sorted(keys) \
1036 if key in all_keys]
1037 if len(results[name]) == 0:
1038 del results[name]
1039 except ValueError:
1040 raise FormatError('Invalid sequence specification: %s' %
1041 line.rstrip())
1042 finally:
1043 f.close()
1044 return results
1046 def set_sequences(self, sequences):
1047 """Set sequences using the given name-to-key-list dictionary."""
1048 f = open(os.path.join(self._path, '.mh_sequences'), 'r+')
1049 try:
1050 os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC))
1051 for name, keys in sequences.iteritems():
1052 if len(keys) == 0:
1053 continue
1054 f.write('%s:' % name)
1055 prev = None
1056 completing = False
1057 for key in sorted(set(keys)):
1058 if key - 1 == prev:
1059 if not completing:
1060 completing = True
1061 f.write('-')
1062 elif completing:
1063 completing = False
1064 f.write('%s %s' % (prev, key))
1065 else:
1066 f.write(' %s' % key)
1067 prev = key
1068 if completing:
1069 f.write(str(prev) + '\n')
1070 else:
1071 f.write('\n')
1072 finally:
1073 _sync_close(f)
1075 def pack(self):
1076 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1077 sequences = self.get_sequences()
1078 prev = 0
1079 changes = []
1080 for key in self.iterkeys():
1081 if key - 1 != prev:
1082 changes.append((key, prev + 1))
1083 if hasattr(os, 'link'):
1084 os.link(os.path.join(self._path, str(key)),
1085 os.path.join(self._path, str(prev + 1)))
1086 os.unlink(os.path.join(self._path, str(key)))
1087 else:
1088 os.rename(os.path.join(self._path, str(key)),
1089 os.path.join(self._path, str(prev + 1)))
1090 prev += 1
1091 self._next_key = prev + 1
1092 if len(changes) == 0:
1093 return
1094 for name, key_list in sequences.items():
1095 for old, new in changes:
1096 if old in key_list:
1097 key_list[key_list.index(old)] = new
1098 self.set_sequences(sequences)
1100 def _dump_sequences(self, message, key):
1101 """Inspect a new MHMessage and update sequences appropriately."""
1102 pending_sequences = message.get_sequences()
1103 all_sequences = self.get_sequences()
1104 for name, key_list in all_sequences.iteritems():
1105 if name in pending_sequences:
1106 key_list.append(key)
1107 elif key in key_list:
1108 del key_list[key_list.index(key)]
1109 for sequence in pending_sequences:
1110 if sequence not in all_sequences:
1111 all_sequences[sequence] = [key]
1112 self.set_sequences(all_sequences)
1115 class Babyl(_singlefileMailbox):
1116 """An Rmail-style Babyl mailbox."""
1118 _special_labels = frozenset(('unseen', 'deleted', 'filed', 'answered',
1119 'forwarded', 'edited', 'resent'))
1121 def __init__(self, path, factory=None, create=True):
1122 """Initialize a Babyl mailbox."""
1123 _singlefileMailbox.__init__(self, path, factory, create)
1124 self._labels = {}
1126 def add(self, message):
1127 """Add message and return assigned key."""
1128 key = _singlefileMailbox.add(self, message)
1129 if isinstance(message, BabylMessage):
1130 self._labels[key] = message.get_labels()
1131 return key
1133 def remove(self, key):
1134 """Remove the keyed message; raise KeyError if it doesn't exist."""
1135 _singlefileMailbox.remove(self, key)
1136 if key in self._labels:
1137 del self._labels[key]
1139 def __setitem__(self, key, message):
1140 """Replace the keyed message; raise KeyError if it doesn't exist."""
1141 _singlefileMailbox.__setitem__(self, key, message)
1142 if isinstance(message, BabylMessage):
1143 self._labels[key] = message.get_labels()
1145 def get_message(self, key):
1146 """Return a Message representation or raise a KeyError."""
1147 start, stop = self._lookup(key)
1148 self._file.seek(start)
1149 self._file.readline() # Skip '1,' line specifying labels.
1150 original_headers = StringIO.StringIO()
1151 while True:
1152 line = self._file.readline()
1153 if line == '*** EOOH ***' + os.linesep or line == '':
1154 break
1155 original_headers.write(line.replace(os.linesep, '\n'))
1156 visible_headers = StringIO.StringIO()
1157 while True:
1158 line = self._file.readline()
1159 if line == os.linesep or line == '':
1160 break
1161 visible_headers.write(line.replace(os.linesep, '\n'))
1162 body = self._file.read(stop - self._file.tell()).replace(os.linesep,
1163 '\n')
1164 msg = BabylMessage(original_headers.getvalue() + body)
1165 msg.set_visible(visible_headers.getvalue())
1166 if key in self._labels:
1167 msg.set_labels(self._labels[key])
1168 return msg
1170 def get_string(self, key):
1171 """Return a string representation or raise a KeyError."""
1172 start, stop = self._lookup(key)
1173 self._file.seek(start)
1174 self._file.readline() # Skip '1,' line specifying labels.
1175 original_headers = StringIO.StringIO()
1176 while True:
1177 line = self._file.readline()
1178 if line == '*** EOOH ***' + os.linesep or line == '':
1179 break
1180 original_headers.write(line.replace(os.linesep, '\n'))
1181 while True:
1182 line = self._file.readline()
1183 if line == os.linesep or line == '':
1184 break
1185 return original_headers.getvalue() + \
1186 self._file.read(stop - self._file.tell()).replace(os.linesep,
1187 '\n')
1189 def get_file(self, key):
1190 """Return a file-like representation or raise a KeyError."""
1191 return StringIO.StringIO(self.get_string(key).replace('\n',
1192 os.linesep))
1194 def get_labels(self):
1195 """Return a list of user-defined labels in the mailbox."""
1196 self._lookup()
1197 labels = set()
1198 for label_list in self._labels.values():
1199 labels.update(label_list)
1200 labels.difference_update(self._special_labels)
1201 return list(labels)
1203 def _generate_toc(self):
1204 """Generate key-to-(start, stop) table of contents."""
1205 starts, stops = [], []
1206 self._file.seek(0)
1207 next_pos = 0
1208 label_lists = []
1209 while True:
1210 line_pos = next_pos
1211 line = self._file.readline()
1212 next_pos = self._file.tell()
1213 if line == '\037\014' + os.linesep:
1214 if len(stops) < len(starts):
1215 stops.append(line_pos - len(os.linesep))
1216 starts.append(next_pos)
1217 labels = [label.strip() for label
1218 in self._file.readline()[1:].split(',')
1219 if label.strip() != '']
1220 label_lists.append(labels)
1221 elif line == '\037' or line == '\037' + os.linesep:
1222 if len(stops) < len(starts):
1223 stops.append(line_pos - len(os.linesep))
1224 elif line == '':
1225 stops.append(line_pos - len(os.linesep))
1226 break
1227 self._toc = dict(enumerate(zip(starts, stops)))
1228 self._labels = dict(enumerate(label_lists))
1229 self._next_key = len(self._toc)
1230 self._file.seek(0, 2)
1231 self._file_length = self._file.tell()
1233 def _pre_mailbox_hook(self, f):
1234 """Called before writing the mailbox to file f."""
1235 f.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1236 (os.linesep, os.linesep, ','.join(self.get_labels()),
1237 os.linesep))
1239 def _pre_message_hook(self, f):
1240 """Called before writing each message to file f."""
1241 f.write('\014' + os.linesep)
1243 def _post_message_hook(self, f):
1244 """Called after writing each message to file f."""
1245 f.write(os.linesep + '\037')
1247 def _install_message(self, message):
1248 """Write message contents and return (start, stop)."""
1249 start = self._file.tell()
1250 if isinstance(message, BabylMessage):
1251 special_labels = []
1252 labels = []
1253 for label in message.get_labels():
1254 if label in self._special_labels:
1255 special_labels.append(label)
1256 else:
1257 labels.append(label)
1258 self._file.write('1')
1259 for label in special_labels:
1260 self._file.write(', ' + label)
1261 self._file.write(',,')
1262 for label in labels:
1263 self._file.write(' ' + label + ',')
1264 self._file.write(os.linesep)
1265 else:
1266 self._file.write('1,,' + os.linesep)
1267 if isinstance(message, email.message.Message):
1268 orig_buffer = StringIO.StringIO()
1269 orig_generator = email.generator.Generator(orig_buffer, False, 0)
1270 orig_generator.flatten(message)
1271 orig_buffer.seek(0)
1272 while True:
1273 line = orig_buffer.readline()
1274 self._file.write(line.replace('\n', os.linesep))
1275 if line == '\n' or line == '':
1276 break
1277 self._file.write('*** EOOH ***' + os.linesep)
1278 if isinstance(message, BabylMessage):
1279 vis_buffer = StringIO.StringIO()
1280 vis_generator = email.generator.Generator(vis_buffer, False, 0)
1281 vis_generator.flatten(message.get_visible())
1282 while True:
1283 line = vis_buffer.readline()
1284 self._file.write(line.replace('\n', os.linesep))
1285 if line == '\n' or line == '':
1286 break
1287 else:
1288 orig_buffer.seek(0)
1289 while True:
1290 line = orig_buffer.readline()
1291 self._file.write(line.replace('\n', os.linesep))
1292 if line == '\n' or line == '':
1293 break
1294 while True:
1295 buffer = orig_buffer.read(4096) # Buffer size is arbitrary.
1296 if buffer == '':
1297 break
1298 self._file.write(buffer.replace('\n', os.linesep))
1299 elif isinstance(message, str):
1300 body_start = message.find('\n\n') + 2
1301 if body_start - 2 != -1:
1302 self._file.write(message[:body_start].replace('\n',
1303 os.linesep))
1304 self._file.write('*** EOOH ***' + os.linesep)
1305 self._file.write(message[:body_start].replace('\n',
1306 os.linesep))
1307 self._file.write(message[body_start:].replace('\n',
1308 os.linesep))
1309 else:
1310 self._file.write('*** EOOH ***' + os.linesep + os.linesep)
1311 self._file.write(message.replace('\n', os.linesep))
1312 elif hasattr(message, 'readline'):
1313 original_pos = message.tell()
1314 first_pass = True
1315 while True:
1316 line = message.readline()
1317 self._file.write(line.replace('\n', os.linesep))
1318 if line == '\n' or line == '':
1319 self._file.write('*** EOOH ***' + os.linesep)
1320 if first_pass:
1321 first_pass = False
1322 message.seek(original_pos)
1323 else:
1324 break
1325 while True:
1326 buffer = message.read(4096) # Buffer size is arbitrary.
1327 if buffer == '':
1328 break
1329 self._file.write(buffer.replace('\n', os.linesep))
1330 else:
1331 raise TypeError('Invalid message type: %s' % type(message))
1332 stop = self._file.tell()
1333 return (start, stop)
1336 class Message(email.message.Message):
1337 """Message with mailbox-format-specific properties."""
1339 def __init__(self, message=None):
1340 """Initialize a Message instance."""
1341 if isinstance(message, email.message.Message):
1342 self._become_message(copy.deepcopy(message))
1343 if isinstance(message, Message):
1344 message._explain_to(self)
1345 elif isinstance(message, str):
1346 self._become_message(email.message_from_string(message))
1347 elif hasattr(message, "read"):
1348 self._become_message(email.message_from_file(message))
1349 elif message is None:
1350 email.message.Message.__init__(self)
1351 else:
1352 raise TypeError('Invalid message type: %s' % type(message))
1354 def _become_message(self, message):
1355 """Assume the non-format-specific state of message."""
1356 for name in ('_headers', '_unixfrom', '_payload', '_charset',
1357 'preamble', 'epilogue', 'defects', '_default_type'):
1358 self.__dict__[name] = message.__dict__[name]
1360 def _explain_to(self, message):
1361 """Copy format-specific state to message insofar as possible."""
1362 if isinstance(message, Message):
1363 return # There's nothing format-specific to explain.
1364 else:
1365 raise TypeError('Cannot convert to specified type')
1368 class MaildirMessage(Message):
1369 """Message with Maildir-specific properties."""
1371 def __init__(self, message=None):
1372 """Initialize a MaildirMessage instance."""
1373 self._subdir = 'new'
1374 self._info = ''
1375 self._date = time.time()
1376 Message.__init__(self, message)
1378 def get_subdir(self):
1379 """Return 'new' or 'cur'."""
1380 return self._subdir
1382 def set_subdir(self, subdir):
1383 """Set subdir to 'new' or 'cur'."""
1384 if subdir == 'new' or subdir == 'cur':
1385 self._subdir = subdir
1386 else:
1387 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir)
1389 def get_flags(self):
1390 """Return as a string the flags that are set."""
1391 if self._info.startswith('2,'):
1392 return self._info[2:]
1393 else:
1394 return ''
1396 def set_flags(self, flags):
1397 """Set the given flags and unset all others."""
1398 self._info = '2,' + ''.join(sorted(flags))
1400 def add_flag(self, flag):
1401 """Set the given flag(s) without changing others."""
1402 self.set_flags(''.join(set(self.get_flags()) | set(flag)))
1404 def remove_flag(self, flag):
1405 """Unset the given string flag(s) without changing others."""
1406 if self.get_flags() != '':
1407 self.set_flags(''.join(set(self.get_flags()) - set(flag)))
1409 def get_date(self):
1410 """Return delivery date of message, in seconds since the epoch."""
1411 return self._date
1413 def set_date(self, date):
1414 """Set delivery date of message, in seconds since the epoch."""
1415 try:
1416 self._date = float(date)
1417 except ValueError:
1418 raise TypeError("can't convert to float: %s" % date)
1420 def get_info(self):
1421 """Get the message's "info" as a string."""
1422 return self._info
1424 def set_info(self, info):
1425 """Set the message's "info" string."""
1426 if isinstance(info, str):
1427 self._info = info
1428 else:
1429 raise TypeError('info must be a string: %s' % type(info))
1431 def _explain_to(self, message):
1432 """Copy Maildir-specific state to message insofar as possible."""
1433 if isinstance(message, MaildirMessage):
1434 message.set_flags(self.get_flags())
1435 message.set_subdir(self.get_subdir())
1436 message.set_date(self.get_date())
1437 elif isinstance(message, _mboxMMDFMessage):
1438 flags = set(self.get_flags())
1439 if 'S' in flags:
1440 message.add_flag('R')
1441 if self.get_subdir() == 'cur':
1442 message.add_flag('O')
1443 if 'T' in flags:
1444 message.add_flag('D')
1445 if 'F' in flags:
1446 message.add_flag('F')
1447 if 'R' in flags:
1448 message.add_flag('A')
1449 message.set_from('MAILER-DAEMON', time.gmtime(self.get_date()))
1450 elif isinstance(message, MHMessage):
1451 flags = set(self.get_flags())
1452 if 'S' not in flags:
1453 message.add_sequence('unseen')
1454 if 'R' in flags:
1455 message.add_sequence('replied')
1456 if 'F' in flags:
1457 message.add_sequence('flagged')
1458 elif isinstance(message, BabylMessage):
1459 flags = set(self.get_flags())
1460 if 'S' not in flags:
1461 message.add_label('unseen')
1462 if 'T' in flags:
1463 message.add_label('deleted')
1464 if 'R' in flags:
1465 message.add_label('answered')
1466 if 'P' in flags:
1467 message.add_label('forwarded')
1468 elif isinstance(message, Message):
1469 pass
1470 else:
1471 raise TypeError('Cannot convert to specified type: %s' %
1472 type(message))
1475 class _mboxMMDFMessage(Message):
1476 """Message with mbox- or MMDF-specific properties."""
1478 def __init__(self, message=None):
1479 """Initialize an mboxMMDFMessage instance."""
1480 self.set_from('MAILER-DAEMON', True)
1481 if isinstance(message, email.message.Message):
1482 unixfrom = message.get_unixfrom()
1483 if unixfrom is not None and unixfrom.startswith('From '):
1484 self.set_from(unixfrom[5:])
1485 Message.__init__(self, message)
1487 def get_from(self):
1488 """Return contents of "From " line."""
1489 return self._from
1491 def set_from(self, from_, time_=None):
1492 """Set "From " line, formatting and appending time_ if specified."""
1493 if time_ is not None:
1494 if time_ is True:
1495 time_ = time.gmtime()
1496 from_ += ' ' + time.asctime(time_)
1497 self._from = from_
1499 def get_flags(self):
1500 """Return as a string the flags that are set."""
1501 return self.get('Status', '') + self.get('X-Status', '')
1503 def set_flags(self, flags):
1504 """Set the given flags and unset all others."""
1505 flags = set(flags)
1506 status_flags, xstatus_flags = '', ''
1507 for flag in ('R', 'O'):
1508 if flag in flags:
1509 status_flags += flag
1510 flags.remove(flag)
1511 for flag in ('D', 'F', 'A'):
1512 if flag in flags:
1513 xstatus_flags += flag
1514 flags.remove(flag)
1515 xstatus_flags += ''.join(sorted(flags))
1516 try:
1517 self.replace_header('Status', status_flags)
1518 except KeyError:
1519 self.add_header('Status', status_flags)
1520 try:
1521 self.replace_header('X-Status', xstatus_flags)
1522 except KeyError:
1523 self.add_header('X-Status', xstatus_flags)
1525 def add_flag(self, flag):
1526 """Set the given flag(s) without changing others."""
1527 self.set_flags(''.join(set(self.get_flags()) | set(flag)))
1529 def remove_flag(self, flag):
1530 """Unset the given string flag(s) without changing others."""
1531 if 'Status' in self or 'X-Status' in self:
1532 self.set_flags(''.join(set(self.get_flags()) - set(flag)))
1534 def _explain_to(self, message):
1535 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1536 if isinstance(message, MaildirMessage):
1537 flags = set(self.get_flags())
1538 if 'O' in flags:
1539 message.set_subdir('cur')
1540 if 'F' in flags:
1541 message.add_flag('F')
1542 if 'A' in flags:
1543 message.add_flag('R')
1544 if 'R' in flags:
1545 message.add_flag('S')
1546 if 'D' in flags:
1547 message.add_flag('T')
1548 del message['status']
1549 del message['x-status']
1550 maybe_date = ' '.join(self.get_from().split()[-5:])
1551 try:
1552 message.set_date(calendar.timegm(time.strptime(maybe_date,
1553 '%a %b %d %H:%M:%S %Y')))
1554 except (ValueError, OverflowError):
1555 pass
1556 elif isinstance(message, _mboxMMDFMessage):
1557 message.set_flags(self.get_flags())
1558 message.set_from(self.get_from())
1559 elif isinstance(message, MHMessage):
1560 flags = set(self.get_flags())
1561 if 'R' not in flags:
1562 message.add_sequence('unseen')
1563 if 'A' in flags:
1564 message.add_sequence('replied')
1565 if 'F' in flags:
1566 message.add_sequence('flagged')
1567 del message['status']
1568 del message['x-status']
1569 elif isinstance(message, BabylMessage):
1570 flags = set(self.get_flags())
1571 if 'R' not in flags:
1572 message.add_label('unseen')
1573 if 'D' in flags:
1574 message.add_label('deleted')
1575 if 'A' in flags:
1576 message.add_label('answered')
1577 del message['status']
1578 del message['x-status']
1579 elif isinstance(message, Message):
1580 pass
1581 else:
1582 raise TypeError('Cannot convert to specified type: %s' %
1583 type(message))
1586 class mboxMessage(_mboxMMDFMessage):
1587 """Message with mbox-specific properties."""
1590 class MHMessage(Message):
1591 """Message with MH-specific properties."""
1593 def __init__(self, message=None):
1594 """Initialize an MHMessage instance."""
1595 self._sequences = []
1596 Message.__init__(self, message)
1598 def get_sequences(self):
1599 """Return a list of sequences that include the message."""
1600 return self._sequences[:]
1602 def set_sequences(self, sequences):
1603 """Set the list of sequences that include the message."""
1604 self._sequences = list(sequences)
1606 def add_sequence(self, sequence):
1607 """Add sequence to list of sequences including the message."""
1608 if isinstance(sequence, str):
1609 if not sequence in self._sequences:
1610 self._sequences.append(sequence)
1611 else:
1612 raise TypeError('sequence must be a string: %s' % type(sequence))
1614 def remove_sequence(self, sequence):
1615 """Remove sequence from the list of sequences including the message."""
1616 try:
1617 self._sequences.remove(sequence)
1618 except ValueError:
1619 pass
1621 def _explain_to(self, message):
1622 """Copy MH-specific state to message insofar as possible."""
1623 if isinstance(message, MaildirMessage):
1624 sequences = set(self.get_sequences())
1625 if 'unseen' in sequences:
1626 message.set_subdir('cur')
1627 else:
1628 message.set_subdir('cur')
1629 message.add_flag('S')
1630 if 'flagged' in sequences:
1631 message.add_flag('F')
1632 if 'replied' in sequences:
1633 message.add_flag('R')
1634 elif isinstance(message, _mboxMMDFMessage):
1635 sequences = set(self.get_sequences())
1636 if 'unseen' not in sequences:
1637 message.add_flag('RO')
1638 else:
1639 message.add_flag('O')
1640 if 'flagged' in sequences:
1641 message.add_flag('F')
1642 if 'replied' in sequences:
1643 message.add_flag('A')
1644 elif isinstance(message, MHMessage):
1645 for sequence in self.get_sequences():
1646 message.add_sequence(sequence)
1647 elif isinstance(message, BabylMessage):
1648 sequences = set(self.get_sequences())
1649 if 'unseen' in sequences:
1650 message.add_label('unseen')
1651 if 'replied' in sequences:
1652 message.add_label('answered')
1653 elif isinstance(message, Message):
1654 pass
1655 else:
1656 raise TypeError('Cannot convert to specified type: %s' %
1657 type(message))
1660 class BabylMessage(Message):
1661 """Message with Babyl-specific properties."""
1663 def __init__(self, message=None):
1664 """Initialize an BabylMessage instance."""
1665 self._labels = []
1666 self._visible = Message()
1667 Message.__init__(self, message)
1669 def get_labels(self):
1670 """Return a list of labels on the message."""
1671 return self._labels[:]
1673 def set_labels(self, labels):
1674 """Set the list of labels on the message."""
1675 self._labels = list(labels)
1677 def add_label(self, label):
1678 """Add label to list of labels on the message."""
1679 if isinstance(label, str):
1680 if label not in self._labels:
1681 self._labels.append(label)
1682 else:
1683 raise TypeError('label must be a string: %s' % type(label))
1685 def remove_label(self, label):
1686 """Remove label from the list of labels on the message."""
1687 try:
1688 self._labels.remove(label)
1689 except ValueError:
1690 pass
1692 def get_visible(self):
1693 """Return a Message representation of visible headers."""
1694 return Message(self._visible)
1696 def set_visible(self, visible):
1697 """Set the Message representation of visible headers."""
1698 self._visible = Message(visible)
1700 def update_visible(self):
1701 """Update and/or sensibly generate a set of visible headers."""
1702 for header in self._visible.keys():
1703 if header in self:
1704 self._visible.replace_header(header, self[header])
1705 else:
1706 del self._visible[header]
1707 for header in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1708 if header in self and header not in self._visible:
1709 self._visible[header] = self[header]
1711 def _explain_to(self, message):
1712 """Copy Babyl-specific state to message insofar as possible."""
1713 if isinstance(message, MaildirMessage):
1714 labels = set(self.get_labels())
1715 if 'unseen' in labels:
1716 message.set_subdir('cur')
1717 else:
1718 message.set_subdir('cur')
1719 message.add_flag('S')
1720 if 'forwarded' in labels or 'resent' in labels:
1721 message.add_flag('P')
1722 if 'answered' in labels:
1723 message.add_flag('R')
1724 if 'deleted' in labels:
1725 message.add_flag('T')
1726 elif isinstance(message, _mboxMMDFMessage):
1727 labels = set(self.get_labels())
1728 if 'unseen' not in labels:
1729 message.add_flag('RO')
1730 else:
1731 message.add_flag('O')
1732 if 'deleted' in labels:
1733 message.add_flag('D')
1734 if 'answered' in labels:
1735 message.add_flag('A')
1736 elif isinstance(message, MHMessage):
1737 labels = set(self.get_labels())
1738 if 'unseen' in labels:
1739 message.add_sequence('unseen')
1740 if 'answered' in labels:
1741 message.add_sequence('replied')
1742 elif isinstance(message, BabylMessage):
1743 message.set_visible(self.get_visible())
1744 for label in self.get_labels():
1745 message.add_label(label)
1746 elif isinstance(message, Message):
1747 pass
1748 else:
1749 raise TypeError('Cannot convert to specified type: %s' %
1750 type(message))
1753 class MMDFMessage(_mboxMMDFMessage):
1754 """Message with MMDF-specific properties."""
1757 class _ProxyFile:
1758 """A read-only wrapper of a file."""
1760 def __init__(self, f, pos=None):
1761 """Initialize a _ProxyFile."""
1762 self._file = f
1763 if pos is None:
1764 self._pos = f.tell()
1765 else:
1766 self._pos = pos
1768 def read(self, size=None):
1769 """Read bytes."""
1770 return self._read(size, self._file.read)
1772 def readline(self, size=None):
1773 """Read a line."""
1774 return self._read(size, self._file.readline)
1776 def readlines(self, sizehint=None):
1777 """Read multiple lines."""
1778 result = []
1779 for line in self:
1780 result.append(line)
1781 if sizehint is not None:
1782 sizehint -= len(line)
1783 if sizehint <= 0:
1784 break
1785 return result
1787 def __iter__(self):
1788 """Iterate over lines."""
1789 return iter(self.readline, "")
1791 def tell(self):
1792 """Return the position."""
1793 return self._pos
1795 def seek(self, offset, whence=0):
1796 """Change position."""
1797 if whence == 1:
1798 self._file.seek(self._pos)
1799 self._file.seek(offset, whence)
1800 self._pos = self._file.tell()
1802 def close(self):
1803 """Close the file."""
1804 del self._file
1806 def _read(self, size, read_method):
1807 """Read size bytes using read_method."""
1808 if size is None:
1809 size = -1
1810 self._file.seek(self._pos)
1811 result = read_method(size)
1812 self._pos = self._file.tell()
1813 return result
1816 class _PartialFile(_ProxyFile):
1817 """A read-only wrapper of part of a file."""
1819 def __init__(self, f, start=None, stop=None):
1820 """Initialize a _PartialFile."""
1821 _ProxyFile.__init__(self, f, start)
1822 self._start = start
1823 self._stop = stop
1825 def tell(self):
1826 """Return the position with respect to start."""
1827 return _ProxyFile.tell(self) - self._start
1829 def seek(self, offset, whence=0):
1830 """Change position, possibly with respect to start or stop."""
1831 if whence == 0:
1832 self._pos = self._start
1833 whence = 1
1834 elif whence == 2:
1835 self._pos = self._stop
1836 whence = 1
1837 _ProxyFile.seek(self, offset, whence)
1839 def _read(self, size, read_method):
1840 """Read size bytes using read_method, honoring start and stop."""
1841 remaining = self._stop - self._pos
1842 if remaining <= 0:
1843 return ''
1844 if size is None or size < 0 or size > remaining:
1845 size = remaining
1846 return _ProxyFile._read(self, size, read_method)
1849 def _lock_file(f, dotlock=True):
1850 """Lock file f using lockf and dot locking."""
1851 dotlock_done = False
1852 try:
1853 if fcntl:
1854 try:
1855 fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
1856 except IOError, e:
1857 if e.errno in (errno.EAGAIN, errno.EACCES):
1858 raise ExternalClashError('lockf: lock unavailable: %s' %
1859 f.name)
1860 else:
1861 raise
1862 if dotlock:
1863 try:
1864 pre_lock = _create_temporary(f.name + '.lock')
1865 pre_lock.close()
1866 except IOError, e:
1867 if e.errno == errno.EACCES:
1868 return # Without write access, just skip dotlocking.
1869 else:
1870 raise
1871 try:
1872 if hasattr(os, 'link'):
1873 os.link(pre_lock.name, f.name + '.lock')
1874 dotlock_done = True
1875 os.unlink(pre_lock.name)
1876 else:
1877 os.rename(pre_lock.name, f.name + '.lock')
1878 dotlock_done = True
1879 except OSError, e:
1880 if e.errno == errno.EEXIST or \
1881 (os.name == 'os2' and e.errno == errno.EACCES):
1882 os.remove(pre_lock.name)
1883 raise ExternalClashError('dot lock unavailable: %s' %
1884 f.name)
1885 else:
1886 raise
1887 except:
1888 if fcntl:
1889 fcntl.lockf(f, fcntl.LOCK_UN)
1890 if dotlock_done:
1891 os.remove(f.name + '.lock')
1892 raise
1894 def _unlock_file(f):
1895 """Unlock file f using lockf and dot locking."""
1896 if fcntl:
1897 fcntl.lockf(f, fcntl.LOCK_UN)
1898 if os.path.exists(f.name + '.lock'):
1899 os.remove(f.name + '.lock')
1901 def _create_carefully(path):
1902 """Create a file if it doesn't exist and open for reading and writing."""
1903 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
1904 try:
1905 return open(path, 'rb+')
1906 finally:
1907 os.close(fd)
1909 def _create_temporary(path):
1910 """Create a temp file based on path and open for reading and writing."""
1911 return _create_carefully('%s.%s.%s.%s' % (path, int(time.time()),
1912 socket.gethostname(),
1913 os.getpid()))
1915 def _sync_flush(f):
1916 """Ensure changes to file f are physically on disk."""
1917 f.flush()
1918 if hasattr(os, 'fsync'):
1919 os.fsync(f.fileno())
1921 def _sync_close(f):
1922 """Close file f, ensuring all changes are physically on disk."""
1923 _sync_flush(f)
1924 f.close()
1926 ## Start: classes from the original module (for backward compatibility).
1928 # Note that the Maildir class, whose name is unchanged, itself offers a next()
1929 # method for backward compatibility.
1931 class _Mailbox:
1933 def __init__(self, fp, factory=rfc822.Message):
1934 self.fp = fp
1935 self.seekp = 0
1936 self.factory = factory
1938 def __iter__(self):
1939 return iter(self.next, None)
1941 def next(self):
1942 while 1:
1943 self.fp.seek(self.seekp)
1944 try:
1945 self._search_start()
1946 except EOFError:
1947 self.seekp = self.fp.tell()
1948 return None
1949 start = self.fp.tell()
1950 self._search_end()
1951 self.seekp = stop = self.fp.tell()
1952 if start != stop:
1953 break
1954 return self.factory(_PartialFile(self.fp, start, stop))
1956 # Recommended to use PortableUnixMailbox instead!
1957 class UnixMailbox(_Mailbox):
1959 def _search_start(self):
1960 while 1:
1961 pos = self.fp.tell()
1962 line = self.fp.readline()
1963 if not line:
1964 raise EOFError
1965 if line[:5] == 'From ' and self._isrealfromline(line):
1966 self.fp.seek(pos)
1967 return
1969 def _search_end(self):
1970 self.fp.readline() # Throw away header line
1971 while 1:
1972 pos = self.fp.tell()
1973 line = self.fp.readline()
1974 if not line:
1975 return
1976 if line[:5] == 'From ' and self._isrealfromline(line):
1977 self.fp.seek(pos)
1978 return
1980 # An overridable mechanism to test for From-line-ness. You can either
1981 # specify a different regular expression or define a whole new
1982 # _isrealfromline() method. Note that this only gets called for lines
1983 # starting with the 5 characters "From ".
1985 # BAW: According to
1986 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
1987 # the only portable, reliable way to find message delimiters in a BSD (i.e
1988 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
1989 # beginning of the file, "^From .*\n". While _fromlinepattern below seems
1990 # like a good idea, in practice, there are too many variations for more
1991 # strict parsing of the line to be completely accurate.
1993 # _strict_isrealfromline() is the old version which tries to do stricter
1994 # parsing of the From_ line. _portable_isrealfromline() simply returns
1995 # true, since it's never called if the line doesn't already start with
1996 # "From ".
1998 # This algorithm, and the way it interacts with _search_start() and
1999 # _search_end() may not be completely correct, because it doesn't check
2000 # that the two characters preceding "From " are \n\n or the beginning of
2001 # the file. Fixing this would require a more extensive rewrite than is
2002 # necessary. For convenience, we've added a PortableUnixMailbox class
2003 # which does no checking of the format of the 'From' line.
2005 _fromlinepattern = (r"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
2006 r"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
2007 r"[^\s]*\s*"
2008 "$")
2009 _regexp = None
2011 def _strict_isrealfromline(self, line):
2012 if not self._regexp:
2013 import re
2014 self._regexp = re.compile(self._fromlinepattern)
2015 return self._regexp.match(line)
2017 def _portable_isrealfromline(self, line):
2018 return True
2020 _isrealfromline = _strict_isrealfromline
2023 class PortableUnixMailbox(UnixMailbox):
2024 _isrealfromline = UnixMailbox._portable_isrealfromline
2027 class MmdfMailbox(_Mailbox):
2029 def _search_start(self):
2030 while 1:
2031 line = self.fp.readline()
2032 if not line:
2033 raise EOFError
2034 if line[:5] == '\001\001\001\001\n':
2035 return
2037 def _search_end(self):
2038 while 1:
2039 pos = self.fp.tell()
2040 line = self.fp.readline()
2041 if not line:
2042 return
2043 if line == '\001\001\001\001\n':
2044 self.fp.seek(pos)
2045 return
2048 class MHMailbox:
2050 def __init__(self, dirname, factory=rfc822.Message):
2051 import re
2052 pat = re.compile('^[1-9][0-9]*$')
2053 self.dirname = dirname
2054 # the three following lines could be combined into:
2055 # list = map(long, filter(pat.match, os.listdir(self.dirname)))
2056 list = os.listdir(self.dirname)
2057 list = filter(pat.match, list)
2058 list = map(long, list)
2059 list.sort()
2060 # This only works in Python 1.6 or later;
2061 # before that str() added 'L':
2062 self.boxes = map(str, list)
2063 self.boxes.reverse()
2064 self.factory = factory
2066 def __iter__(self):
2067 return iter(self.next, None)
2069 def next(self):
2070 if not self.boxes:
2071 return None
2072 fn = self.boxes.pop()
2073 fp = open(os.path.join(self.dirname, fn))
2074 msg = self.factory(fp)
2075 try:
2076 msg._mh_msgno = fn
2077 except (AttributeError, TypeError):
2078 pass
2079 return msg
2082 class BabylMailbox(_Mailbox):
2084 def _search_start(self):
2085 while 1:
2086 line = self.fp.readline()
2087 if not line:
2088 raise EOFError
2089 if line == '*** EOOH ***\n':
2090 return
2092 def _search_end(self):
2093 while 1:
2094 pos = self.fp.tell()
2095 line = self.fp.readline()
2096 if not line:
2097 return
2098 if line == '\037\014\n' or line == '\037':
2099 self.fp.seek(pos)
2100 return
2102 ## End: classes from the original module (for backward compatibility).
2105 class Error(Exception):
2106 """Raised for module-specific errors."""
2108 class NoSuchMailboxError(Error):
2109 """The specified mailbox does not exist and won't be created."""
2111 class NotEmptyError(Error):
2112 """The specified mailbox is not empty and deletion was requested."""
2114 class ExternalClashError(Error):
2115 """Another process caused an action to fail."""
2117 class FormatError(Error):
2118 """A file appears to have an invalid format."""