ยปCore Development>Code coverage>Lib/mailbox.py

Python code coverage for Lib/mailbox.py

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