Change to flush and close logic to fix #1760556.
[python.git] / Lib / mailbox.py
blob3f7a12a6e67318773671daf38db1c0ad2ef3f7fb
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 msg = MaildirMessage(f)
319 finally:
320 f.close()
321 subdir, name = os.path.split(subpath)
322 msg.set_subdir(subdir)
323 if self.colon in name:
324 msg.set_info(name.split(self.colon)[-1])
325 msg.set_date(os.path.getmtime(os.path.join(self._path, subpath)))
326 return msg
328 def get_string(self, key):
329 """Return a string representation or raise a KeyError."""
330 f = open(os.path.join(self._path, self._lookup(key)), 'r')
331 try:
332 return f.read()
333 finally:
334 f.close()
336 def get_file(self, key):
337 """Return a file-like representation or raise a KeyError."""
338 f = open(os.path.join(self._path, self._lookup(key)), 'rb')
339 return _ProxyFile(f)
341 def iterkeys(self):
342 """Return an iterator over keys."""
343 self._refresh()
344 for key in self._toc:
345 try:
346 self._lookup(key)
347 except KeyError:
348 continue
349 yield key
351 def has_key(self, key):
352 """Return True if the keyed message exists, False otherwise."""
353 self._refresh()
354 return key in self._toc
356 def __len__(self):
357 """Return a count of messages in the mailbox."""
358 self._refresh()
359 return len(self._toc)
361 def flush(self):
362 """Write any pending changes to disk."""
363 return # Maildir changes are always written immediately.
365 def lock(self):
366 """Lock the mailbox."""
367 return
369 def unlock(self):
370 """Unlock the mailbox if it is locked."""
371 return
373 def close(self):
374 """Flush and close the mailbox."""
375 return
377 def list_folders(self):
378 """Return a list of folder names."""
379 result = []
380 for entry in os.listdir(self._path):
381 if len(entry) > 1 and entry[0] == '.' and \
382 os.path.isdir(os.path.join(self._path, entry)):
383 result.append(entry[1:])
384 return result
386 def get_folder(self, folder):
387 """Return a Maildir instance for the named folder."""
388 return Maildir(os.path.join(self._path, '.' + folder),
389 factory=self._factory,
390 create=False)
392 def add_folder(self, folder):
393 """Create a folder and return a Maildir instance representing it."""
394 path = os.path.join(self._path, '.' + folder)
395 result = Maildir(path, factory=self._factory)
396 maildirfolder_path = os.path.join(path, 'maildirfolder')
397 if not os.path.exists(maildirfolder_path):
398 os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY))
399 return result
401 def remove_folder(self, folder):
402 """Delete the named folder, which must be empty."""
403 path = os.path.join(self._path, '.' + folder)
404 for entry in os.listdir(os.path.join(path, 'new')) + \
405 os.listdir(os.path.join(path, 'cur')):
406 if len(entry) < 1 or entry[0] != '.':
407 raise NotEmptyError('Folder contains message(s): %s' % folder)
408 for entry in os.listdir(path):
409 if entry != 'new' and entry != 'cur' and entry != 'tmp' and \
410 os.path.isdir(os.path.join(path, entry)):
411 raise NotEmptyError("Folder contains subdirectory '%s': %s" %
412 (folder, entry))
413 for root, dirs, files in os.walk(path, topdown=False):
414 for entry in files:
415 os.remove(os.path.join(root, entry))
416 for entry in dirs:
417 os.rmdir(os.path.join(root, entry))
418 os.rmdir(path)
420 def clean(self):
421 """Delete old files in "tmp"."""
422 now = time.time()
423 for entry in os.listdir(os.path.join(self._path, 'tmp')):
424 path = os.path.join(self._path, 'tmp', entry)
425 if now - os.path.getatime(path) > 129600: # 60 * 60 * 36
426 os.remove(path)
428 _count = 1 # This is used to generate unique file names.
430 def _create_tmp(self):
431 """Create a file in the tmp subdirectory and open and return it."""
432 now = time.time()
433 hostname = socket.gethostname()
434 if '/' in hostname:
435 hostname = hostname.replace('/', r'\057')
436 if ':' in hostname:
437 hostname = hostname.replace(':', r'\072')
438 uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(),
439 Maildir._count, hostname)
440 path = os.path.join(self._path, 'tmp', uniq)
441 try:
442 os.stat(path)
443 except OSError, e:
444 if e.errno == errno.ENOENT:
445 Maildir._count += 1
446 try:
447 return _create_carefully(path)
448 except OSError, e:
449 if e.errno != errno.EEXIST:
450 raise
451 else:
452 raise
454 # Fall through to here if stat succeeded or open raised EEXIST.
455 raise ExternalClashError('Name clash prevented file creation: %s' %
456 path)
458 def _refresh(self):
459 """Update table of contents mapping."""
460 self._toc = {}
461 for subdir in ('new', 'cur'):
462 subdir_path = os.path.join(self._path, subdir)
463 for entry in os.listdir(subdir_path):
464 p = os.path.join(subdir_path, entry)
465 if os.path.isdir(p):
466 continue
467 uniq = entry.split(self.colon)[0]
468 self._toc[uniq] = os.path.join(subdir, entry)
470 def _lookup(self, key):
471 """Use TOC to return subpath for given key, or raise a KeyError."""
472 try:
473 if os.path.exists(os.path.join(self._path, self._toc[key])):
474 return self._toc[key]
475 except KeyError:
476 pass
477 self._refresh()
478 try:
479 return self._toc[key]
480 except KeyError:
481 raise KeyError('No message with key: %s' % key)
483 # This method is for backward compatibility only.
484 def next(self):
485 """Return the next message in a one-time iteration."""
486 if not hasattr(self, '_onetime_keys'):
487 self._onetime_keys = self.iterkeys()
488 while True:
489 try:
490 return self[self._onetime_keys.next()]
491 except StopIteration:
492 return None
493 except KeyError:
494 continue
497 class _singlefileMailbox(Mailbox):
498 """A single-file mailbox."""
500 def __init__(self, path, factory=None, create=True):
501 """Initialize a single-file mailbox."""
502 Mailbox.__init__(self, path, factory, create)
503 try:
504 f = open(self._path, 'rb+')
505 except IOError, e:
506 if e.errno == errno.ENOENT:
507 if create:
508 f = open(self._path, 'wb+')
509 else:
510 raise NoSuchMailboxError(self._path)
511 elif e.errno == errno.EACCES:
512 f = open(self._path, 'rb')
513 else:
514 raise
515 self._file = f
516 self._toc = None
517 self._next_key = 0
518 self._pending = False # No changes require rewriting the file.
519 self._locked = False
520 self._file_length = None # Used to record mailbox size
522 def add(self, message):
523 """Add message and return assigned key."""
524 self._lookup()
525 self._toc[self._next_key] = self._append_message(message)
526 self._next_key += 1
527 self._pending = True
528 return self._next_key - 1
530 def remove(self, key):
531 """Remove the keyed message; raise KeyError if it doesn't exist."""
532 self._lookup(key)
533 del self._toc[key]
534 self._pending = True
536 def __setitem__(self, key, message):
537 """Replace the keyed message; raise KeyError if it doesn't exist."""
538 self._lookup(key)
539 self._toc[key] = self._append_message(message)
540 self._pending = True
542 def iterkeys(self):
543 """Return an iterator over keys."""
544 self._lookup()
545 for key in self._toc.keys():
546 yield key
548 def has_key(self, key):
549 """Return True if the keyed message exists, False otherwise."""
550 self._lookup()
551 return key in self._toc
553 def __len__(self):
554 """Return a count of messages in the mailbox."""
555 self._lookup()
556 return len(self._toc)
558 def lock(self):
559 """Lock the mailbox."""
560 if not self._locked:
561 _lock_file(self._file)
562 self._locked = True
564 def unlock(self):
565 """Unlock the mailbox if it is locked."""
566 if self._locked:
567 _unlock_file(self._file)
568 self._locked = False
570 def flush(self):
571 """Write any pending changes to disk."""
572 if not self._pending:
573 return
575 # In order to be writing anything out at all, self._toc must
576 # already have been generated (and presumably has been modified
577 # by adding or deleting an item).
578 assert self._toc is not None
580 # Check length of self._file; if it's changed, some other process
581 # has modified the mailbox since we scanned it.
582 self._file.seek(0, 2)
583 cur_len = self._file.tell()
584 if cur_len != self._file_length:
585 raise ExternalClashError('Size of mailbox file changed '
586 '(expected %i, found %i)' %
587 (self._file_length, cur_len))
589 new_file = _create_temporary(self._path)
590 try:
591 new_toc = {}
592 self._pre_mailbox_hook(new_file)
593 for key in sorted(self._toc.keys()):
594 start, stop = self._toc[key]
595 self._file.seek(start)
596 self._pre_message_hook(new_file)
597 new_start = new_file.tell()
598 while True:
599 buffer = self._file.read(min(4096,
600 stop - self._file.tell()))
601 if buffer == '':
602 break
603 new_file.write(buffer)
604 new_toc[key] = (new_start, new_file.tell())
605 self._post_message_hook(new_file)
606 except:
607 new_file.close()
608 os.remove(new_file.name)
609 raise
610 _sync_close(new_file)
611 # self._file is about to get replaced, so no need to sync.
612 self._file.close()
613 try:
614 os.rename(new_file.name, self._path)
615 except OSError, e:
616 if e.errno == errno.EEXIST or \
617 (os.name == 'os2' and e.errno == errno.EACCES):
618 os.remove(self._path)
619 os.rename(new_file.name, self._path)
620 else:
621 raise
622 self._file = open(self._path, 'rb+')
623 self._toc = new_toc
624 self._pending = False
625 if self._locked:
626 _lock_file(self._file, dotlock=False)
628 def _pre_mailbox_hook(self, f):
629 """Called before writing the mailbox to file f."""
630 return
632 def _pre_message_hook(self, f):
633 """Called before writing each message to file f."""
634 return
636 def _post_message_hook(self, f):
637 """Called after writing each message to file f."""
638 return
640 def close(self):
641 """Flush and close the mailbox."""
642 self.flush()
643 if self._locked:
644 self.unlock()
645 self._file.close() # Sync has been done by self.flush() above.
647 def _lookup(self, key=None):
648 """Return (start, stop) or raise KeyError."""
649 if self._toc is None:
650 self._generate_toc()
651 if key is not None:
652 try:
653 return self._toc[key]
654 except KeyError:
655 raise KeyError('No message with key: %s' % key)
657 def _append_message(self, message):
658 """Append message to mailbox and return (start, stop) offsets."""
659 self._file.seek(0, 2)
660 self._pre_message_hook(self._file)
661 offsets = self._install_message(message)
662 self._post_message_hook(self._file)
663 self._file.flush()
664 self._file_length = self._file.tell() # Record current length of mailbox
665 return offsets
669 class _mboxMMDF(_singlefileMailbox):
670 """An mbox or MMDF mailbox."""
672 _mangle_from_ = True
674 def get_message(self, key):
675 """Return a Message representation or raise a KeyError."""
676 start, stop = self._lookup(key)
677 self._file.seek(start)
678 from_line = self._file.readline().replace(os.linesep, '')
679 string = self._file.read(stop - self._file.tell())
680 msg = self._message_factory(string.replace(os.linesep, '\n'))
681 msg.set_from(from_line[5:])
682 return msg
684 def get_string(self, key, from_=False):
685 """Return a string representation or raise a KeyError."""
686 start, stop = self._lookup(key)
687 self._file.seek(start)
688 if not from_:
689 self._file.readline()
690 string = self._file.read(stop - self._file.tell())
691 return string.replace(os.linesep, '\n')
693 def get_file(self, key, from_=False):
694 """Return a file-like representation or raise a KeyError."""
695 start, stop = self._lookup(key)
696 self._file.seek(start)
697 if not from_:
698 self._file.readline()
699 return _PartialFile(self._file, self._file.tell(), stop)
701 def _install_message(self, message):
702 """Format a message and blindly write to self._file."""
703 from_line = None
704 if isinstance(message, str) and message.startswith('From '):
705 newline = message.find('\n')
706 if newline != -1:
707 from_line = message[:newline]
708 message = message[newline + 1:]
709 else:
710 from_line = message
711 message = ''
712 elif isinstance(message, _mboxMMDFMessage):
713 from_line = 'From ' + message.get_from()
714 elif isinstance(message, email.message.Message):
715 from_line = message.get_unixfrom() # May be None.
716 if from_line is None:
717 from_line = 'From MAILER-DAEMON %s' % time.asctime(time.gmtime())
718 start = self._file.tell()
719 self._file.write(from_line + os.linesep)
720 self._dump_message(message, self._file, self._mangle_from_)
721 stop = self._file.tell()
722 return (start, stop)
725 class mbox(_mboxMMDF):
726 """A classic mbox mailbox."""
728 _mangle_from_ = True
730 def __init__(self, path, factory=None, create=True):
731 """Initialize an mbox mailbox."""
732 self._message_factory = mboxMessage
733 _mboxMMDF.__init__(self, path, factory, create)
735 def _pre_message_hook(self, f):
736 """Called before writing each message to file f."""
737 if f.tell() != 0:
738 f.write(os.linesep)
740 def _generate_toc(self):
741 """Generate key-to-(start, stop) table of contents."""
742 starts, stops = [], []
743 self._file.seek(0)
744 while True:
745 line_pos = self._file.tell()
746 line = self._file.readline()
747 if line.startswith('From '):
748 if len(stops) < len(starts):
749 stops.append(line_pos - len(os.linesep))
750 starts.append(line_pos)
751 elif line == '':
752 stops.append(line_pos)
753 break
754 self._toc = dict(enumerate(zip(starts, stops)))
755 self._next_key = len(self._toc)
756 self._file_length = self._file.tell()
759 class MMDF(_mboxMMDF):
760 """An MMDF mailbox."""
762 def __init__(self, path, factory=None, create=True):
763 """Initialize an MMDF mailbox."""
764 self._message_factory = MMDFMessage
765 _mboxMMDF.__init__(self, path, factory, create)
767 def _pre_message_hook(self, f):
768 """Called before writing each message to file f."""
769 f.write('\001\001\001\001' + os.linesep)
771 def _post_message_hook(self, f):
772 """Called after writing each message to file f."""
773 f.write(os.linesep + '\001\001\001\001' + os.linesep)
775 def _generate_toc(self):
776 """Generate key-to-(start, stop) table of contents."""
777 starts, stops = [], []
778 self._file.seek(0)
779 next_pos = 0
780 while True:
781 line_pos = next_pos
782 line = self._file.readline()
783 next_pos = self._file.tell()
784 if line.startswith('\001\001\001\001' + os.linesep):
785 starts.append(next_pos)
786 while True:
787 line_pos = next_pos
788 line = self._file.readline()
789 next_pos = self._file.tell()
790 if line == '\001\001\001\001' + os.linesep:
791 stops.append(line_pos - len(os.linesep))
792 break
793 elif line == '':
794 stops.append(line_pos)
795 break
796 elif line == '':
797 break
798 self._toc = dict(enumerate(zip(starts, stops)))
799 self._next_key = len(self._toc)
800 self._file.seek(0, 2)
801 self._file_length = self._file.tell()
804 class MH(Mailbox):
805 """An MH mailbox."""
807 def __init__(self, path, factory=None, create=True):
808 """Initialize an MH instance."""
809 Mailbox.__init__(self, path, factory, create)
810 if not os.path.exists(self._path):
811 if create:
812 os.mkdir(self._path, 0700)
813 os.close(os.open(os.path.join(self._path, '.mh_sequences'),
814 os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0600))
815 else:
816 raise NoSuchMailboxError(self._path)
817 self._locked = False
819 def add(self, message):
820 """Add message and return assigned key."""
821 keys = self.keys()
822 if len(keys) == 0:
823 new_key = 1
824 else:
825 new_key = max(keys) + 1
826 new_path = os.path.join(self._path, str(new_key))
827 f = _create_carefully(new_path)
828 try:
829 if self._locked:
830 _lock_file(f)
831 try:
832 self._dump_message(message, f)
833 if isinstance(message, MHMessage):
834 self._dump_sequences(message, new_key)
835 finally:
836 if self._locked:
837 _unlock_file(f)
838 finally:
839 _sync_close(f)
840 return new_key
842 def remove(self, key):
843 """Remove the keyed message; raise KeyError if it doesn't exist."""
844 path = os.path.join(self._path, str(key))
845 try:
846 f = open(path, 'rb+')
847 except IOError, e:
848 if e.errno == errno.ENOENT:
849 raise KeyError('No message with key: %s' % key)
850 else:
851 raise
852 try:
853 if self._locked:
854 _lock_file(f)
855 try:
856 f.close()
857 os.remove(os.path.join(self._path, str(key)))
858 finally:
859 if self._locked:
860 _unlock_file(f)
861 finally:
862 f.close()
864 def __setitem__(self, key, message):
865 """Replace the keyed message; raise KeyError if it doesn't exist."""
866 path = os.path.join(self._path, str(key))
867 try:
868 f = open(path, 'rb+')
869 except IOError, e:
870 if e.errno == errno.ENOENT:
871 raise KeyError('No message with key: %s' % key)
872 else:
873 raise
874 try:
875 if self._locked:
876 _lock_file(f)
877 try:
878 os.close(os.open(path, os.O_WRONLY | os.O_TRUNC))
879 self._dump_message(message, f)
880 if isinstance(message, MHMessage):
881 self._dump_sequences(message, key)
882 finally:
883 if self._locked:
884 _unlock_file(f)
885 finally:
886 _sync_close(f)
888 def get_message(self, key):
889 """Return a Message representation or raise a KeyError."""
890 try:
891 if self._locked:
892 f = open(os.path.join(self._path, str(key)), 'r+')
893 else:
894 f = open(os.path.join(self._path, str(key)), 'r')
895 except IOError, e:
896 if e.errno == errno.ENOENT:
897 raise KeyError('No message with key: %s' % key)
898 else:
899 raise
900 try:
901 if self._locked:
902 _lock_file(f)
903 try:
904 msg = MHMessage(f)
905 finally:
906 if self._locked:
907 _unlock_file(f)
908 finally:
909 f.close()
910 for name, key_list in self.get_sequences():
911 if key in key_list:
912 msg.add_sequence(name)
913 return msg
915 def get_string(self, key):
916 """Return a string representation or raise a KeyError."""
917 try:
918 if self._locked:
919 f = open(os.path.join(self._path, str(key)), 'r+')
920 else:
921 f = open(os.path.join(self._path, str(key)), 'r')
922 except IOError, e:
923 if e.errno == errno.ENOENT:
924 raise KeyError('No message with key: %s' % key)
925 else:
926 raise
927 try:
928 if self._locked:
929 _lock_file(f)
930 try:
931 return f.read()
932 finally:
933 if self._locked:
934 _unlock_file(f)
935 finally:
936 f.close()
938 def get_file(self, key):
939 """Return a file-like representation or raise a KeyError."""
940 try:
941 f = open(os.path.join(self._path, str(key)), 'rb')
942 except IOError, e:
943 if e.errno == errno.ENOENT:
944 raise KeyError('No message with key: %s' % key)
945 else:
946 raise
947 return _ProxyFile(f)
949 def iterkeys(self):
950 """Return an iterator over keys."""
951 return iter(sorted(int(entry) for entry in os.listdir(self._path)
952 if entry.isdigit()))
954 def has_key(self, key):
955 """Return True if the keyed message exists, False otherwise."""
956 return os.path.exists(os.path.join(self._path, str(key)))
958 def __len__(self):
959 """Return a count of messages in the mailbox."""
960 return len(list(self.iterkeys()))
962 def lock(self):
963 """Lock the mailbox."""
964 if not self._locked:
965 self._file = open(os.path.join(self._path, '.mh_sequences'), 'rb+')
966 _lock_file(self._file)
967 self._locked = True
969 def unlock(self):
970 """Unlock the mailbox if it is locked."""
971 if self._locked:
972 _unlock_file(self._file)
973 _sync_close(self._file)
974 del self._file
975 self._locked = False
977 def flush(self):
978 """Write any pending changes to the disk."""
979 return
981 def close(self):
982 """Flush and close the mailbox."""
983 if self._locked:
984 self.unlock()
986 def list_folders(self):
987 """Return a list of folder names."""
988 result = []
989 for entry in os.listdir(self._path):
990 if os.path.isdir(os.path.join(self._path, entry)):
991 result.append(entry)
992 return result
994 def get_folder(self, folder):
995 """Return an MH instance for the named folder."""
996 return MH(os.path.join(self._path, folder),
997 factory=self._factory, create=False)
999 def add_folder(self, folder):
1000 """Create a folder and return an MH instance representing it."""
1001 return MH(os.path.join(self._path, folder),
1002 factory=self._factory)
1004 def remove_folder(self, folder):
1005 """Delete the named folder, which must be empty."""
1006 path = os.path.join(self._path, folder)
1007 entries = os.listdir(path)
1008 if entries == ['.mh_sequences']:
1009 os.remove(os.path.join(path, '.mh_sequences'))
1010 elif entries == []:
1011 pass
1012 else:
1013 raise NotEmptyError('Folder not empty: %s' % self._path)
1014 os.rmdir(path)
1016 def get_sequences(self):
1017 """Return a name-to-key-list dictionary to define each sequence."""
1018 results = {}
1019 f = open(os.path.join(self._path, '.mh_sequences'), 'r')
1020 try:
1021 all_keys = set(self.keys())
1022 for line in f:
1023 try:
1024 name, contents = line.split(':')
1025 keys = set()
1026 for spec in contents.split():
1027 if spec.isdigit():
1028 keys.add(int(spec))
1029 else:
1030 start, stop = (int(x) for x in spec.split('-'))
1031 keys.update(range(start, stop + 1))
1032 results[name] = [key for key in sorted(keys) \
1033 if key in all_keys]
1034 if len(results[name]) == 0:
1035 del results[name]
1036 except ValueError:
1037 raise FormatError('Invalid sequence specification: %s' %
1038 line.rstrip())
1039 finally:
1040 f.close()
1041 return results
1043 def set_sequences(self, sequences):
1044 """Set sequences using the given name-to-key-list dictionary."""
1045 f = open(os.path.join(self._path, '.mh_sequences'), 'r+')
1046 try:
1047 os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC))
1048 for name, keys in sequences.iteritems():
1049 if len(keys) == 0:
1050 continue
1051 f.write('%s:' % name)
1052 prev = None
1053 completing = False
1054 for key in sorted(set(keys)):
1055 if key - 1 == prev:
1056 if not completing:
1057 completing = True
1058 f.write('-')
1059 elif completing:
1060 completing = False
1061 f.write('%s %s' % (prev, key))
1062 else:
1063 f.write(' %s' % key)
1064 prev = key
1065 if completing:
1066 f.write(str(prev) + '\n')
1067 else:
1068 f.write('\n')
1069 finally:
1070 _sync_close(f)
1072 def pack(self):
1073 """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1074 sequences = self.get_sequences()
1075 prev = 0
1076 changes = []
1077 for key in self.iterkeys():
1078 if key - 1 != prev:
1079 changes.append((key, prev + 1))
1080 if hasattr(os, 'link'):
1081 os.link(os.path.join(self._path, str(key)),
1082 os.path.join(self._path, str(prev + 1)))
1083 os.unlink(os.path.join(self._path, str(key)))
1084 else:
1085 os.rename(os.path.join(self._path, str(key)),
1086 os.path.join(self._path, str(prev + 1)))
1087 prev += 1
1088 self._next_key = prev + 1
1089 if len(changes) == 0:
1090 return
1091 for name, key_list in sequences.items():
1092 for old, new in changes:
1093 if old in key_list:
1094 key_list[key_list.index(old)] = new
1095 self.set_sequences(sequences)
1097 def _dump_sequences(self, message, key):
1098 """Inspect a new MHMessage and update sequences appropriately."""
1099 pending_sequences = message.get_sequences()
1100 all_sequences = self.get_sequences()
1101 for name, key_list in all_sequences.iteritems():
1102 if name in pending_sequences:
1103 key_list.append(key)
1104 elif key in key_list:
1105 del key_list[key_list.index(key)]
1106 for sequence in pending_sequences:
1107 if sequence not in all_sequences:
1108 all_sequences[sequence] = [key]
1109 self.set_sequences(all_sequences)
1112 class Babyl(_singlefileMailbox):
1113 """An Rmail-style Babyl mailbox."""
1115 _special_labels = frozenset(('unseen', 'deleted', 'filed', 'answered',
1116 'forwarded', 'edited', 'resent'))
1118 def __init__(self, path, factory=None, create=True):
1119 """Initialize a Babyl mailbox."""
1120 _singlefileMailbox.__init__(self, path, factory, create)
1121 self._labels = {}
1123 def add(self, message):
1124 """Add message and return assigned key."""
1125 key = _singlefileMailbox.add(self, message)
1126 if isinstance(message, BabylMessage):
1127 self._labels[key] = message.get_labels()
1128 return key
1130 def remove(self, key):
1131 """Remove the keyed message; raise KeyError if it doesn't exist."""
1132 _singlefileMailbox.remove(self, key)
1133 if key in self._labels:
1134 del self._labels[key]
1136 def __setitem__(self, key, message):
1137 """Replace the keyed message; raise KeyError if it doesn't exist."""
1138 _singlefileMailbox.__setitem__(self, key, message)
1139 if isinstance(message, BabylMessage):
1140 self._labels[key] = message.get_labels()
1142 def get_message(self, key):
1143 """Return a Message representation or raise a KeyError."""
1144 start, stop = self._lookup(key)
1145 self._file.seek(start)
1146 self._file.readline() # Skip '1,' line specifying labels.
1147 original_headers = StringIO.StringIO()
1148 while True:
1149 line = self._file.readline()
1150 if line == '*** EOOH ***' + os.linesep or line == '':
1151 break
1152 original_headers.write(line.replace(os.linesep, '\n'))
1153 visible_headers = StringIO.StringIO()
1154 while True:
1155 line = self._file.readline()
1156 if line == os.linesep or line == '':
1157 break
1158 visible_headers.write(line.replace(os.linesep, '\n'))
1159 body = self._file.read(stop - self._file.tell()).replace(os.linesep,
1160 '\n')
1161 msg = BabylMessage(original_headers.getvalue() + body)
1162 msg.set_visible(visible_headers.getvalue())
1163 if key in self._labels:
1164 msg.set_labels(self._labels[key])
1165 return msg
1167 def get_string(self, key):
1168 """Return a string representation or raise a KeyError."""
1169 start, stop = self._lookup(key)
1170 self._file.seek(start)
1171 self._file.readline() # Skip '1,' line specifying labels.
1172 original_headers = StringIO.StringIO()
1173 while True:
1174 line = self._file.readline()
1175 if line == '*** EOOH ***' + os.linesep or line == '':
1176 break
1177 original_headers.write(line.replace(os.linesep, '\n'))
1178 while True:
1179 line = self._file.readline()
1180 if line == os.linesep or line == '':
1181 break
1182 return original_headers.getvalue() + \
1183 self._file.read(stop - self._file.tell()).replace(os.linesep,
1184 '\n')
1186 def get_file(self, key):
1187 """Return a file-like representation or raise a KeyError."""
1188 return StringIO.StringIO(self.get_string(key).replace('\n',
1189 os.linesep))
1191 def get_labels(self):
1192 """Return a list of user-defined labels in the mailbox."""
1193 self._lookup()
1194 labels = set()
1195 for label_list in self._labels.values():
1196 labels.update(label_list)
1197 labels.difference_update(self._special_labels)
1198 return list(labels)
1200 def _generate_toc(self):
1201 """Generate key-to-(start, stop) table of contents."""
1202 starts, stops = [], []
1203 self._file.seek(0)
1204 next_pos = 0
1205 label_lists = []
1206 while True:
1207 line_pos = next_pos
1208 line = self._file.readline()
1209 next_pos = self._file.tell()
1210 if line == '\037\014' + os.linesep:
1211 if len(stops) < len(starts):
1212 stops.append(line_pos - len(os.linesep))
1213 starts.append(next_pos)
1214 labels = [label.strip() for label
1215 in self._file.readline()[1:].split(',')
1216 if label.strip() != '']
1217 label_lists.append(labels)
1218 elif line == '\037' or line == '\037' + os.linesep:
1219 if len(stops) < len(starts):
1220 stops.append(line_pos - len(os.linesep))
1221 elif line == '':
1222 stops.append(line_pos - len(os.linesep))
1223 break
1224 self._toc = dict(enumerate(zip(starts, stops)))
1225 self._labels = dict(enumerate(label_lists))
1226 self._next_key = len(self._toc)
1227 self._file.seek(0, 2)
1228 self._file_length = self._file.tell()
1230 def _pre_mailbox_hook(self, f):
1231 """Called before writing the mailbox to file f."""
1232 f.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' %
1233 (os.linesep, os.linesep, ','.join(self.get_labels()),
1234 os.linesep))
1236 def _pre_message_hook(self, f):
1237 """Called before writing each message to file f."""
1238 f.write('\014' + os.linesep)
1240 def _post_message_hook(self, f):
1241 """Called after writing each message to file f."""
1242 f.write(os.linesep + '\037')
1244 def _install_message(self, message):
1245 """Write message contents and return (start, stop)."""
1246 start = self._file.tell()
1247 if isinstance(message, BabylMessage):
1248 special_labels = []
1249 labels = []
1250 for label in message.get_labels():
1251 if label in self._special_labels:
1252 special_labels.append(label)
1253 else:
1254 labels.append(label)
1255 self._file.write('1')
1256 for label in special_labels:
1257 self._file.write(', ' + label)
1258 self._file.write(',,')
1259 for label in labels:
1260 self._file.write(' ' + label + ',')
1261 self._file.write(os.linesep)
1262 else:
1263 self._file.write('1,,' + os.linesep)
1264 if isinstance(message, email.message.Message):
1265 orig_buffer = StringIO.StringIO()
1266 orig_generator = email.generator.Generator(orig_buffer, False, 0)
1267 orig_generator.flatten(message)
1268 orig_buffer.seek(0)
1269 while True:
1270 line = orig_buffer.readline()
1271 self._file.write(line.replace('\n', os.linesep))
1272 if line == '\n' or line == '':
1273 break
1274 self._file.write('*** EOOH ***' + os.linesep)
1275 if isinstance(message, BabylMessage):
1276 vis_buffer = StringIO.StringIO()
1277 vis_generator = email.generator.Generator(vis_buffer, False, 0)
1278 vis_generator.flatten(message.get_visible())
1279 while True:
1280 line = vis_buffer.readline()
1281 self._file.write(line.replace('\n', os.linesep))
1282 if line == '\n' or line == '':
1283 break
1284 else:
1285 orig_buffer.seek(0)
1286 while True:
1287 line = orig_buffer.readline()
1288 self._file.write(line.replace('\n', os.linesep))
1289 if line == '\n' or line == '':
1290 break
1291 while True:
1292 buffer = orig_buffer.read(4096) # Buffer size is arbitrary.
1293 if buffer == '':
1294 break
1295 self._file.write(buffer.replace('\n', os.linesep))
1296 elif isinstance(message, str):
1297 body_start = message.find('\n\n') + 2
1298 if body_start - 2 != -1:
1299 self._file.write(message[:body_start].replace('\n',
1300 os.linesep))
1301 self._file.write('*** EOOH ***' + os.linesep)
1302 self._file.write(message[:body_start].replace('\n',
1303 os.linesep))
1304 self._file.write(message[body_start:].replace('\n',
1305 os.linesep))
1306 else:
1307 self._file.write('*** EOOH ***' + os.linesep + os.linesep)
1308 self._file.write(message.replace('\n', os.linesep))
1309 elif hasattr(message, 'readline'):
1310 original_pos = message.tell()
1311 first_pass = True
1312 while True:
1313 line = message.readline()
1314 self._file.write(line.replace('\n', os.linesep))
1315 if line == '\n' or line == '':
1316 self._file.write('*** EOOH ***' + os.linesep)
1317 if first_pass:
1318 first_pass = False
1319 message.seek(original_pos)
1320 else:
1321 break
1322 while True:
1323 buffer = message.read(4096) # Buffer size is arbitrary.
1324 if buffer == '':
1325 break
1326 self._file.write(buffer.replace('\n', os.linesep))
1327 else:
1328 raise TypeError('Invalid message type: %s' % type(message))
1329 stop = self._file.tell()
1330 return (start, stop)
1333 class Message(email.message.Message):
1334 """Message with mailbox-format-specific properties."""
1336 def __init__(self, message=None):
1337 """Initialize a Message instance."""
1338 if isinstance(message, email.message.Message):
1339 self._become_message(copy.deepcopy(message))
1340 if isinstance(message, Message):
1341 message._explain_to(self)
1342 elif isinstance(message, str):
1343 self._become_message(email.message_from_string(message))
1344 elif hasattr(message, "read"):
1345 self._become_message(email.message_from_file(message))
1346 elif message is None:
1347 email.message.Message.__init__(self)
1348 else:
1349 raise TypeError('Invalid message type: %s' % type(message))
1351 def _become_message(self, message):
1352 """Assume the non-format-specific state of message."""
1353 for name in ('_headers', '_unixfrom', '_payload', '_charset',
1354 'preamble', 'epilogue', 'defects', '_default_type'):
1355 self.__dict__[name] = message.__dict__[name]
1357 def _explain_to(self, message):
1358 """Copy format-specific state to message insofar as possible."""
1359 if isinstance(message, Message):
1360 return # There's nothing format-specific to explain.
1361 else:
1362 raise TypeError('Cannot convert to specified type')
1365 class MaildirMessage(Message):
1366 """Message with Maildir-specific properties."""
1368 def __init__(self, message=None):
1369 """Initialize a MaildirMessage instance."""
1370 self._subdir = 'new'
1371 self._info = ''
1372 self._date = time.time()
1373 Message.__init__(self, message)
1375 def get_subdir(self):
1376 """Return 'new' or 'cur'."""
1377 return self._subdir
1379 def set_subdir(self, subdir):
1380 """Set subdir to 'new' or 'cur'."""
1381 if subdir == 'new' or subdir == 'cur':
1382 self._subdir = subdir
1383 else:
1384 raise ValueError("subdir must be 'new' or 'cur': %s" % subdir)
1386 def get_flags(self):
1387 """Return as a string the flags that are set."""
1388 if self._info.startswith('2,'):
1389 return self._info[2:]
1390 else:
1391 return ''
1393 def set_flags(self, flags):
1394 """Set the given flags and unset all others."""
1395 self._info = '2,' + ''.join(sorted(flags))
1397 def add_flag(self, flag):
1398 """Set the given flag(s) without changing others."""
1399 self.set_flags(''.join(set(self.get_flags()) | set(flag)))
1401 def remove_flag(self, flag):
1402 """Unset the given string flag(s) without changing others."""
1403 if self.get_flags() != '':
1404 self.set_flags(''.join(set(self.get_flags()) - set(flag)))
1406 def get_date(self):
1407 """Return delivery date of message, in seconds since the epoch."""
1408 return self._date
1410 def set_date(self, date):
1411 """Set delivery date of message, in seconds since the epoch."""
1412 try:
1413 self._date = float(date)
1414 except ValueError:
1415 raise TypeError("can't convert to float: %s" % date)
1417 def get_info(self):
1418 """Get the message's "info" as a string."""
1419 return self._info
1421 def set_info(self, info):
1422 """Set the message's "info" string."""
1423 if isinstance(info, str):
1424 self._info = info
1425 else:
1426 raise TypeError('info must be a string: %s' % type(info))
1428 def _explain_to(self, message):
1429 """Copy Maildir-specific state to message insofar as possible."""
1430 if isinstance(message, MaildirMessage):
1431 message.set_flags(self.get_flags())
1432 message.set_subdir(self.get_subdir())
1433 message.set_date(self.get_date())
1434 elif isinstance(message, _mboxMMDFMessage):
1435 flags = set(self.get_flags())
1436 if 'S' in flags:
1437 message.add_flag('R')
1438 if self.get_subdir() == 'cur':
1439 message.add_flag('O')
1440 if 'T' in flags:
1441 message.add_flag('D')
1442 if 'F' in flags:
1443 message.add_flag('F')
1444 if 'R' in flags:
1445 message.add_flag('A')
1446 message.set_from('MAILER-DAEMON', time.gmtime(self.get_date()))
1447 elif isinstance(message, MHMessage):
1448 flags = set(self.get_flags())
1449 if 'S' not in flags:
1450 message.add_sequence('unseen')
1451 if 'R' in flags:
1452 message.add_sequence('replied')
1453 if 'F' in flags:
1454 message.add_sequence('flagged')
1455 elif isinstance(message, BabylMessage):
1456 flags = set(self.get_flags())
1457 if 'S' not in flags:
1458 message.add_label('unseen')
1459 if 'T' in flags:
1460 message.add_label('deleted')
1461 if 'R' in flags:
1462 message.add_label('answered')
1463 if 'P' in flags:
1464 message.add_label('forwarded')
1465 elif isinstance(message, Message):
1466 pass
1467 else:
1468 raise TypeError('Cannot convert to specified type: %s' %
1469 type(message))
1472 class _mboxMMDFMessage(Message):
1473 """Message with mbox- or MMDF-specific properties."""
1475 def __init__(self, message=None):
1476 """Initialize an mboxMMDFMessage instance."""
1477 self.set_from('MAILER-DAEMON', True)
1478 if isinstance(message, email.message.Message):
1479 unixfrom = message.get_unixfrom()
1480 if unixfrom is not None and unixfrom.startswith('From '):
1481 self.set_from(unixfrom[5:])
1482 Message.__init__(self, message)
1484 def get_from(self):
1485 """Return contents of "From " line."""
1486 return self._from
1488 def set_from(self, from_, time_=None):
1489 """Set "From " line, formatting and appending time_ if specified."""
1490 if time_ is not None:
1491 if time_ is True:
1492 time_ = time.gmtime()
1493 from_ += ' ' + time.asctime(time_)
1494 self._from = from_
1496 def get_flags(self):
1497 """Return as a string the flags that are set."""
1498 return self.get('Status', '') + self.get('X-Status', '')
1500 def set_flags(self, flags):
1501 """Set the given flags and unset all others."""
1502 flags = set(flags)
1503 status_flags, xstatus_flags = '', ''
1504 for flag in ('R', 'O'):
1505 if flag in flags:
1506 status_flags += flag
1507 flags.remove(flag)
1508 for flag in ('D', 'F', 'A'):
1509 if flag in flags:
1510 xstatus_flags += flag
1511 flags.remove(flag)
1512 xstatus_flags += ''.join(sorted(flags))
1513 try:
1514 self.replace_header('Status', status_flags)
1515 except KeyError:
1516 self.add_header('Status', status_flags)
1517 try:
1518 self.replace_header('X-Status', xstatus_flags)
1519 except KeyError:
1520 self.add_header('X-Status', xstatus_flags)
1522 def add_flag(self, flag):
1523 """Set the given flag(s) without changing others."""
1524 self.set_flags(''.join(set(self.get_flags()) | set(flag)))
1526 def remove_flag(self, flag):
1527 """Unset the given string flag(s) without changing others."""
1528 if 'Status' in self or 'X-Status' in self:
1529 self.set_flags(''.join(set(self.get_flags()) - set(flag)))
1531 def _explain_to(self, message):
1532 """Copy mbox- or MMDF-specific state to message insofar as possible."""
1533 if isinstance(message, MaildirMessage):
1534 flags = set(self.get_flags())
1535 if 'O' in flags:
1536 message.set_subdir('cur')
1537 if 'F' in flags:
1538 message.add_flag('F')
1539 if 'A' in flags:
1540 message.add_flag('R')
1541 if 'R' in flags:
1542 message.add_flag('S')
1543 if 'D' in flags:
1544 message.add_flag('T')
1545 del message['status']
1546 del message['x-status']
1547 maybe_date = ' '.join(self.get_from().split()[-5:])
1548 try:
1549 message.set_date(calendar.timegm(time.strptime(maybe_date,
1550 '%a %b %d %H:%M:%S %Y')))
1551 except (ValueError, OverflowError):
1552 pass
1553 elif isinstance(message, _mboxMMDFMessage):
1554 message.set_flags(self.get_flags())
1555 message.set_from(self.get_from())
1556 elif isinstance(message, MHMessage):
1557 flags = set(self.get_flags())
1558 if 'R' not in flags:
1559 message.add_sequence('unseen')
1560 if 'A' in flags:
1561 message.add_sequence('replied')
1562 if 'F' in flags:
1563 message.add_sequence('flagged')
1564 del message['status']
1565 del message['x-status']
1566 elif isinstance(message, BabylMessage):
1567 flags = set(self.get_flags())
1568 if 'R' not in flags:
1569 message.add_label('unseen')
1570 if 'D' in flags:
1571 message.add_label('deleted')
1572 if 'A' in flags:
1573 message.add_label('answered')
1574 del message['status']
1575 del message['x-status']
1576 elif isinstance(message, Message):
1577 pass
1578 else:
1579 raise TypeError('Cannot convert to specified type: %s' %
1580 type(message))
1583 class mboxMessage(_mboxMMDFMessage):
1584 """Message with mbox-specific properties."""
1587 class MHMessage(Message):
1588 """Message with MH-specific properties."""
1590 def __init__(self, message=None):
1591 """Initialize an MHMessage instance."""
1592 self._sequences = []
1593 Message.__init__(self, message)
1595 def get_sequences(self):
1596 """Return a list of sequences that include the message."""
1597 return self._sequences[:]
1599 def set_sequences(self, sequences):
1600 """Set the list of sequences that include the message."""
1601 self._sequences = list(sequences)
1603 def add_sequence(self, sequence):
1604 """Add sequence to list of sequences including the message."""
1605 if isinstance(sequence, str):
1606 if not sequence in self._sequences:
1607 self._sequences.append(sequence)
1608 else:
1609 raise TypeError('sequence must be a string: %s' % type(sequence))
1611 def remove_sequence(self, sequence):
1612 """Remove sequence from the list of sequences including the message."""
1613 try:
1614 self._sequences.remove(sequence)
1615 except ValueError:
1616 pass
1618 def _explain_to(self, message):
1619 """Copy MH-specific state to message insofar as possible."""
1620 if isinstance(message, MaildirMessage):
1621 sequences = set(self.get_sequences())
1622 if 'unseen' in sequences:
1623 message.set_subdir('cur')
1624 else:
1625 message.set_subdir('cur')
1626 message.add_flag('S')
1627 if 'flagged' in sequences:
1628 message.add_flag('F')
1629 if 'replied' in sequences:
1630 message.add_flag('R')
1631 elif isinstance(message, _mboxMMDFMessage):
1632 sequences = set(self.get_sequences())
1633 if 'unseen' not in sequences:
1634 message.add_flag('RO')
1635 else:
1636 message.add_flag('O')
1637 if 'flagged' in sequences:
1638 message.add_flag('F')
1639 if 'replied' in sequences:
1640 message.add_flag('A')
1641 elif isinstance(message, MHMessage):
1642 for sequence in self.get_sequences():
1643 message.add_sequence(sequence)
1644 elif isinstance(message, BabylMessage):
1645 sequences = set(self.get_sequences())
1646 if 'unseen' in sequences:
1647 message.add_label('unseen')
1648 if 'replied' in sequences:
1649 message.add_label('answered')
1650 elif isinstance(message, Message):
1651 pass
1652 else:
1653 raise TypeError('Cannot convert to specified type: %s' %
1654 type(message))
1657 class BabylMessage(Message):
1658 """Message with Babyl-specific properties."""
1660 def __init__(self, message=None):
1661 """Initialize an BabylMessage instance."""
1662 self._labels = []
1663 self._visible = Message()
1664 Message.__init__(self, message)
1666 def get_labels(self):
1667 """Return a list of labels on the message."""
1668 return self._labels[:]
1670 def set_labels(self, labels):
1671 """Set the list of labels on the message."""
1672 self._labels = list(labels)
1674 def add_label(self, label):
1675 """Add label to list of labels on the message."""
1676 if isinstance(label, str):
1677 if label not in self._labels:
1678 self._labels.append(label)
1679 else:
1680 raise TypeError('label must be a string: %s' % type(label))
1682 def remove_label(self, label):
1683 """Remove label from the list of labels on the message."""
1684 try:
1685 self._labels.remove(label)
1686 except ValueError:
1687 pass
1689 def get_visible(self):
1690 """Return a Message representation of visible headers."""
1691 return Message(self._visible)
1693 def set_visible(self, visible):
1694 """Set the Message representation of visible headers."""
1695 self._visible = Message(visible)
1697 def update_visible(self):
1698 """Update and/or sensibly generate a set of visible headers."""
1699 for header in self._visible.keys():
1700 if header in self:
1701 self._visible.replace_header(header, self[header])
1702 else:
1703 del self._visible[header]
1704 for header in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1705 if header in self and header not in self._visible:
1706 self._visible[header] = self[header]
1708 def _explain_to(self, message):
1709 """Copy Babyl-specific state to message insofar as possible."""
1710 if isinstance(message, MaildirMessage):
1711 labels = set(self.get_labels())
1712 if 'unseen' in labels:
1713 message.set_subdir('cur')
1714 else:
1715 message.set_subdir('cur')
1716 message.add_flag('S')
1717 if 'forwarded' in labels or 'resent' in labels:
1718 message.add_flag('P')
1719 if 'answered' in labels:
1720 message.add_flag('R')
1721 if 'deleted' in labels:
1722 message.add_flag('T')
1723 elif isinstance(message, _mboxMMDFMessage):
1724 labels = set(self.get_labels())
1725 if 'unseen' not in labels:
1726 message.add_flag('RO')
1727 else:
1728 message.add_flag('O')
1729 if 'deleted' in labels:
1730 message.add_flag('D')
1731 if 'answered' in labels:
1732 message.add_flag('A')
1733 elif isinstance(message, MHMessage):
1734 labels = set(self.get_labels())
1735 if 'unseen' in labels:
1736 message.add_sequence('unseen')
1737 if 'answered' in labels:
1738 message.add_sequence('replied')
1739 elif isinstance(message, BabylMessage):
1740 message.set_visible(self.get_visible())
1741 for label in self.get_labels():
1742 message.add_label(label)
1743 elif isinstance(message, Message):
1744 pass
1745 else:
1746 raise TypeError('Cannot convert to specified type: %s' %
1747 type(message))
1750 class MMDFMessage(_mboxMMDFMessage):
1751 """Message with MMDF-specific properties."""
1754 class _ProxyFile:
1755 """A read-only wrapper of a file."""
1757 def __init__(self, f, pos=None):
1758 """Initialize a _ProxyFile."""
1759 self._file = f
1760 if pos is None:
1761 self._pos = f.tell()
1762 else:
1763 self._pos = pos
1765 def read(self, size=None):
1766 """Read bytes."""
1767 return self._read(size, self._file.read)
1769 def readline(self, size=None):
1770 """Read a line."""
1771 return self._read(size, self._file.readline)
1773 def readlines(self, sizehint=None):
1774 """Read multiple lines."""
1775 result = []
1776 for line in self:
1777 result.append(line)
1778 if sizehint is not None:
1779 sizehint -= len(line)
1780 if sizehint <= 0:
1781 break
1782 return result
1784 def __iter__(self):
1785 """Iterate over lines."""
1786 return iter(self.readline, "")
1788 def tell(self):
1789 """Return the position."""
1790 return self._pos
1792 def seek(self, offset, whence=0):
1793 """Change position."""
1794 if whence == 1:
1795 self._file.seek(self._pos)
1796 self._file.seek(offset, whence)
1797 self._pos = self._file.tell()
1799 def close(self):
1800 """Close the file."""
1801 del self._file
1803 def _read(self, size, read_method):
1804 """Read size bytes using read_method."""
1805 if size is None:
1806 size = -1
1807 self._file.seek(self._pos)
1808 result = read_method(size)
1809 self._pos = self._file.tell()
1810 return result
1813 class _PartialFile(_ProxyFile):
1814 """A read-only wrapper of part of a file."""
1816 def __init__(self, f, start=None, stop=None):
1817 """Initialize a _PartialFile."""
1818 _ProxyFile.__init__(self, f, start)
1819 self._start = start
1820 self._stop = stop
1822 def tell(self):
1823 """Return the position with respect to start."""
1824 return _ProxyFile.tell(self) - self._start
1826 def seek(self, offset, whence=0):
1827 """Change position, possibly with respect to start or stop."""
1828 if whence == 0:
1829 self._pos = self._start
1830 whence = 1
1831 elif whence == 2:
1832 self._pos = self._stop
1833 whence = 1
1834 _ProxyFile.seek(self, offset, whence)
1836 def _read(self, size, read_method):
1837 """Read size bytes using read_method, honoring start and stop."""
1838 remaining = self._stop - self._pos
1839 if remaining <= 0:
1840 return ''
1841 if size is None or size < 0 or size > remaining:
1842 size = remaining
1843 return _ProxyFile._read(self, size, read_method)
1846 def _lock_file(f, dotlock=True):
1847 """Lock file f using lockf and dot locking."""
1848 dotlock_done = False
1849 try:
1850 if fcntl:
1851 try:
1852 fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
1853 except IOError, e:
1854 if e.errno in (errno.EAGAIN, errno.EACCES):
1855 raise ExternalClashError('lockf: lock unavailable: %s' %
1856 f.name)
1857 else:
1858 raise
1859 if dotlock:
1860 try:
1861 pre_lock = _create_temporary(f.name + '.lock')
1862 pre_lock.close()
1863 except IOError, e:
1864 if e.errno == errno.EACCES:
1865 return # Without write access, just skip dotlocking.
1866 else:
1867 raise
1868 try:
1869 if hasattr(os, 'link'):
1870 os.link(pre_lock.name, f.name + '.lock')
1871 dotlock_done = True
1872 os.unlink(pre_lock.name)
1873 else:
1874 os.rename(pre_lock.name, f.name + '.lock')
1875 dotlock_done = True
1876 except OSError, e:
1877 if e.errno == errno.EEXIST or \
1878 (os.name == 'os2' and e.errno == errno.EACCES):
1879 os.remove(pre_lock.name)
1880 raise ExternalClashError('dot lock unavailable: %s' %
1881 f.name)
1882 else:
1883 raise
1884 except:
1885 if fcntl:
1886 fcntl.lockf(f, fcntl.LOCK_UN)
1887 if dotlock_done:
1888 os.remove(f.name + '.lock')
1889 raise
1891 def _unlock_file(f):
1892 """Unlock file f using lockf and dot locking."""
1893 if fcntl:
1894 fcntl.lockf(f, fcntl.LOCK_UN)
1895 if os.path.exists(f.name + '.lock'):
1896 os.remove(f.name + '.lock')
1898 def _create_carefully(path):
1899 """Create a file if it doesn't exist and open for reading and writing."""
1900 fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
1901 try:
1902 return open(path, 'rb+')
1903 finally:
1904 os.close(fd)
1906 def _create_temporary(path):
1907 """Create a temp file based on path and open for reading and writing."""
1908 return _create_carefully('%s.%s.%s.%s' % (path, int(time.time()),
1909 socket.gethostname(),
1910 os.getpid()))
1912 def _sync_flush(f):
1913 """Ensure changes to file f are physically on disk."""
1914 f.flush()
1915 if hasattr(os, 'fsync'):
1916 os.fsync(f.fileno())
1918 def _sync_close(f):
1919 """Close file f, ensuring all changes are physically on disk."""
1920 _sync_flush(f)
1921 f.close()
1923 ## Start: classes from the original module (for backward compatibility).
1925 # Note that the Maildir class, whose name is unchanged, itself offers a next()
1926 # method for backward compatibility.
1928 class _Mailbox:
1930 def __init__(self, fp, factory=rfc822.Message):
1931 self.fp = fp
1932 self.seekp = 0
1933 self.factory = factory
1935 def __iter__(self):
1936 return iter(self.next, None)
1938 def next(self):
1939 while 1:
1940 self.fp.seek(self.seekp)
1941 try:
1942 self._search_start()
1943 except EOFError:
1944 self.seekp = self.fp.tell()
1945 return None
1946 start = self.fp.tell()
1947 self._search_end()
1948 self.seekp = stop = self.fp.tell()
1949 if start != stop:
1950 break
1951 return self.factory(_PartialFile(self.fp, start, stop))
1953 # Recommended to use PortableUnixMailbox instead!
1954 class UnixMailbox(_Mailbox):
1956 def _search_start(self):
1957 while 1:
1958 pos = self.fp.tell()
1959 line = self.fp.readline()
1960 if not line:
1961 raise EOFError
1962 if line[:5] == 'From ' and self._isrealfromline(line):
1963 self.fp.seek(pos)
1964 return
1966 def _search_end(self):
1967 self.fp.readline() # Throw away header line
1968 while 1:
1969 pos = self.fp.tell()
1970 line = self.fp.readline()
1971 if not line:
1972 return
1973 if line[:5] == 'From ' and self._isrealfromline(line):
1974 self.fp.seek(pos)
1975 return
1977 # An overridable mechanism to test for From-line-ness. You can either
1978 # specify a different regular expression or define a whole new
1979 # _isrealfromline() method. Note that this only gets called for lines
1980 # starting with the 5 characters "From ".
1982 # BAW: According to
1983 #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
1984 # the only portable, reliable way to find message delimiters in a BSD (i.e
1985 # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the
1986 # beginning of the file, "^From .*\n". While _fromlinepattern below seems
1987 # like a good idea, in practice, there are too many variations for more
1988 # strict parsing of the line to be completely accurate.
1990 # _strict_isrealfromline() is the old version which tries to do stricter
1991 # parsing of the From_ line. _portable_isrealfromline() simply returns
1992 # true, since it's never called if the line doesn't already start with
1993 # "From ".
1995 # This algorithm, and the way it interacts with _search_start() and
1996 # _search_end() may not be completely correct, because it doesn't check
1997 # that the two characters preceding "From " are \n\n or the beginning of
1998 # the file. Fixing this would require a more extensive rewrite than is
1999 # necessary. For convenience, we've added a PortableUnixMailbox class
2000 # which does no checking of the format of the 'From' line.
2002 _fromlinepattern = (r"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+"
2003 r"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*"
2004 r"[^\s]*\s*"
2005 "$")
2006 _regexp = None
2008 def _strict_isrealfromline(self, line):
2009 if not self._regexp:
2010 import re
2011 self._regexp = re.compile(self._fromlinepattern)
2012 return self._regexp.match(line)
2014 def _portable_isrealfromline(self, line):
2015 return True
2017 _isrealfromline = _strict_isrealfromline
2020 class PortableUnixMailbox(UnixMailbox):
2021 _isrealfromline = UnixMailbox._portable_isrealfromline
2024 class MmdfMailbox(_Mailbox):
2026 def _search_start(self):
2027 while 1:
2028 line = self.fp.readline()
2029 if not line:
2030 raise EOFError
2031 if line[:5] == '\001\001\001\001\n':
2032 return
2034 def _search_end(self):
2035 while 1:
2036 pos = self.fp.tell()
2037 line = self.fp.readline()
2038 if not line:
2039 return
2040 if line == '\001\001\001\001\n':
2041 self.fp.seek(pos)
2042 return
2045 class MHMailbox:
2047 def __init__(self, dirname, factory=rfc822.Message):
2048 import re
2049 pat = re.compile('^[1-9][0-9]*$')
2050 self.dirname = dirname
2051 # the three following lines could be combined into:
2052 # list = map(long, filter(pat.match, os.listdir(self.dirname)))
2053 list = os.listdir(self.dirname)
2054 list = filter(pat.match, list)
2055 list = map(long, list)
2056 list.sort()
2057 # This only works in Python 1.6 or later;
2058 # before that str() added 'L':
2059 self.boxes = map(str, list)
2060 self.boxes.reverse()
2061 self.factory = factory
2063 def __iter__(self):
2064 return iter(self.next, None)
2066 def next(self):
2067 if not self.boxes:
2068 return None
2069 fn = self.boxes.pop()
2070 fp = open(os.path.join(self.dirname, fn))
2071 msg = self.factory(fp)
2072 try:
2073 msg._mh_msgno = fn
2074 except (AttributeError, TypeError):
2075 pass
2076 return msg
2079 class BabylMailbox(_Mailbox):
2081 def _search_start(self):
2082 while 1:
2083 line = self.fp.readline()
2084 if not line:
2085 raise EOFError
2086 if line == '*** EOOH ***\n':
2087 return
2089 def _search_end(self):
2090 while 1:
2091 pos = self.fp.tell()
2092 line = self.fp.readline()
2093 if not line:
2094 return
2095 if line == '\037\014\n' or line == '\037':
2096 self.fp.seek(pos)
2097 return
2099 ## End: classes from the original module (for backward compatibility).
2102 class Error(Exception):
2103 """Raised for module-specific errors."""
2105 class NoSuchMailboxError(Error):
2106 """The specified mailbox does not exist and won't be created."""
2108 class NotEmptyError(Error):
2109 """The specified mailbox is not empty and deletion was requested."""
2111 class ExternalClashError(Error):
2112 """Another process caused an action to fail."""
2114 class FormatError(Error):
2115 """A file appears to have an invalid format."""