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