• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes."""
2
3# Notes for authors of new mailbox subclasses:
4#
5# Remember to fsync() changes to disk before closing a modified file
6# or returning from a flush() method.  See functions _sync_flush() and
7# _sync_close().
8
9import os
10import time
11import calendar
12import socket
13import errno
14import copy
15import warnings
16import email
17import email.message
18import email.generator
19import io
20import contextlib
21from types import GenericAlias
22try:
23    import fcntl
24except ImportError:
25    fcntl = None
26
27__all__ = ['Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF',
28           'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage',
29           'BabylMessage', 'MMDFMessage', 'Error', 'NoSuchMailboxError',
30           'NotEmptyError', 'ExternalClashError', 'FormatError']
31
32linesep = os.linesep.encode('ascii')
33
34class Mailbox:
35    """A group of messages in a particular place."""
36
37    def __init__(self, path, factory=None, create=True):
38        """Initialize a Mailbox instance."""
39        self._path = os.path.abspath(os.path.expanduser(path))
40        self._factory = factory
41
42    def add(self, message):
43        """Add message and return assigned key."""
44        raise NotImplementedError('Method must be implemented by subclass')
45
46    def remove(self, key):
47        """Remove the keyed message; raise KeyError if it doesn't exist."""
48        raise NotImplementedError('Method must be implemented by subclass')
49
50    def __delitem__(self, key):
51        self.remove(key)
52
53    def discard(self, key):
54        """If the keyed message exists, remove it."""
55        try:
56            self.remove(key)
57        except KeyError:
58            pass
59
60    def __setitem__(self, key, message):
61        """Replace the keyed message; raise KeyError if it doesn't exist."""
62        raise NotImplementedError('Method must be implemented by subclass')
63
64    def get(self, key, default=None):
65        """Return the keyed message, or default if it doesn't exist."""
66        try:
67            return self.__getitem__(key)
68        except KeyError:
69            return default
70
71    def __getitem__(self, key):
72        """Return the keyed message; raise KeyError if it doesn't exist."""
73        if not self._factory:
74            return self.get_message(key)
75        else:
76            with contextlib.closing(self.get_file(key)) as file:
77                return self._factory(file)
78
79    def get_message(self, key):
80        """Return a Message representation or raise a KeyError."""
81        raise NotImplementedError('Method must be implemented by subclass')
82
83    def get_string(self, key):
84        """Return a string representation or raise a KeyError.
85
86        Uses email.message.Message to create a 7bit clean string
87        representation of the message."""
88        return email.message_from_bytes(self.get_bytes(key)).as_string()
89
90    def get_bytes(self, key):
91        """Return a byte string representation or raise a KeyError."""
92        raise NotImplementedError('Method must be implemented by subclass')
93
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')
97
98    def iterkeys(self):
99        """Return an iterator over keys."""
100        raise NotImplementedError('Method must be implemented by subclass')
101
102    def keys(self):
103        """Return a list of keys."""
104        return list(self.iterkeys())
105
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
114
115    def __iter__(self):
116        return self.itervalues()
117
118    def values(self):
119        """Return a list of messages. Memory intensive."""
120        return list(self.itervalues())
121
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)
130
131    def items(self):
132        """Return a list of (key, message) tuples. Memory intensive."""
133        return list(self.iteritems())
134
135    def __contains__(self, key):
136        """Return True if the keyed message exists, False otherwise."""
137        raise NotImplementedError('Method must be implemented by subclass')
138
139    def __len__(self):
140        """Return a count of messages in the mailbox."""
141        raise NotImplementedError('Method must be implemented by subclass')
142
143    def clear(self):
144        """Delete all messages."""
145        for key in self.keys():
146            self.discard(key)
147
148    def pop(self, key, default=None):
149        """Delete the keyed message and return it, or default."""
150        try:
151            result = self[key]
152        except KeyError:
153            return default
154        self.discard(key)
155        return result
156
157    def popitem(self):
158        """Delete an arbitrary (key, message) pair and return it."""
159        for key in self.iterkeys():
160            return (key, self.pop(key))     # This is only run once.
161        else:
162            raise KeyError('No messages in mailbox')
163
164    def update(self, arg=None):
165        """Change the messages that correspond to certain keys."""
166        if hasattr(arg, 'iteritems'):
167            source = arg.iteritems()
168        elif hasattr(arg, 'items'):
169            source = arg.items()
170        else:
171            source = arg
172        bad_key = False
173        for key, message in source:
174            try:
175                self[key] = message
176            except KeyError:
177                bad_key = True
178        if bad_key:
179            raise KeyError('No message with key(s)')
180
181    def flush(self):
182        """Write any pending changes to the disk."""
183        raise NotImplementedError('Method must be implemented by subclass')
184
185    def lock(self):
186        """Lock the mailbox."""
187        raise NotImplementedError('Method must be implemented by subclass')
188
189    def unlock(self):
190        """Unlock the mailbox if it is locked."""
191        raise NotImplementedError('Method must be implemented by subclass')
192
193    def close(self):
194        """Flush and close the mailbox."""
195        raise NotImplementedError('Method must be implemented by subclass')
196
197    def _string_to_bytes(self, message):
198        # If a message is not 7bit clean, we refuse to handle it since it
199        # likely came from reading invalid messages in text mode, and that way
200        # lies mojibake.
201        try:
202            return message.encode('ascii')
203        except UnicodeError:
204            raise ValueError("String input must be ASCII-only; "
205                "use bytes or a Message instead")
206
207    # Whether each message must end in a newline
208    _append_newline = False
209
210    def _dump_message(self, message, target, mangle_from_=False):
211        # This assumes the target file is open in binary mode.
212        """Dump message contents to target file."""
213        if isinstance(message, email.message.Message):
214            buffer = io.BytesIO()
215            gen = email.generator.BytesGenerator(buffer, mangle_from_, 0)
216            gen.flatten(message)
217            buffer.seek(0)
218            data = buffer.read()
219            data = data.replace(b'\n', linesep)
220            target.write(data)
221            if self._append_newline and not data.endswith(linesep):
222                # Make sure the message ends with a newline
223                target.write(linesep)
224        elif isinstance(message, (str, bytes, io.StringIO)):
225            if isinstance(message, io.StringIO):
226                warnings.warn("Use of StringIO input is deprecated, "
227                    "use BytesIO instead", DeprecationWarning, 3)
228                message = message.getvalue()
229            if isinstance(message, str):
230                message = self._string_to_bytes(message)
231            if mangle_from_:
232                message = message.replace(b'\nFrom ', b'\n>From ')
233            message = message.replace(b'\n', linesep)
234            target.write(message)
235            if self._append_newline and not message.endswith(linesep):
236                # Make sure the message ends with a newline
237                target.write(linesep)
238        elif hasattr(message, 'read'):
239            if hasattr(message, 'buffer'):
240                warnings.warn("Use of text mode files is deprecated, "
241                    "use a binary mode file instead", DeprecationWarning, 3)
242                message = message.buffer
243            lastline = None
244            while True:
245                line = message.readline()
246                # Universal newline support.
247                if line.endswith(b'\r\n'):
248                    line = line[:-2] + b'\n'
249                elif line.endswith(b'\r'):
250                    line = line[:-1] + b'\n'
251                if not line:
252                    break
253                if mangle_from_ and line.startswith(b'From '):
254                    line = b'>From ' + line[5:]
255                line = line.replace(b'\n', linesep)
256                target.write(line)
257                lastline = line
258            if self._append_newline and lastline and not lastline.endswith(linesep):
259                # Make sure the message ends with a newline
260                target.write(linesep)
261        else:
262            raise TypeError('Invalid message type: %s' % type(message))
263
264    __class_getitem__ = classmethod(GenericAlias)
265
266
267class Maildir(Mailbox):
268    """A qmail-style Maildir mailbox."""
269
270    colon = ':'
271
272    def __init__(self, dirname, factory=None, create=True):
273        """Initialize a Maildir instance."""
274        Mailbox.__init__(self, dirname, factory, create)
275        self._paths = {
276            'tmp': os.path.join(self._path, 'tmp'),
277            'new': os.path.join(self._path, 'new'),
278            'cur': os.path.join(self._path, 'cur'),
279            }
280        if not os.path.exists(self._path):
281            if create:
282                os.mkdir(self._path, 0o700)
283                for path in self._paths.values():
284                    os.mkdir(path, 0o700)
285            else:
286                raise NoSuchMailboxError(self._path)
287        self._toc = {}
288        self._toc_mtimes = {'cur': 0, 'new': 0}
289        self._last_read = 0         # Records last time we read cur/new
290        self._skewfactor = 0.1      # Adjust if os/fs clocks are skewing
291
292    def add(self, message):
293        """Add message and return assigned key."""
294        tmp_file = self._create_tmp()
295        try:
296            self._dump_message(message, tmp_file)
297        except BaseException:
298            tmp_file.close()
299            os.remove(tmp_file.name)
300            raise
301        _sync_close(tmp_file)
302        if isinstance(message, MaildirMessage):
303            subdir = message.get_subdir()
304            suffix = self.colon + message.get_info()
305            if suffix == self.colon:
306                suffix = ''
307        else:
308            subdir = 'new'
309            suffix = ''
310        uniq = os.path.basename(tmp_file.name).split(self.colon)[0]
311        dest = os.path.join(self._path, subdir, uniq + suffix)
312        if isinstance(message, MaildirMessage):
313            os.utime(tmp_file.name,
314                     (os.path.getatime(tmp_file.name), message.get_date()))
315        # No file modification should be done after the file is moved to its
316        # final position in order to prevent race conditions with changes
317        # from other programs
318        try:
319            try:
320                os.link(tmp_file.name, dest)
321            except (AttributeError, PermissionError):
322                os.rename(tmp_file.name, dest)
323            else:
324                os.remove(tmp_file.name)
325        except OSError as e:
326            os.remove(tmp_file.name)
327            if e.errno == errno.EEXIST:
328                raise ExternalClashError('Name clash with existing message: %s'
329                                         % dest)
330            else:
331                raise
332        return uniq
333
334    def remove(self, key):
335        """Remove the keyed message; raise KeyError if it doesn't exist."""
336        os.remove(os.path.join(self._path, self._lookup(key)))
337
338    def discard(self, key):
339        """If the keyed message exists, remove it."""
340        # This overrides an inapplicable implementation in the superclass.
341        try:
342            self.remove(key)
343        except (KeyError, FileNotFoundError):
344            pass
345
346    def __setitem__(self, key, message):
347        """Replace the keyed message; raise KeyError if it doesn't exist."""
348        old_subpath = self._lookup(key)
349        temp_key = self.add(message)
350        temp_subpath = self._lookup(temp_key)
351        if isinstance(message, MaildirMessage):
352            # temp's subdir and suffix were specified by message.
353            dominant_subpath = temp_subpath
354        else:
355            # temp's subdir and suffix were defaults from add().
356            dominant_subpath = old_subpath
357        subdir = os.path.dirname(dominant_subpath)
358        if self.colon in dominant_subpath:
359            suffix = self.colon + dominant_subpath.split(self.colon)[-1]
360        else:
361            suffix = ''
362        self.discard(key)
363        tmp_path = os.path.join(self._path, temp_subpath)
364        new_path = os.path.join(self._path, subdir, key + suffix)
365        if isinstance(message, MaildirMessage):
366            os.utime(tmp_path,
367                     (os.path.getatime(tmp_path), message.get_date()))
368        # No file modification should be done after the file is moved to its
369        # final position in order to prevent race conditions with changes
370        # from other programs
371        os.rename(tmp_path, new_path)
372
373    def get_message(self, key):
374        """Return a Message representation or raise a KeyError."""
375        subpath = self._lookup(key)
376        with open(os.path.join(self._path, subpath), 'rb') as f:
377            if self._factory:
378                msg = self._factory(f)
379            else:
380                msg = MaildirMessage(f)
381        subdir, name = os.path.split(subpath)
382        msg.set_subdir(subdir)
383        if self.colon in name:
384            msg.set_info(name.split(self.colon)[-1])
385        msg.set_date(os.path.getmtime(os.path.join(self._path, subpath)))
386        return msg
387
388    def get_bytes(self, key):
389        """Return a bytes representation or raise a KeyError."""
390        with open(os.path.join(self._path, self._lookup(key)), 'rb') as f:
391            return f.read().replace(linesep, b'\n')
392
393    def get_file(self, key):
394        """Return a file-like representation or raise a KeyError."""
395        f = open(os.path.join(self._path, self._lookup(key)), 'rb')
396        return _ProxyFile(f)
397
398    def get_info(self, key):
399        """Get the keyed message's "info" as a string."""
400        subpath = self._lookup(key)
401        if self.colon in subpath:
402            return subpath.split(self.colon)[-1]
403        return ''
404
405    def set_info(self, key, info: str):
406        """Set the keyed message's "info" string."""
407        if not isinstance(info, str):
408            raise TypeError(f'info must be a string: {type(info)}')
409        old_subpath = self._lookup(key)
410        new_subpath = old_subpath.split(self.colon)[0]
411        if info:
412            new_subpath += self.colon + info
413        if new_subpath == old_subpath:
414            return
415        old_path = os.path.join(self._path, old_subpath)
416        new_path = os.path.join(self._path, new_subpath)
417        os.rename(old_path, new_path)
418        self._toc[key] = new_subpath
419
420    def get_flags(self, key):
421        """Return as a string the standard flags that are set on the keyed message."""
422        info = self.get_info(key)
423        if info.startswith('2,'):
424            return info[2:]
425        return ''
426
427    def set_flags(self, key, flags: str):
428        """Set the given flags and unset all others on the keyed message."""
429        if not isinstance(flags, str):
430            raise TypeError(f'flags must be a string: {type(flags)}')
431        # TODO: check if flags are valid standard flag characters?
432        self.set_info(key, '2,' + ''.join(sorted(set(flags))))
433
434    def add_flag(self, key, flag: str):
435        """Set the given flag(s) without changing others on the keyed message."""
436        if not isinstance(flag, str):
437            raise TypeError(f'flag must be a string: {type(flag)}')
438        # TODO: check that flag is a valid standard flag character?
439        self.set_flags(key, ''.join(set(self.get_flags(key)) | set(flag)))
440
441    def remove_flag(self, key, flag: str):
442        """Unset the given string flag(s) without changing others on the keyed message."""
443        if not isinstance(flag, str):
444            raise TypeError(f'flag must be a string: {type(flag)}')
445        if self.get_flags(key):
446            self.set_flags(key, ''.join(set(self.get_flags(key)) - set(flag)))
447
448    def iterkeys(self):
449        """Return an iterator over keys."""
450        self._refresh()
451        for key in self._toc:
452            try:
453                self._lookup(key)
454            except KeyError:
455                continue
456            yield key
457
458    def __contains__(self, key):
459        """Return True if the keyed message exists, False otherwise."""
460        self._refresh()
461        return key in self._toc
462
463    def __len__(self):
464        """Return a count of messages in the mailbox."""
465        self._refresh()
466        return len(self._toc)
467
468    def flush(self):
469        """Write any pending changes to disk."""
470        # Maildir changes are always written immediately, so there's nothing
471        # to do.
472        pass
473
474    def lock(self):
475        """Lock the mailbox."""
476        return
477
478    def unlock(self):
479        """Unlock the mailbox if it is locked."""
480        return
481
482    def close(self):
483        """Flush and close the mailbox."""
484        return
485
486    def list_folders(self):
487        """Return a list of folder names."""
488        result = []
489        for entry in os.listdir(self._path):
490            if len(entry) > 1 and entry[0] == '.' and \
491               os.path.isdir(os.path.join(self._path, entry)):
492                result.append(entry[1:])
493        return result
494
495    def get_folder(self, folder):
496        """Return a Maildir instance for the named folder."""
497        return Maildir(os.path.join(self._path, '.' + folder),
498                       factory=self._factory,
499                       create=False)
500
501    def add_folder(self, folder):
502        """Create a folder and return a Maildir instance representing it."""
503        path = os.path.join(self._path, '.' + folder)
504        result = Maildir(path, factory=self._factory)
505        maildirfolder_path = os.path.join(path, 'maildirfolder')
506        if not os.path.exists(maildirfolder_path):
507            os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY,
508                0o666))
509        return result
510
511    def remove_folder(self, folder):
512        """Delete the named folder, which must be empty."""
513        path = os.path.join(self._path, '.' + folder)
514        for entry in os.listdir(os.path.join(path, 'new')) + \
515                     os.listdir(os.path.join(path, 'cur')):
516            if len(entry) < 1 or entry[0] != '.':
517                raise NotEmptyError('Folder contains message(s): %s' % folder)
518        for entry in os.listdir(path):
519            if entry != 'new' and entry != 'cur' and entry != 'tmp' and \
520               os.path.isdir(os.path.join(path, entry)):
521                raise NotEmptyError("Folder contains subdirectory '%s': %s" %
522                                    (folder, entry))
523        for root, dirs, files in os.walk(path, topdown=False):
524            for entry in files:
525                os.remove(os.path.join(root, entry))
526            for entry in dirs:
527                os.rmdir(os.path.join(root, entry))
528        os.rmdir(path)
529
530    def clean(self):
531        """Delete old files in "tmp"."""
532        now = time.time()
533        for entry in os.listdir(os.path.join(self._path, 'tmp')):
534            path = os.path.join(self._path, 'tmp', entry)
535            if now - os.path.getatime(path) > 129600:   # 60 * 60 * 36
536                os.remove(path)
537
538    _count = 1  # This is used to generate unique file names.
539
540    def _create_tmp(self):
541        """Create a file in the tmp subdirectory and open and return it."""
542        now = time.time()
543        hostname = socket.gethostname()
544        if '/' in hostname:
545            hostname = hostname.replace('/', r'\057')
546        if ':' in hostname:
547            hostname = hostname.replace(':', r'\072')
548        uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(),
549                                    Maildir._count, hostname)
550        path = os.path.join(self._path, 'tmp', uniq)
551        try:
552            os.stat(path)
553        except FileNotFoundError:
554            Maildir._count += 1
555            try:
556                return _create_carefully(path)
557            except FileExistsError:
558                pass
559
560        # Fall through to here if stat succeeded or open raised EEXIST.
561        raise ExternalClashError('Name clash prevented file creation: %s' %
562                                 path)
563
564    def _refresh(self):
565        """Update table of contents mapping."""
566        # If it has been less than two seconds since the last _refresh() call,
567        # we have to unconditionally re-read the mailbox just in case it has
568        # been modified, because os.path.mtime() has a 2 sec resolution in the
569        # most common worst case (FAT) and a 1 sec resolution typically.  This
570        # results in a few unnecessary re-reads when _refresh() is called
571        # multiple times in that interval, but once the clock ticks over, we
572        # will only re-read as needed.  Because the filesystem might be being
573        # served by an independent system with its own clock, we record and
574        # compare with the mtimes from the filesystem.  Because the other
575        # system's clock might be skewing relative to our clock, we add an
576        # extra delta to our wait.  The default is one tenth second, but is an
577        # instance variable and so can be adjusted if dealing with a
578        # particularly skewed or irregular system.
579        if time.time() - self._last_read > 2 + self._skewfactor:
580            refresh = False
581            for subdir in self._toc_mtimes:
582                mtime = os.path.getmtime(self._paths[subdir])
583                if mtime > self._toc_mtimes[subdir]:
584                    refresh = True
585                self._toc_mtimes[subdir] = mtime
586            if not refresh:
587                return
588        # Refresh toc
589        self._toc = {}
590        for subdir in self._toc_mtimes:
591            path = self._paths[subdir]
592            for entry in os.listdir(path):
593                if entry.startswith('.'):
594                    continue
595                p = os.path.join(path, entry)
596                if os.path.isdir(p):
597                    continue
598                uniq = entry.split(self.colon)[0]
599                self._toc[uniq] = os.path.join(subdir, entry)
600        self._last_read = time.time()
601
602    def _lookup(self, key):
603        """Use TOC to return subpath for given key, or raise a KeyError."""
604        try:
605            if os.path.exists(os.path.join(self._path, self._toc[key])):
606                return self._toc[key]
607        except KeyError:
608            pass
609        self._refresh()
610        try:
611            return self._toc[key]
612        except KeyError:
613            raise KeyError('No message with key: %s' % key) from None
614
615    # This method is for backward compatibility only.
616    def next(self):
617        """Return the next message in a one-time iteration."""
618        if not hasattr(self, '_onetime_keys'):
619            self._onetime_keys = self.iterkeys()
620        while True:
621            try:
622                return self[next(self._onetime_keys)]
623            except StopIteration:
624                return None
625            except KeyError:
626                continue
627
628
629class _singlefileMailbox(Mailbox):
630    """A single-file mailbox."""
631
632    def __init__(self, path, factory=None, create=True):
633        """Initialize a single-file mailbox."""
634        Mailbox.__init__(self, path, factory, create)
635        try:
636            f = open(self._path, 'rb+')
637        except OSError as e:
638            if e.errno == errno.ENOENT:
639                if create:
640                    f = open(self._path, 'wb+')
641                else:
642                    raise NoSuchMailboxError(self._path)
643            elif e.errno in (errno.EACCES, errno.EROFS):
644                f = open(self._path, 'rb')
645            else:
646                raise
647        self._file = f
648        self._toc = None
649        self._next_key = 0
650        self._pending = False       # No changes require rewriting the file.
651        self._pending_sync = False  # No need to sync the file
652        self._locked = False
653        self._file_length = None    # Used to record mailbox size
654
655    def add(self, message):
656        """Add message and return assigned key."""
657        self._lookup()
658        self._toc[self._next_key] = self._append_message(message)
659        self._next_key += 1
660        # _append_message appends the message to the mailbox file. We
661        # don't need a full rewrite + rename, sync is enough.
662        self._pending_sync = True
663        return self._next_key - 1
664
665    def remove(self, key):
666        """Remove the keyed message; raise KeyError if it doesn't exist."""
667        self._lookup(key)
668        del self._toc[key]
669        self._pending = True
670
671    def __setitem__(self, key, message):
672        """Replace the keyed message; raise KeyError if it doesn't exist."""
673        self._lookup(key)
674        self._toc[key] = self._append_message(message)
675        self._pending = True
676
677    def iterkeys(self):
678        """Return an iterator over keys."""
679        self._lookup()
680        yield from self._toc.keys()
681
682    def __contains__(self, key):
683        """Return True if the keyed message exists, False otherwise."""
684        self._lookup()
685        return key in self._toc
686
687    def __len__(self):
688        """Return a count of messages in the mailbox."""
689        self._lookup()
690        return len(self._toc)
691
692    def lock(self):
693        """Lock the mailbox."""
694        if not self._locked:
695            _lock_file(self._file)
696            self._locked = True
697
698    def unlock(self):
699        """Unlock the mailbox if it is locked."""
700        if self._locked:
701            _unlock_file(self._file)
702            self._locked = False
703
704    def flush(self):
705        """Write any pending changes to disk."""
706        if not self._pending:
707            if self._pending_sync:
708                # Messages have only been added, so syncing the file
709                # is enough.
710                _sync_flush(self._file)
711                self._pending_sync = False
712            return
713
714        # In order to be writing anything out at all, self._toc must
715        # already have been generated (and presumably has been modified
716        # by adding or deleting an item).
717        assert self._toc is not None
718
719        # Check length of self._file; if it's changed, some other process
720        # has modified the mailbox since we scanned it.
721        self._file.seek(0, 2)
722        cur_len = self._file.tell()
723        if cur_len != self._file_length:
724            raise ExternalClashError('Size of mailbox file changed '
725                                     '(expected %i, found %i)' %
726                                     (self._file_length, cur_len))
727
728        new_file = _create_temporary(self._path)
729        try:
730            new_toc = {}
731            self._pre_mailbox_hook(new_file)
732            for key in sorted(self._toc.keys()):
733                start, stop = self._toc[key]
734                self._file.seek(start)
735                self._pre_message_hook(new_file)
736                new_start = new_file.tell()
737                while True:
738                    buffer = self._file.read(min(4096,
739                                                 stop - self._file.tell()))
740                    if not buffer:
741                        break
742                    new_file.write(buffer)
743                new_toc[key] = (new_start, new_file.tell())
744                self._post_message_hook(new_file)
745            self._file_length = new_file.tell()
746        except:
747            new_file.close()
748            os.remove(new_file.name)
749            raise
750        _sync_close(new_file)
751        # self._file is about to get replaced, so no need to sync.
752        self._file.close()
753        # Make sure the new file's mode and owner are the same as the old file's
754        info = os.stat(self._path)
755        os.chmod(new_file.name, info.st_mode)
756        try:
757            os.chown(new_file.name, info.st_uid, info.st_gid)
758        except (AttributeError, OSError):
759            pass
760        try:
761            os.rename(new_file.name, self._path)
762        except FileExistsError:
763            os.remove(self._path)
764            os.rename(new_file.name, self._path)
765        self._file = open(self._path, 'rb+')
766        self._toc = new_toc
767        self._pending = False
768        self._pending_sync = False
769        if self._locked:
770            _lock_file(self._file, dotlock=False)
771
772    def _pre_mailbox_hook(self, f):
773        """Called before writing the mailbox to file f."""
774        return
775
776    def _pre_message_hook(self, f):
777        """Called before writing each message to file f."""
778        return
779
780    def _post_message_hook(self, f):
781        """Called after writing each message to file f."""
782        return
783
784    def close(self):
785        """Flush and close the mailbox."""
786        try:
787            self.flush()
788        finally:
789            try:
790                if self._locked:
791                    self.unlock()
792            finally:
793                self._file.close()  # Sync has been done by self.flush() above.
794
795    def _lookup(self, key=None):
796        """Return (start, stop) or raise KeyError."""
797        if self._toc is None:
798            self._generate_toc()
799        if key is not None:
800            try:
801                return self._toc[key]
802            except KeyError:
803                raise KeyError('No message with key: %s' % key) from None
804
805    def _append_message(self, message):
806        """Append message to mailbox and return (start, stop) offsets."""
807        self._file.seek(0, 2)
808        before = self._file.tell()
809        if len(self._toc) == 0 and not self._pending:
810            # This is the first message, and the _pre_mailbox_hook
811            # hasn't yet been called. If self._pending is True,
812            # messages have been removed, so _pre_mailbox_hook must
813            # have been called already.
814            self._pre_mailbox_hook(self._file)
815        try:
816            self._pre_message_hook(self._file)
817            offsets = self._install_message(message)
818            self._post_message_hook(self._file)
819        except BaseException:
820            self._file.truncate(before)
821            raise
822        self._file.flush()
823        self._file_length = self._file.tell()  # Record current length of mailbox
824        return offsets
825
826
827
828class _mboxMMDF(_singlefileMailbox):
829    """An mbox or MMDF mailbox."""
830
831    _mangle_from_ = True
832
833    def get_message(self, key):
834        """Return a Message representation or raise a KeyError."""
835        start, stop = self._lookup(key)
836        self._file.seek(start)
837        from_line = self._file.readline().replace(linesep, b'').decode('ascii')
838        string = self._file.read(stop - self._file.tell())
839        msg = self._message_factory(string.replace(linesep, b'\n'))
840        msg.set_unixfrom(from_line)
841        msg.set_from(from_line[5:])
842        return msg
843
844    def get_string(self, key, from_=False):
845        """Return a string representation or raise a KeyError."""
846        return email.message_from_bytes(
847            self.get_bytes(key, from_)).as_string(unixfrom=from_)
848
849    def get_bytes(self, key, from_=False):
850        """Return a string representation or raise a KeyError."""
851        start, stop = self._lookup(key)
852        self._file.seek(start)
853        if not from_:
854            self._file.readline()
855        string = self._file.read(stop - self._file.tell())
856        return string.replace(linesep, b'\n')
857
858    def get_file(self, key, from_=False):
859        """Return a file-like representation or raise a KeyError."""
860        start, stop = self._lookup(key)
861        self._file.seek(start)
862        if not from_:
863            self._file.readline()
864        return _PartialFile(self._file, self._file.tell(), stop)
865
866    def _install_message(self, message):
867        """Format a message and blindly write to self._file."""
868        from_line = None
869        if isinstance(message, str):
870            message = self._string_to_bytes(message)
871        if isinstance(message, bytes) and message.startswith(b'From '):
872            newline = message.find(b'\n')
873            if newline != -1:
874                from_line = message[:newline]
875                message = message[newline + 1:]
876            else:
877                from_line = message
878                message = b''
879        elif isinstance(message, _mboxMMDFMessage):
880            author = message.get_from().encode('ascii')
881            from_line = b'From ' + author
882        elif isinstance(message, email.message.Message):
883            from_line = message.get_unixfrom()  # May be None.
884            if from_line is not None:
885                from_line = from_line.encode('ascii')
886        if from_line is None:
887            from_line = b'From MAILER-DAEMON ' + time.asctime(time.gmtime()).encode()
888        start = self._file.tell()
889        self._file.write(from_line + linesep)
890        self._dump_message(message, self._file, self._mangle_from_)
891        stop = self._file.tell()
892        return (start, stop)
893
894
895class mbox(_mboxMMDF):
896    """A classic mbox mailbox."""
897
898    _mangle_from_ = True
899
900    # All messages must end in a newline character, and
901    # _post_message_hooks outputs an empty line between messages.
902    _append_newline = True
903
904    def __init__(self, path, factory=None, create=True):
905        """Initialize an mbox mailbox."""
906        self._message_factory = mboxMessage
907        _mboxMMDF.__init__(self, path, factory, create)
908
909    def _post_message_hook(self, f):
910        """Called after writing each message to file f."""
911        f.write(linesep)
912
913    def _generate_toc(self):
914        """Generate key-to-(start, stop) table of contents."""
915        starts, stops = [], []
916        last_was_empty = False
917        self._file.seek(0)
918        while True:
919            line_pos = self._file.tell()
920            line = self._file.readline()
921            if line.startswith(b'From '):
922                if len(stops) < len(starts):
923                    if last_was_empty:
924                        stops.append(line_pos - len(linesep))
925                    else:
926                        # The last line before the "From " line wasn't
927                        # blank, but we consider it a start of a
928                        # message anyway.
929                        stops.append(line_pos)
930                starts.append(line_pos)
931                last_was_empty = False
932            elif not line:
933                if last_was_empty:
934                    stops.append(line_pos - len(linesep))
935                else:
936                    stops.append(line_pos)
937                break
938            elif line == linesep:
939                last_was_empty = True
940            else:
941                last_was_empty = False
942        self._toc = dict(enumerate(zip(starts, stops)))
943        self._next_key = len(self._toc)
944        self._file_length = self._file.tell()
945
946
947class MMDF(_mboxMMDF):
948    """An MMDF mailbox."""
949
950    def __init__(self, path, factory=None, create=True):
951        """Initialize an MMDF mailbox."""
952        self._message_factory = MMDFMessage
953        _mboxMMDF.__init__(self, path, factory, create)
954
955    def _pre_message_hook(self, f):
956        """Called before writing each message to file f."""
957        f.write(b'\001\001\001\001' + linesep)
958
959    def _post_message_hook(self, f):
960        """Called after writing each message to file f."""
961        f.write(linesep + b'\001\001\001\001' + linesep)
962
963    def _generate_toc(self):
964        """Generate key-to-(start, stop) table of contents."""
965        starts, stops = [], []
966        self._file.seek(0)
967        next_pos = 0
968        while True:
969            line_pos = next_pos
970            line = self._file.readline()
971            next_pos = self._file.tell()
972            if line.startswith(b'\001\001\001\001' + linesep):
973                starts.append(next_pos)
974                while True:
975                    line_pos = next_pos
976                    line = self._file.readline()
977                    next_pos = self._file.tell()
978                    if line == b'\001\001\001\001' + linesep:
979                        stops.append(line_pos - len(linesep))
980                        break
981                    elif not line:
982                        stops.append(line_pos)
983                        break
984            elif not line:
985                break
986        self._toc = dict(enumerate(zip(starts, stops)))
987        self._next_key = len(self._toc)
988        self._file.seek(0, 2)
989        self._file_length = self._file.tell()
990
991
992class MH(Mailbox):
993    """An MH mailbox."""
994
995    def __init__(self, path, factory=None, create=True):
996        """Initialize an MH instance."""
997        Mailbox.__init__(self, path, factory, create)
998        if not os.path.exists(self._path):
999            if create:
1000                os.mkdir(self._path, 0o700)
1001                os.close(os.open(os.path.join(self._path, '.mh_sequences'),
1002                                 os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600))
1003            else:
1004                raise NoSuchMailboxError(self._path)
1005        self._locked = False
1006
1007    def add(self, message):
1008        """Add message and return assigned key."""
1009        keys = self.keys()
1010        if len(keys) == 0:
1011            new_key = 1
1012        else:
1013            new_key = max(keys) + 1
1014        new_path = os.path.join(self._path, str(new_key))
1015        f = _create_carefully(new_path)
1016        closed = False
1017        try:
1018            if self._locked:
1019                _lock_file(f)
1020            try:
1021                try:
1022                    self._dump_message(message, f)
1023                except BaseException:
1024                    # Unlock and close so it can be deleted on Windows
1025                    if self._locked:
1026                        _unlock_file(f)
1027                    _sync_close(f)
1028                    closed = True
1029                    os.remove(new_path)
1030                    raise
1031                if isinstance(message, MHMessage):
1032                    self._dump_sequences(message, new_key)
1033            finally:
1034                if self._locked:
1035                    _unlock_file(f)
1036        finally:
1037            if not closed:
1038                _sync_close(f)
1039        return new_key
1040
1041    def remove(self, key):
1042        """Remove the keyed message; raise KeyError if it doesn't exist."""
1043        path = os.path.join(self._path, str(key))
1044        try:
1045            f = open(path, 'rb+')
1046        except OSError as e:
1047            if e.errno == errno.ENOENT:
1048                raise KeyError('No message with key: %s' % key)
1049            else:
1050                raise
1051        else:
1052            f.close()
1053            os.remove(path)
1054
1055    def __setitem__(self, key, message):
1056        """Replace the keyed message; raise KeyError if it doesn't exist."""
1057        path = os.path.join(self._path, str(key))
1058        try:
1059            f = open(path, 'rb+')
1060        except OSError as e:
1061            if e.errno == errno.ENOENT:
1062                raise KeyError('No message with key: %s' % key)
1063            else:
1064                raise
1065        try:
1066            if self._locked:
1067                _lock_file(f)
1068            try:
1069                os.close(os.open(path, os.O_WRONLY | os.O_TRUNC))
1070                self._dump_message(message, f)
1071                if isinstance(message, MHMessage):
1072                    self._dump_sequences(message, key)
1073            finally:
1074                if self._locked:
1075                    _unlock_file(f)
1076        finally:
1077            _sync_close(f)
1078
1079    def get_message(self, key):
1080        """Return a Message representation or raise a KeyError."""
1081        try:
1082            if self._locked:
1083                f = open(os.path.join(self._path, str(key)), 'rb+')
1084            else:
1085                f = open(os.path.join(self._path, str(key)), 'rb')
1086        except OSError as e:
1087            if e.errno == errno.ENOENT:
1088                raise KeyError('No message with key: %s' % key)
1089            else:
1090                raise
1091        with f:
1092            if self._locked:
1093                _lock_file(f)
1094            try:
1095                msg = MHMessage(f)
1096            finally:
1097                if self._locked:
1098                    _unlock_file(f)
1099        for name, key_list in self.get_sequences().items():
1100            if key in key_list:
1101                msg.add_sequence(name)
1102        return msg
1103
1104    def get_bytes(self, key):
1105        """Return a bytes representation or raise a KeyError."""
1106        try:
1107            if self._locked:
1108                f = open(os.path.join(self._path, str(key)), 'rb+')
1109            else:
1110                f = open(os.path.join(self._path, str(key)), 'rb')
1111        except OSError as e:
1112            if e.errno == errno.ENOENT:
1113                raise KeyError('No message with key: %s' % key)
1114            else:
1115                raise
1116        with f:
1117            if self._locked:
1118                _lock_file(f)
1119            try:
1120                return f.read().replace(linesep, b'\n')
1121            finally:
1122                if self._locked:
1123                    _unlock_file(f)
1124
1125    def get_file(self, key):
1126        """Return a file-like representation or raise a KeyError."""
1127        try:
1128            f = open(os.path.join(self._path, str(key)), 'rb')
1129        except OSError as e:
1130            if e.errno == errno.ENOENT:
1131                raise KeyError('No message with key: %s' % key)
1132            else:
1133                raise
1134        return _ProxyFile(f)
1135
1136    def iterkeys(self):
1137        """Return an iterator over keys."""
1138        return iter(sorted(int(entry) for entry in os.listdir(self._path)
1139                                      if entry.isdigit()))
1140
1141    def __contains__(self, key):
1142        """Return True if the keyed message exists, False otherwise."""
1143        return os.path.exists(os.path.join(self._path, str(key)))
1144
1145    def __len__(self):
1146        """Return a count of messages in the mailbox."""
1147        return len(list(self.iterkeys()))
1148
1149    def _open_mh_sequences_file(self, text):
1150        mode = '' if text else 'b'
1151        kwargs = {'encoding': 'ASCII'} if text else {}
1152        path = os.path.join(self._path, '.mh_sequences')
1153        while True:
1154            try:
1155                return open(path, 'r+' + mode, **kwargs)
1156            except FileNotFoundError:
1157                pass
1158            try:
1159                return open(path, 'x+' + mode, **kwargs)
1160            except FileExistsError:
1161                pass
1162
1163    def lock(self):
1164        """Lock the mailbox."""
1165        if not self._locked:
1166            self._file = self._open_mh_sequences_file(text=False)
1167            _lock_file(self._file)
1168            self._locked = True
1169
1170    def unlock(self):
1171        """Unlock the mailbox if it is locked."""
1172        if self._locked:
1173            _unlock_file(self._file)
1174            _sync_close(self._file)
1175            del self._file
1176            self._locked = False
1177
1178    def flush(self):
1179        """Write any pending changes to the disk."""
1180        return
1181
1182    def close(self):
1183        """Flush and close the mailbox."""
1184        if self._locked:
1185            self.unlock()
1186
1187    def list_folders(self):
1188        """Return a list of folder names."""
1189        result = []
1190        for entry in os.listdir(self._path):
1191            if os.path.isdir(os.path.join(self._path, entry)):
1192                result.append(entry)
1193        return result
1194
1195    def get_folder(self, folder):
1196        """Return an MH instance for the named folder."""
1197        return MH(os.path.join(self._path, folder),
1198                  factory=self._factory, create=False)
1199
1200    def add_folder(self, folder):
1201        """Create a folder and return an MH instance representing it."""
1202        return MH(os.path.join(self._path, folder),
1203                  factory=self._factory)
1204
1205    def remove_folder(self, folder):
1206        """Delete the named folder, which must be empty."""
1207        path = os.path.join(self._path, folder)
1208        entries = os.listdir(path)
1209        if entries == ['.mh_sequences']:
1210            os.remove(os.path.join(path, '.mh_sequences'))
1211        elif entries == []:
1212            pass
1213        else:
1214            raise NotEmptyError('Folder not empty: %s' % self._path)
1215        os.rmdir(path)
1216
1217    def get_sequences(self):
1218        """Return a name-to-key-list dictionary to define each sequence."""
1219        results = {}
1220        try:
1221            f = open(os.path.join(self._path, '.mh_sequences'), 'r', encoding='ASCII')
1222        except FileNotFoundError:
1223            return results
1224        with f:
1225            all_keys = set(self.keys())
1226            for line in f:
1227                try:
1228                    name, contents = line.split(':')
1229                    keys = set()
1230                    for spec in contents.split():
1231                        if spec.isdigit():
1232                            keys.add(int(spec))
1233                        else:
1234                            start, stop = (int(x) for x in spec.split('-'))
1235                            keys.update(range(start, stop + 1))
1236                    results[name] = [key for key in sorted(keys) \
1237                                         if key in all_keys]
1238                    if len(results[name]) == 0:
1239                        del results[name]
1240                except ValueError:
1241                    raise FormatError('Invalid sequence specification: %s' %
1242                                      line.rstrip())
1243        return results
1244
1245    def set_sequences(self, sequences):
1246        """Set sequences using the given name-to-key-list dictionary."""
1247        f = self._open_mh_sequences_file(text=True)
1248        try:
1249            os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC))
1250            for name, keys in sequences.items():
1251                if len(keys) == 0:
1252                    continue
1253                f.write(name + ':')
1254                prev = None
1255                completing = False
1256                for key in sorted(set(keys)):
1257                    if key - 1 == prev:
1258                        if not completing:
1259                            completing = True
1260                            f.write('-')
1261                    elif completing:
1262                        completing = False
1263                        f.write('%s %s' % (prev, key))
1264                    else:
1265                        f.write(' %s' % key)
1266                    prev = key
1267                if completing:
1268                    f.write(str(prev) + '\n')
1269                else:
1270                    f.write('\n')
1271        finally:
1272            _sync_close(f)
1273
1274    def pack(self):
1275        """Re-name messages to eliminate numbering gaps. Invalidates keys."""
1276        sequences = self.get_sequences()
1277        prev = 0
1278        changes = []
1279        for key in self.iterkeys():
1280            if key - 1 != prev:
1281                changes.append((key, prev + 1))
1282                try:
1283                    os.link(os.path.join(self._path, str(key)),
1284                            os.path.join(self._path, str(prev + 1)))
1285                except (AttributeError, PermissionError):
1286                    os.rename(os.path.join(self._path, str(key)),
1287                              os.path.join(self._path, str(prev + 1)))
1288                else:
1289                    os.unlink(os.path.join(self._path, str(key)))
1290            prev += 1
1291        self._next_key = prev + 1
1292        if len(changes) == 0:
1293            return
1294        for name, key_list in sequences.items():
1295            for old, new in changes:
1296                if old in key_list:
1297                    key_list[key_list.index(old)] = new
1298        self.set_sequences(sequences)
1299
1300    def _dump_sequences(self, message, key):
1301        """Inspect a new MHMessage and update sequences appropriately."""
1302        pending_sequences = message.get_sequences()
1303        all_sequences = self.get_sequences()
1304        for name, key_list in all_sequences.items():
1305            if name in pending_sequences:
1306                key_list.append(key)
1307            elif key in key_list:
1308                del key_list[key_list.index(key)]
1309        for sequence in pending_sequences:
1310            if sequence not in all_sequences:
1311                all_sequences[sequence] = [key]
1312        self.set_sequences(all_sequences)
1313
1314
1315class Babyl(_singlefileMailbox):
1316    """An Rmail-style Babyl mailbox."""
1317
1318    _special_labels = frozenset({'unseen', 'deleted', 'filed', 'answered',
1319                                 'forwarded', 'edited', 'resent'})
1320
1321    def __init__(self, path, factory=None, create=True):
1322        """Initialize a Babyl mailbox."""
1323        _singlefileMailbox.__init__(self, path, factory, create)
1324        self._labels = {}
1325
1326    def add(self, message):
1327        """Add message and return assigned key."""
1328        key = _singlefileMailbox.add(self, message)
1329        if isinstance(message, BabylMessage):
1330            self._labels[key] = message.get_labels()
1331        return key
1332
1333    def remove(self, key):
1334        """Remove the keyed message; raise KeyError if it doesn't exist."""
1335        _singlefileMailbox.remove(self, key)
1336        if key in self._labels:
1337            del self._labels[key]
1338
1339    def __setitem__(self, key, message):
1340        """Replace the keyed message; raise KeyError if it doesn't exist."""
1341        _singlefileMailbox.__setitem__(self, key, message)
1342        if isinstance(message, BabylMessage):
1343            self._labels[key] = message.get_labels()
1344
1345    def get_message(self, key):
1346        """Return a Message representation or raise a KeyError."""
1347        start, stop = self._lookup(key)
1348        self._file.seek(start)
1349        self._file.readline()   # Skip b'1,' line specifying labels.
1350        original_headers = io.BytesIO()
1351        while True:
1352            line = self._file.readline()
1353            if line == b'*** EOOH ***' + linesep or not line:
1354                break
1355            original_headers.write(line.replace(linesep, b'\n'))
1356        visible_headers = io.BytesIO()
1357        while True:
1358            line = self._file.readline()
1359            if line == linesep or not line:
1360                break
1361            visible_headers.write(line.replace(linesep, b'\n'))
1362        # Read up to the stop, or to the end
1363        n = stop - self._file.tell()
1364        assert n >= 0
1365        body = self._file.read(n)
1366        body = body.replace(linesep, b'\n')
1367        msg = BabylMessage(original_headers.getvalue() + body)
1368        msg.set_visible(visible_headers.getvalue())
1369        if key in self._labels:
1370            msg.set_labels(self._labels[key])
1371        return msg
1372
1373    def get_bytes(self, key):
1374        """Return a string representation or raise a KeyError."""
1375        start, stop = self._lookup(key)
1376        self._file.seek(start)
1377        self._file.readline()   # Skip b'1,' line specifying labels.
1378        original_headers = io.BytesIO()
1379        while True:
1380            line = self._file.readline()
1381            if line == b'*** EOOH ***' + linesep or not line:
1382                break
1383            original_headers.write(line.replace(linesep, b'\n'))
1384        while True:
1385            line = self._file.readline()
1386            if line == linesep or not line:
1387                break
1388        headers = original_headers.getvalue()
1389        n = stop - self._file.tell()
1390        assert n >= 0
1391        data = self._file.read(n)
1392        data = data.replace(linesep, b'\n')
1393        return headers + data
1394
1395    def get_file(self, key):
1396        """Return a file-like representation or raise a KeyError."""
1397        return io.BytesIO(self.get_bytes(key).replace(b'\n', linesep))
1398
1399    def get_labels(self):
1400        """Return a list of user-defined labels in the mailbox."""
1401        self._lookup()
1402        labels = set()
1403        for label_list in self._labels.values():
1404            labels.update(label_list)
1405        labels.difference_update(self._special_labels)
1406        return list(labels)
1407
1408    def _generate_toc(self):
1409        """Generate key-to-(start, stop) table of contents."""
1410        starts, stops = [], []
1411        self._file.seek(0)
1412        next_pos = 0
1413        label_lists = []
1414        while True:
1415            line_pos = next_pos
1416            line = self._file.readline()
1417            next_pos = self._file.tell()
1418            if line == b'\037\014' + linesep:
1419                if len(stops) < len(starts):
1420                    stops.append(line_pos - len(linesep))
1421                starts.append(next_pos)
1422                labels = [label.strip() for label
1423                                        in self._file.readline()[1:].split(b',')
1424                                        if label.strip()]
1425                label_lists.append(labels)
1426            elif line == b'\037' or line == b'\037' + linesep:
1427                if len(stops) < len(starts):
1428                    stops.append(line_pos - len(linesep))
1429            elif not line:
1430                stops.append(line_pos - len(linesep))
1431                break
1432        self._toc = dict(enumerate(zip(starts, stops)))
1433        self._labels = dict(enumerate(label_lists))
1434        self._next_key = len(self._toc)
1435        self._file.seek(0, 2)
1436        self._file_length = self._file.tell()
1437
1438    def _pre_mailbox_hook(self, f):
1439        """Called before writing the mailbox to file f."""
1440        babyl = b'BABYL OPTIONS:' + linesep
1441        babyl += b'Version: 5' + linesep
1442        labels = self.get_labels()
1443        labels = (label.encode() for label in labels)
1444        babyl += b'Labels:' + b','.join(labels) + linesep
1445        babyl += b'\037'
1446        f.write(babyl)
1447
1448    def _pre_message_hook(self, f):
1449        """Called before writing each message to file f."""
1450        f.write(b'\014' + linesep)
1451
1452    def _post_message_hook(self, f):
1453        """Called after writing each message to file f."""
1454        f.write(linesep + b'\037')
1455
1456    def _install_message(self, message):
1457        """Write message contents and return (start, stop)."""
1458        start = self._file.tell()
1459        if isinstance(message, BabylMessage):
1460            special_labels = []
1461            labels = []
1462            for label in message.get_labels():
1463                if label in self._special_labels:
1464                    special_labels.append(label)
1465                else:
1466                    labels.append(label)
1467            self._file.write(b'1')
1468            for label in special_labels:
1469                self._file.write(b', ' + label.encode())
1470            self._file.write(b',,')
1471            for label in labels:
1472                self._file.write(b' ' + label.encode() + b',')
1473            self._file.write(linesep)
1474        else:
1475            self._file.write(b'1,,' + linesep)
1476        if isinstance(message, email.message.Message):
1477            orig_buffer = io.BytesIO()
1478            orig_generator = email.generator.BytesGenerator(orig_buffer, False, 0)
1479            orig_generator.flatten(message)
1480            orig_buffer.seek(0)
1481            while True:
1482                line = orig_buffer.readline()
1483                self._file.write(line.replace(b'\n', linesep))
1484                if line == b'\n' or not line:
1485                    break
1486            self._file.write(b'*** EOOH ***' + linesep)
1487            if isinstance(message, BabylMessage):
1488                vis_buffer = io.BytesIO()
1489                vis_generator = email.generator.BytesGenerator(vis_buffer, False, 0)
1490                vis_generator.flatten(message.get_visible())
1491                while True:
1492                    line = vis_buffer.readline()
1493                    self._file.write(line.replace(b'\n', linesep))
1494                    if line == b'\n' or not line:
1495                        break
1496            else:
1497                orig_buffer.seek(0)
1498                while True:
1499                    line = orig_buffer.readline()
1500                    self._file.write(line.replace(b'\n', linesep))
1501                    if line == b'\n' or not line:
1502                        break
1503            while True:
1504                buffer = orig_buffer.read(4096) # Buffer size is arbitrary.
1505                if not buffer:
1506                    break
1507                self._file.write(buffer.replace(b'\n', linesep))
1508        elif isinstance(message, (bytes, str, io.StringIO)):
1509            if isinstance(message, io.StringIO):
1510                warnings.warn("Use of StringIO input is deprecated, "
1511                    "use BytesIO instead", DeprecationWarning, 3)
1512                message = message.getvalue()
1513            if isinstance(message, str):
1514                message = self._string_to_bytes(message)
1515            body_start = message.find(b'\n\n') + 2
1516            if body_start - 2 != -1:
1517                self._file.write(message[:body_start].replace(b'\n', linesep))
1518                self._file.write(b'*** EOOH ***' + linesep)
1519                self._file.write(message[:body_start].replace(b'\n', linesep))
1520                self._file.write(message[body_start:].replace(b'\n', linesep))
1521            else:
1522                self._file.write(b'*** EOOH ***' + linesep + linesep)
1523                self._file.write(message.replace(b'\n', linesep))
1524        elif hasattr(message, 'readline'):
1525            if hasattr(message, 'buffer'):
1526                warnings.warn("Use of text mode files is deprecated, "
1527                    "use a binary mode file instead", DeprecationWarning, 3)
1528                message = message.buffer
1529            original_pos = message.tell()
1530            first_pass = True
1531            while True:
1532                line = message.readline()
1533                # Universal newline support.
1534                if line.endswith(b'\r\n'):
1535                    line = line[:-2] + b'\n'
1536                elif line.endswith(b'\r'):
1537                    line = line[:-1] + b'\n'
1538                self._file.write(line.replace(b'\n', linesep))
1539                if line == b'\n' or not line:
1540                    if first_pass:
1541                        first_pass = False
1542                        self._file.write(b'*** EOOH ***' + linesep)
1543                        message.seek(original_pos)
1544                    else:
1545                        break
1546            while True:
1547                line = message.readline()
1548                if not line:
1549                    break
1550                # Universal newline support.
1551                if line.endswith(b'\r\n'):
1552                    line = line[:-2] + linesep
1553                elif line.endswith(b'\r'):
1554                    line = line[:-1] + linesep
1555                elif line.endswith(b'\n'):
1556                    line = line[:-1] + linesep
1557                self._file.write(line)
1558        else:
1559            raise TypeError('Invalid message type: %s' % type(message))
1560        stop = self._file.tell()
1561        return (start, stop)
1562
1563
1564class Message(email.message.Message):
1565    """Message with mailbox-format-specific properties."""
1566
1567    def __init__(self, message=None):
1568        """Initialize a Message instance."""
1569        if isinstance(message, email.message.Message):
1570            self._become_message(copy.deepcopy(message))
1571            if isinstance(message, Message):
1572                message._explain_to(self)
1573        elif isinstance(message, bytes):
1574            self._become_message(email.message_from_bytes(message))
1575        elif isinstance(message, str):
1576            self._become_message(email.message_from_string(message))
1577        elif isinstance(message, io.TextIOWrapper):
1578            self._become_message(email.message_from_file(message))
1579        elif hasattr(message, "read"):
1580            self._become_message(email.message_from_binary_file(message))
1581        elif message is None:
1582            email.message.Message.__init__(self)
1583        else:
1584            raise TypeError('Invalid message type: %s' % type(message))
1585
1586    def _become_message(self, message):
1587        """Assume the non-format-specific state of message."""
1588        type_specific = getattr(message, '_type_specific_attributes', [])
1589        for name in message.__dict__:
1590            if name not in type_specific:
1591                self.__dict__[name] = message.__dict__[name]
1592
1593    def _explain_to(self, message):
1594        """Copy format-specific state to message insofar as possible."""
1595        if isinstance(message, Message):
1596            return  # There's nothing format-specific to explain.
1597        else:
1598            raise TypeError('Cannot convert to specified type')
1599
1600
1601class MaildirMessage(Message):
1602    """Message with Maildir-specific properties."""
1603
1604    _type_specific_attributes = ['_subdir', '_info', '_date']
1605
1606    def __init__(self, message=None):
1607        """Initialize a MaildirMessage instance."""
1608        self._subdir = 'new'
1609        self._info = ''
1610        self._date = time.time()
1611        Message.__init__(self, message)
1612
1613    def get_subdir(self):
1614        """Return 'new' or 'cur'."""
1615        return self._subdir
1616
1617    def set_subdir(self, subdir):
1618        """Set subdir to 'new' or 'cur'."""
1619        if subdir == 'new' or subdir == 'cur':
1620            self._subdir = subdir
1621        else:
1622            raise ValueError("subdir must be 'new' or 'cur': %s" % subdir)
1623
1624    def get_flags(self):
1625        """Return as a string the flags that are set."""
1626        if self._info.startswith('2,'):
1627            return self._info[2:]
1628        else:
1629            return ''
1630
1631    def set_flags(self, flags):
1632        """Set the given flags and unset all others."""
1633        self._info = '2,' + ''.join(sorted(flags))
1634
1635    def add_flag(self, flag):
1636        """Set the given flag(s) without changing others."""
1637        self.set_flags(''.join(set(self.get_flags()) | set(flag)))
1638
1639    def remove_flag(self, flag):
1640        """Unset the given string flag(s) without changing others."""
1641        if self.get_flags():
1642            self.set_flags(''.join(set(self.get_flags()) - set(flag)))
1643
1644    def get_date(self):
1645        """Return delivery date of message, in seconds since the epoch."""
1646        return self._date
1647
1648    def set_date(self, date):
1649        """Set delivery date of message, in seconds since the epoch."""
1650        try:
1651            self._date = float(date)
1652        except ValueError:
1653            raise TypeError("can't convert to float: %s" % date) from None
1654
1655    def get_info(self):
1656        """Get the message's "info" as a string."""
1657        return self._info
1658
1659    def set_info(self, info):
1660        """Set the message's "info" string."""
1661        if isinstance(info, str):
1662            self._info = info
1663        else:
1664            raise TypeError('info must be a string: %s' % type(info))
1665
1666    def _explain_to(self, message):
1667        """Copy Maildir-specific state to message insofar as possible."""
1668        if isinstance(message, MaildirMessage):
1669            message.set_flags(self.get_flags())
1670            message.set_subdir(self.get_subdir())
1671            message.set_date(self.get_date())
1672        elif isinstance(message, _mboxMMDFMessage):
1673            flags = set(self.get_flags())
1674            if 'S' in flags:
1675                message.add_flag('R')
1676            if self.get_subdir() == 'cur':
1677                message.add_flag('O')
1678            if 'T' in flags:
1679                message.add_flag('D')
1680            if 'F' in flags:
1681                message.add_flag('F')
1682            if 'R' in flags:
1683                message.add_flag('A')
1684            message.set_from('MAILER-DAEMON', time.gmtime(self.get_date()))
1685        elif isinstance(message, MHMessage):
1686            flags = set(self.get_flags())
1687            if 'S' not in flags:
1688                message.add_sequence('unseen')
1689            if 'R' in flags:
1690                message.add_sequence('replied')
1691            if 'F' in flags:
1692                message.add_sequence('flagged')
1693        elif isinstance(message, BabylMessage):
1694            flags = set(self.get_flags())
1695            if 'S' not in flags:
1696                message.add_label('unseen')
1697            if 'T' in flags:
1698                message.add_label('deleted')
1699            if 'R' in flags:
1700                message.add_label('answered')
1701            if 'P' in flags:
1702                message.add_label('forwarded')
1703        elif isinstance(message, Message):
1704            pass
1705        else:
1706            raise TypeError('Cannot convert to specified type: %s' %
1707                            type(message))
1708
1709
1710class _mboxMMDFMessage(Message):
1711    """Message with mbox- or MMDF-specific properties."""
1712
1713    _type_specific_attributes = ['_from']
1714
1715    def __init__(self, message=None):
1716        """Initialize an mboxMMDFMessage instance."""
1717        self.set_from('MAILER-DAEMON', True)
1718        if isinstance(message, email.message.Message):
1719            unixfrom = message.get_unixfrom()
1720            if unixfrom is not None and unixfrom.startswith('From '):
1721                self.set_from(unixfrom[5:])
1722        Message.__init__(self, message)
1723
1724    def get_from(self):
1725        """Return contents of "From " line."""
1726        return self._from
1727
1728    def set_from(self, from_, time_=None):
1729        """Set "From " line, formatting and appending time_ if specified."""
1730        if time_ is not None:
1731            if time_ is True:
1732                time_ = time.gmtime()
1733            from_ += ' ' + time.asctime(time_)
1734        self._from = from_
1735
1736    def get_flags(self):
1737        """Return as a string the flags that are set."""
1738        return self.get('Status', '') + self.get('X-Status', '')
1739
1740    def set_flags(self, flags):
1741        """Set the given flags and unset all others."""
1742        flags = set(flags)
1743        status_flags, xstatus_flags = '', ''
1744        for flag in ('R', 'O'):
1745            if flag in flags:
1746                status_flags += flag
1747                flags.remove(flag)
1748        for flag in ('D', 'F', 'A'):
1749            if flag in flags:
1750                xstatus_flags += flag
1751                flags.remove(flag)
1752        xstatus_flags += ''.join(sorted(flags))
1753        try:
1754            self.replace_header('Status', status_flags)
1755        except KeyError:
1756            self.add_header('Status', status_flags)
1757        try:
1758            self.replace_header('X-Status', xstatus_flags)
1759        except KeyError:
1760            self.add_header('X-Status', xstatus_flags)
1761
1762    def add_flag(self, flag):
1763        """Set the given flag(s) without changing others."""
1764        self.set_flags(''.join(set(self.get_flags()) | set(flag)))
1765
1766    def remove_flag(self, flag):
1767        """Unset the given string flag(s) without changing others."""
1768        if 'Status' in self or 'X-Status' in self:
1769            self.set_flags(''.join(set(self.get_flags()) - set(flag)))
1770
1771    def _explain_to(self, message):
1772        """Copy mbox- or MMDF-specific state to message insofar as possible."""
1773        if isinstance(message, MaildirMessage):
1774            flags = set(self.get_flags())
1775            if 'O' in flags:
1776                message.set_subdir('cur')
1777            if 'F' in flags:
1778                message.add_flag('F')
1779            if 'A' in flags:
1780                message.add_flag('R')
1781            if 'R' in flags:
1782                message.add_flag('S')
1783            if 'D' in flags:
1784                message.add_flag('T')
1785            del message['status']
1786            del message['x-status']
1787            maybe_date = ' '.join(self.get_from().split()[-5:])
1788            try:
1789                message.set_date(calendar.timegm(time.strptime(maybe_date,
1790                                                      '%a %b %d %H:%M:%S %Y')))
1791            except (ValueError, OverflowError):
1792                pass
1793        elif isinstance(message, _mboxMMDFMessage):
1794            message.set_flags(self.get_flags())
1795            message.set_from(self.get_from())
1796        elif isinstance(message, MHMessage):
1797            flags = set(self.get_flags())
1798            if 'R' not in flags:
1799                message.add_sequence('unseen')
1800            if 'A' in flags:
1801                message.add_sequence('replied')
1802            if 'F' in flags:
1803                message.add_sequence('flagged')
1804            del message['status']
1805            del message['x-status']
1806        elif isinstance(message, BabylMessage):
1807            flags = set(self.get_flags())
1808            if 'R' not in flags:
1809                message.add_label('unseen')
1810            if 'D' in flags:
1811                message.add_label('deleted')
1812            if 'A' in flags:
1813                message.add_label('answered')
1814            del message['status']
1815            del message['x-status']
1816        elif isinstance(message, Message):
1817            pass
1818        else:
1819            raise TypeError('Cannot convert to specified type: %s' %
1820                            type(message))
1821
1822
1823class mboxMessage(_mboxMMDFMessage):
1824    """Message with mbox-specific properties."""
1825
1826
1827class MHMessage(Message):
1828    """Message with MH-specific properties."""
1829
1830    _type_specific_attributes = ['_sequences']
1831
1832    def __init__(self, message=None):
1833        """Initialize an MHMessage instance."""
1834        self._sequences = []
1835        Message.__init__(self, message)
1836
1837    def get_sequences(self):
1838        """Return a list of sequences that include the message."""
1839        return self._sequences[:]
1840
1841    def set_sequences(self, sequences):
1842        """Set the list of sequences that include the message."""
1843        self._sequences = list(sequences)
1844
1845    def add_sequence(self, sequence):
1846        """Add sequence to list of sequences including the message."""
1847        if isinstance(sequence, str):
1848            if not sequence in self._sequences:
1849                self._sequences.append(sequence)
1850        else:
1851            raise TypeError('sequence type must be str: %s' % type(sequence))
1852
1853    def remove_sequence(self, sequence):
1854        """Remove sequence from the list of sequences including the message."""
1855        try:
1856            self._sequences.remove(sequence)
1857        except ValueError:
1858            pass
1859
1860    def _explain_to(self, message):
1861        """Copy MH-specific state to message insofar as possible."""
1862        if isinstance(message, MaildirMessage):
1863            sequences = set(self.get_sequences())
1864            if 'unseen' in sequences:
1865                message.set_subdir('cur')
1866            else:
1867                message.set_subdir('cur')
1868                message.add_flag('S')
1869            if 'flagged' in sequences:
1870                message.add_flag('F')
1871            if 'replied' in sequences:
1872                message.add_flag('R')
1873        elif isinstance(message, _mboxMMDFMessage):
1874            sequences = set(self.get_sequences())
1875            if 'unseen' not in sequences:
1876                message.add_flag('RO')
1877            else:
1878                message.add_flag('O')
1879            if 'flagged' in sequences:
1880                message.add_flag('F')
1881            if 'replied' in sequences:
1882                message.add_flag('A')
1883        elif isinstance(message, MHMessage):
1884            for sequence in self.get_sequences():
1885                message.add_sequence(sequence)
1886        elif isinstance(message, BabylMessage):
1887            sequences = set(self.get_sequences())
1888            if 'unseen' in sequences:
1889                message.add_label('unseen')
1890            if 'replied' in sequences:
1891                message.add_label('answered')
1892        elif isinstance(message, Message):
1893            pass
1894        else:
1895            raise TypeError('Cannot convert to specified type: %s' %
1896                            type(message))
1897
1898
1899class BabylMessage(Message):
1900    """Message with Babyl-specific properties."""
1901
1902    _type_specific_attributes = ['_labels', '_visible']
1903
1904    def __init__(self, message=None):
1905        """Initialize a BabylMessage instance."""
1906        self._labels = []
1907        self._visible = Message()
1908        Message.__init__(self, message)
1909
1910    def get_labels(self):
1911        """Return a list of labels on the message."""
1912        return self._labels[:]
1913
1914    def set_labels(self, labels):
1915        """Set the list of labels on the message."""
1916        self._labels = list(labels)
1917
1918    def add_label(self, label):
1919        """Add label to list of labels on the message."""
1920        if isinstance(label, str):
1921            if label not in self._labels:
1922                self._labels.append(label)
1923        else:
1924            raise TypeError('label must be a string: %s' % type(label))
1925
1926    def remove_label(self, label):
1927        """Remove label from the list of labels on the message."""
1928        try:
1929            self._labels.remove(label)
1930        except ValueError:
1931            pass
1932
1933    def get_visible(self):
1934        """Return a Message representation of visible headers."""
1935        return Message(self._visible)
1936
1937    def set_visible(self, visible):
1938        """Set the Message representation of visible headers."""
1939        self._visible = Message(visible)
1940
1941    def update_visible(self):
1942        """Update and/or sensibly generate a set of visible headers."""
1943        for header in self._visible.keys():
1944            if header in self:
1945                self._visible.replace_header(header, self[header])
1946            else:
1947                del self._visible[header]
1948        for header in ('Date', 'From', 'Reply-To', 'To', 'CC', 'Subject'):
1949            if header in self and header not in self._visible:
1950                self._visible[header] = self[header]
1951
1952    def _explain_to(self, message):
1953        """Copy Babyl-specific state to message insofar as possible."""
1954        if isinstance(message, MaildirMessage):
1955            labels = set(self.get_labels())
1956            if 'unseen' in labels:
1957                message.set_subdir('cur')
1958            else:
1959                message.set_subdir('cur')
1960                message.add_flag('S')
1961            if 'forwarded' in labels or 'resent' in labels:
1962                message.add_flag('P')
1963            if 'answered' in labels:
1964                message.add_flag('R')
1965            if 'deleted' in labels:
1966                message.add_flag('T')
1967        elif isinstance(message, _mboxMMDFMessage):
1968            labels = set(self.get_labels())
1969            if 'unseen' not in labels:
1970                message.add_flag('RO')
1971            else:
1972                message.add_flag('O')
1973            if 'deleted' in labels:
1974                message.add_flag('D')
1975            if 'answered' in labels:
1976                message.add_flag('A')
1977        elif isinstance(message, MHMessage):
1978            labels = set(self.get_labels())
1979            if 'unseen' in labels:
1980                message.add_sequence('unseen')
1981            if 'answered' in labels:
1982                message.add_sequence('replied')
1983        elif isinstance(message, BabylMessage):
1984            message.set_visible(self.get_visible())
1985            for label in self.get_labels():
1986                message.add_label(label)
1987        elif isinstance(message, Message):
1988            pass
1989        else:
1990            raise TypeError('Cannot convert to specified type: %s' %
1991                            type(message))
1992
1993
1994class MMDFMessage(_mboxMMDFMessage):
1995    """Message with MMDF-specific properties."""
1996
1997
1998class _ProxyFile:
1999    """A read-only wrapper of a file."""
2000
2001    def __init__(self, f, pos=None):
2002        """Initialize a _ProxyFile."""
2003        self._file = f
2004        if pos is None:
2005            self._pos = f.tell()
2006        else:
2007            self._pos = pos
2008
2009    def read(self, size=None):
2010        """Read bytes."""
2011        return self._read(size, self._file.read)
2012
2013    def read1(self, size=None):
2014        """Read bytes."""
2015        return self._read(size, self._file.read1)
2016
2017    def readline(self, size=None):
2018        """Read a line."""
2019        return self._read(size, self._file.readline)
2020
2021    def readlines(self, sizehint=None):
2022        """Read multiple lines."""
2023        result = []
2024        for line in self:
2025            result.append(line)
2026            if sizehint is not None:
2027                sizehint -= len(line)
2028                if sizehint <= 0:
2029                    break
2030        return result
2031
2032    def __iter__(self):
2033        """Iterate over lines."""
2034        while line := self.readline():
2035            yield line
2036
2037    def tell(self):
2038        """Return the position."""
2039        return self._pos
2040
2041    def seek(self, offset, whence=0):
2042        """Change position."""
2043        if whence == 1:
2044            self._file.seek(self._pos)
2045        self._file.seek(offset, whence)
2046        self._pos = self._file.tell()
2047
2048    def close(self):
2049        """Close the file."""
2050        if hasattr(self, '_file'):
2051            try:
2052                if hasattr(self._file, 'close'):
2053                    self._file.close()
2054            finally:
2055                del self._file
2056
2057    def _read(self, size, read_method):
2058        """Read size bytes using read_method."""
2059        if size is None:
2060            size = -1
2061        self._file.seek(self._pos)
2062        result = read_method(size)
2063        self._pos = self._file.tell()
2064        return result
2065
2066    def __enter__(self):
2067        """Context management protocol support."""
2068        return self
2069
2070    def __exit__(self, *exc):
2071        self.close()
2072
2073    def readable(self):
2074        return self._file.readable()
2075
2076    def writable(self):
2077        return self._file.writable()
2078
2079    def seekable(self):
2080        return self._file.seekable()
2081
2082    def flush(self):
2083        return self._file.flush()
2084
2085    @property
2086    def closed(self):
2087        if not hasattr(self, '_file'):
2088            return True
2089        if not hasattr(self._file, 'closed'):
2090            return False
2091        return self._file.closed
2092
2093    __class_getitem__ = classmethod(GenericAlias)
2094
2095
2096class _PartialFile(_ProxyFile):
2097    """A read-only wrapper of part of a file."""
2098
2099    def __init__(self, f, start=None, stop=None):
2100        """Initialize a _PartialFile."""
2101        _ProxyFile.__init__(self, f, start)
2102        self._start = start
2103        self._stop = stop
2104
2105    def tell(self):
2106        """Return the position with respect to start."""
2107        return _ProxyFile.tell(self) - self._start
2108
2109    def seek(self, offset, whence=0):
2110        """Change position, possibly with respect to start or stop."""
2111        if whence == 0:
2112            self._pos = self._start
2113            whence = 1
2114        elif whence == 2:
2115            self._pos = self._stop
2116            whence = 1
2117        _ProxyFile.seek(self, offset, whence)
2118
2119    def _read(self, size, read_method):
2120        """Read size bytes using read_method, honoring start and stop."""
2121        remaining = self._stop - self._pos
2122        if remaining <= 0:
2123            return b''
2124        if size is None or size < 0 or size > remaining:
2125            size = remaining
2126        return _ProxyFile._read(self, size, read_method)
2127
2128    def close(self):
2129        # do *not* close the underlying file object for partial files,
2130        # since it's global to the mailbox object
2131        if hasattr(self, '_file'):
2132            del self._file
2133
2134
2135def _lock_file(f, dotlock=True):
2136    """Lock file f using lockf and dot locking."""
2137    dotlock_done = False
2138    try:
2139        if fcntl:
2140            try:
2141                fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
2142            except OSError as e:
2143                if e.errno in (errno.EAGAIN, errno.EACCES, errno.EROFS):
2144                    raise ExternalClashError('lockf: lock unavailable: %s' %
2145                                             f.name)
2146                else:
2147                    raise
2148        if dotlock:
2149            try:
2150                pre_lock = _create_temporary(f.name + '.lock')
2151                pre_lock.close()
2152            except OSError as e:
2153                if e.errno in (errno.EACCES, errno.EROFS):
2154                    return  # Without write access, just skip dotlocking.
2155                else:
2156                    raise
2157            try:
2158                try:
2159                    os.link(pre_lock.name, f.name + '.lock')
2160                    dotlock_done = True
2161                except (AttributeError, PermissionError):
2162                    os.rename(pre_lock.name, f.name + '.lock')
2163                    dotlock_done = True
2164                else:
2165                    os.unlink(pre_lock.name)
2166            except FileExistsError:
2167                os.remove(pre_lock.name)
2168                raise ExternalClashError('dot lock unavailable: %s' %
2169                                         f.name)
2170    except:
2171        if fcntl:
2172            fcntl.lockf(f, fcntl.LOCK_UN)
2173        if dotlock_done:
2174            os.remove(f.name + '.lock')
2175        raise
2176
2177def _unlock_file(f):
2178    """Unlock file f using lockf and dot locking."""
2179    if fcntl:
2180        fcntl.lockf(f, fcntl.LOCK_UN)
2181    if os.path.exists(f.name + '.lock'):
2182        os.remove(f.name + '.lock')
2183
2184def _create_carefully(path):
2185    """Create a file if it doesn't exist and open for reading and writing."""
2186    fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0o666)
2187    try:
2188        return open(path, 'rb+')
2189    finally:
2190        os.close(fd)
2191
2192def _create_temporary(path):
2193    """Create a temp file based on path and open for reading and writing."""
2194    return _create_carefully('%s.%s.%s.%s' % (path, int(time.time()),
2195                                              socket.gethostname(),
2196                                              os.getpid()))
2197
2198def _sync_flush(f):
2199    """Ensure changes to file f are physically on disk."""
2200    f.flush()
2201    if hasattr(os, 'fsync'):
2202        os.fsync(f.fileno())
2203
2204def _sync_close(f):
2205    """Close file f, ensuring all changes are physically on disk."""
2206    _sync_flush(f)
2207    f.close()
2208
2209
2210class Error(Exception):
2211    """Raised for module-specific errors."""
2212
2213class NoSuchMailboxError(Error):
2214    """The specified mailbox does not exist and won't be created."""
2215
2216class NotEmptyError(Error):
2217    """The specified mailbox is not empty and deletion was requested."""
2218
2219class ExternalClashError(Error):
2220    """Another process caused an action to fail."""
2221
2222class FormatError(Error):
2223    """A file appears to have an invalid format."""
2224