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