diff options
Diffstat (limited to 'Lib/mailbox.py')
-rw-r--r-- | Lib/mailbox.py | 642 |
1 files changed, 283 insertions, 359 deletions
diff --git a/Lib/mailbox.py b/Lib/mailbox.py index 530d3c5a66e..c73fb95fe21 100644 --- a/Lib/mailbox.py +++ b/Lib/mailbox.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python3 """Read/write support for Maildir, mbox, MH, Babyl, and MMDF mailboxes.""" @@ -15,10 +15,12 @@ import calendar import socket import errno import copy +import warnings import email import email.message import email.generator -import StringIO +import io +import contextlib try: if sys.platform == 'os2emx': # OS/2 EMX fcntl() not adequate @@ -27,17 +29,11 @@ try: except ImportError: fcntl = None -import warnings -with warnings.catch_warnings(): - if sys.py3kwarning: - warnings.filterwarnings("ignore", ".*rfc822 has been removed", - DeprecationWarning) - import rfc822 - __all__ = [ 'Mailbox', 'Maildir', 'mbox', 'MH', 'Babyl', 'MMDF', 'Message', 'MaildirMessage', 'mboxMessage', 'MHMessage', - 'BabylMessage', 'MMDFMessage', 'UnixMailbox', - 'PortableUnixMailbox', 'MmdfMailbox', 'MHMailbox', 'BabylMailbox' ] + 'BabylMessage', 'MMDFMessage'] + +linesep = os.linesep.encode('ascii') class Mailbox: """A group of messages in a particular place.""" @@ -81,14 +77,22 @@ class Mailbox: if not self._factory: return self.get_message(key) else: - return self._factory(self.get_file(key)) + with contextlib.closing(self.get_file(key)) as file: + return self._factory(file) def get_message(self, key): """Return a Message representation or raise a KeyError.""" raise NotImplementedError('Method must be implemented by subclass') def get_string(self, key): - """Return a string representation or raise a KeyError.""" + """Return a string representation or raise a KeyError. + + Uses email.message.Message to create a 7bit clean string + representation of the message.""" + return email.message_from_bytes(self.get_bytes(key)).as_string() + + def get_bytes(self, key): + """Return a byte string representation or raise a KeyError.""" raise NotImplementedError('Method must be implemented by subclass') def get_file(self, key): @@ -105,7 +109,7 @@ class Mailbox: def itervalues(self): """Return an iterator over all messages.""" - for key in self.iterkeys(): + for key in self.keys(): try: value = self[key] except KeyError: @@ -121,7 +125,7 @@ class Mailbox: def iteritems(self): """Return an iterator over (key, message) tuples.""" - for key in self.iterkeys(): + for key in self.keys(): try: value = self[key] except KeyError: @@ -132,20 +136,17 @@ class Mailbox: """Return a list of (key, message) tuples. Memory intensive.""" return list(self.iteritems()) - def has_key(self, key): + def __contains__(self, key): """Return True if the keyed message exists, False otherwise.""" raise NotImplementedError('Method must be implemented by subclass') - def __contains__(self, key): - return self.has_key(key) - def __len__(self): """Return a count of messages in the mailbox.""" raise NotImplementedError('Method must be implemented by subclass') def clear(self): """Delete all messages.""" - for key in self.iterkeys(): + for key in self.keys(): self.discard(key) def pop(self, key, default=None): @@ -159,7 +160,7 @@ class Mailbox: def popitem(self): """Delete an arbitrary (key, message) pair and return it.""" - for key in self.iterkeys(): + for key in self.keys(): return (key, self.pop(key)) # This is only run once. else: raise KeyError('No messages in mailbox') @@ -167,7 +168,7 @@ class Mailbox: def update(self, arg=None): """Change the messages that correspond to certain keys.""" if hasattr(arg, 'iteritems'): - source = arg.iteritems() + source = arg.items() elif hasattr(arg, 'items'): source = arg.items() else: @@ -197,46 +198,70 @@ class Mailbox: """Flush and close the mailbox.""" raise NotImplementedError('Method must be implemented by subclass') + def _string_to_bytes(self, message): + # If a message is not 7bit clean, we refuse to handle it since it + # likely came from reading invalid messages in text mode, and that way + # lies mojibake. + try: + return message.encode('ascii') + except UnicodeError: + raise ValueError("String input must be ASCII-only; " + "use bytes or a Message instead") + # Whether each message must end in a newline _append_newline = False def _dump_message(self, message, target, mangle_from_=False): - # Most files are opened in binary mode to allow predictable seeking. - # To get native line endings on disk, the user-friendly \n line endings - # used in strings and by email.Message are translated here. + # This assumes the target file is open in binary mode. """Dump message contents to target file.""" if isinstance(message, email.message.Message): - buffer = StringIO.StringIO() - gen = email.generator.Generator(buffer, mangle_from_, 0) + buffer = io.BytesIO() + gen = email.generator.BytesGenerator(buffer, mangle_from_, 0) gen.flatten(message) buffer.seek(0) - data = buffer.read().replace('\n', os.linesep) + data = buffer.read() + data = data.replace(b'\n', linesep) target.write(data) - if self._append_newline and not data.endswith(os.linesep): + if self._append_newline and not data.endswith(linesep): # Make sure the message ends with a newline - target.write(os.linesep) - elif isinstance(message, str): + target.write(linesep) + elif isinstance(message, (str, bytes, io.StringIO)): + if isinstance(message, io.StringIO): + warnings.warn("Use of StringIO input is deprecated, " + "use BytesIO instead", DeprecationWarning, 3) + message = message.getvalue() + if isinstance(message, str): + message = self._string_to_bytes(message) if mangle_from_: - message = message.replace('\nFrom ', '\n>From ') - message = message.replace('\n', os.linesep) + message = message.replace(b'\nFrom ', b'\n>From ') + message = message.replace(b'\n', linesep) target.write(message) - if self._append_newline and not message.endswith(os.linesep): + if self._append_newline and not message.endswith(linesep): # Make sure the message ends with a newline - target.write(os.linesep) + target.write(linesep) elif hasattr(message, 'read'): + if hasattr(message, 'buffer'): + warnings.warn("Use of text mode files is deprecated, " + "use a binary mode file instead", DeprecationWarning, 3) + message = message.buffer lastline = None while True: line = message.readline() - if line == '': + # Universal newline support. + if line.endswith(b'\r\n'): + line = line[:-2] + b'\n' + elif line.endswith(b'\r'): + line = line[:-1] + b'\n' + if not line: break - if mangle_from_ and line.startswith('From '): - line = '>From ' + line[5:] - line = line.replace('\n', os.linesep) + if mangle_from_ and line.startswith(b'From '): + line = b'>From ' + line[5:] + line = line.replace(b'\n', linesep) target.write(line) lastline = line - if self._append_newline and lastline and not lastline.endswith(os.linesep): + if self._append_newline and lastline and not lastline.endswith(linesep): # Make sure the message ends with a newline - target.write(os.linesep) + target.write(linesep) else: raise TypeError('Invalid message type: %s' % type(message)) @@ -246,7 +271,7 @@ class Maildir(Mailbox): colon = ':' - def __init__(self, dirname, factory=rfc822.Message, create=True): + def __init__(self, dirname, factory=None, create=True): """Initialize a Maildir instance.""" Mailbox.__init__(self, dirname, factory, create) self._paths = { @@ -256,7 +281,7 @@ class Maildir(Mailbox): } if not os.path.exists(self._path): if create: - os.mkdir(self._path, 0700) + os.mkdir(self._path, 0o700) for path in self._paths.values(): os.mkdir(path, 0o700) else: @@ -292,7 +317,7 @@ class Maildir(Mailbox): os.remove(tmp_file.name) else: os.rename(tmp_file.name, dest) - except OSError, e: + except OSError as e: os.remove(tmp_file.name) if e.errno == errno.EEXIST: raise ExternalClashError('Name clash with existing message: %s' @@ -314,7 +339,7 @@ class Maildir(Mailbox): self.remove(key) except KeyError: pass - except OSError, e: + except OSError as e: if e.errno != errno.ENOENT: raise @@ -344,7 +369,7 @@ class Maildir(Mailbox): def get_message(self, key): """Return a Message representation or raise a KeyError.""" subpath = self._lookup(key) - f = open(os.path.join(self._path, subpath), 'r') + f = open(os.path.join(self._path, subpath), 'rb') try: if self._factory: msg = self._factory(f) @@ -359,11 +384,11 @@ class Maildir(Mailbox): msg.set_date(os.path.getmtime(os.path.join(self._path, subpath))) return msg - def get_string(self, key): - """Return a string representation or raise a KeyError.""" - f = open(os.path.join(self._path, self._lookup(key)), 'r') + def get_bytes(self, key): + """Return a bytes representation or raise a KeyError.""" + f = open(os.path.join(self._path, self._lookup(key)), 'rb') try: - return f.read() + return f.read().replace(linesep, b'\n') finally: f.close() @@ -382,7 +407,7 @@ class Maildir(Mailbox): continue yield key - def has_key(self, key): + def __contains__(self, key): """Return True if the keyed message exists, False otherwise.""" self._refresh() return key in self._toc @@ -432,7 +457,7 @@ class Maildir(Mailbox): maildirfolder_path = os.path.join(path, 'maildirfolder') if not os.path.exists(maildirfolder_path): os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY, - 0666)) + 0o666)) return result def remove_folder(self, folder): @@ -477,12 +502,12 @@ class Maildir(Mailbox): path = os.path.join(self._path, 'tmp', uniq) try: os.stat(path) - except OSError, e: + except OSError as e: if e.errno == errno.ENOENT: Maildir._count += 1 try: return _create_carefully(path) - except OSError, e: + except OSError as e: if e.errno != errno.EEXIST: raise else: @@ -545,10 +570,10 @@ class Maildir(Mailbox): def next(self): """Return the next message in a one-time iteration.""" if not hasattr(self, '_onetime_keys'): - self._onetime_keys = self.iterkeys() + self._onetime_keys = iter(self.keys()) while True: try: - return self[self._onetime_keys.next()] + return self[next(self._onetime_keys)] except StopIteration: return None except KeyError: @@ -563,7 +588,7 @@ class _singlefileMailbox(Mailbox): Mailbox.__init__(self, path, factory, create) try: f = open(self._path, 'rb+') - except IOError, e: + except IOError as e: if e.errno == errno.ENOENT: if create: f = open(self._path, 'wb+') @@ -609,7 +634,7 @@ class _singlefileMailbox(Mailbox): for key in self._toc.keys(): yield key - def has_key(self, key): + def __contains__(self, key): """Return True if the keyed message exists, False otherwise.""" self._lookup() return key in self._toc @@ -667,7 +692,7 @@ class _singlefileMailbox(Mailbox): while True: buffer = self._file.read(min(4096, stop - self._file.tell())) - if buffer == '': + if not buffer: break new_file.write(buffer) new_toc[key] = (new_start, new_file.tell()) @@ -685,7 +710,7 @@ class _singlefileMailbox(Mailbox): os.chmod(new_file.name, mode) try: os.rename(new_file.name, self._path) - except OSError, e: + except OSError as e: if e.errno == errno.EEXIST or \ (os.name == 'os2' and e.errno == errno.EACCES): os.remove(self._path) @@ -760,20 +785,25 @@ class _mboxMMDF(_singlefileMailbox): """Return a Message representation or raise a KeyError.""" start, stop = self._lookup(key) self._file.seek(start) - from_line = self._file.readline().replace(os.linesep, '') + from_line = self._file.readline().replace(linesep, b'') string = self._file.read(stop - self._file.tell()) - msg = self._message_factory(string.replace(os.linesep, '\n')) - msg.set_from(from_line[5:]) + msg = self._message_factory(string.replace(linesep, b'\n')) + msg.set_from(from_line[5:].decode('ascii')) return msg def get_string(self, key, from_=False): """Return a string representation or raise a KeyError.""" + return email.message_from_bytes( + self.get_bytes(key)).as_string(unixfrom=from_) + + def get_bytes(self, key, from_=False): + """Return a string representation or raise a KeyError.""" start, stop = self._lookup(key) self._file.seek(start) if not from_: self._file.readline() string = self._file.read(stop - self._file.tell()) - return string.replace(os.linesep, '\n') + return string.replace(linesep, b'\n') def get_file(self, key, from_=False): """Return a file-like representation or raise a KeyError.""" @@ -786,22 +816,27 @@ class _mboxMMDF(_singlefileMailbox): def _install_message(self, message): """Format a message and blindly write to self._file.""" from_line = None - if isinstance(message, str) and message.startswith('From '): - newline = message.find('\n') + if isinstance(message, str): + message = self._string_to_bytes(message) + if isinstance(message, bytes) and message.startswith(b'From '): + newline = message.find(b'\n') if newline != -1: from_line = message[:newline] message = message[newline + 1:] else: from_line = message - message = '' + message = b'' elif isinstance(message, _mboxMMDFMessage): - from_line = 'From ' + message.get_from() + author = message.get_from().encode('ascii') + from_line = b'From ' + author elif isinstance(message, email.message.Message): from_line = message.get_unixfrom() # May be None. + if from_line is not None: + from_line = from_line.encode('ascii') if from_line is None: - from_line = 'From MAILER-DAEMON %s' % time.asctime(time.gmtime()) + from_line = b'From MAILER-DAEMON ' + time.asctime(time.gmtime()).encode() start = self._file.tell() - self._file.write(from_line + os.linesep) + self._file.write(from_line + linesep) self._dump_message(message, self._file, self._mangle_from_) stop = self._file.tell() return (start, stop) @@ -823,7 +858,7 @@ class mbox(_mboxMMDF): def _post_message_hook(self, f): """Called after writing each message to file f.""" - f.write(os.linesep) + f.write(linesep) def _generate_toc(self): """Generate key-to-(start, stop) table of contents.""" @@ -833,10 +868,10 @@ class mbox(_mboxMMDF): while True: line_pos = self._file.tell() line = self._file.readline() - if line.startswith('From '): + if line.startswith(b'From '): if len(stops) < len(starts): if last_was_empty: - stops.append(line_pos - len(os.linesep)) + stops.append(line_pos - len(linesep)) else: # The last line before the "From " line wasn't # blank, but we consider it a start of a @@ -846,11 +881,11 @@ class mbox(_mboxMMDF): last_was_empty = False elif not line: if last_was_empty: - stops.append(line_pos - len(os.linesep)) + stops.append(line_pos - len(linesep)) else: stops.append(line_pos) break - elif line == os.linesep: + elif line == linesep: last_was_empty = True else: last_was_empty = False @@ -869,11 +904,11 @@ class MMDF(_mboxMMDF): def _pre_message_hook(self, f): """Called before writing each message to file f.""" - f.write('\001\001\001\001' + os.linesep) + f.write(b'\001\001\001\001' + linesep) def _post_message_hook(self, f): """Called after writing each message to file f.""" - f.write(os.linesep + '\001\001\001\001' + os.linesep) + f.write(linesep + b'\001\001\001\001' + linesep) def _generate_toc(self): """Generate key-to-(start, stop) table of contents.""" @@ -884,19 +919,19 @@ class MMDF(_mboxMMDF): line_pos = next_pos line = self._file.readline() next_pos = self._file.tell() - if line.startswith('\001\001\001\001' + os.linesep): + if line.startswith(b'\001\001\001\001' + linesep): starts.append(next_pos) while True: line_pos = next_pos line = self._file.readline() next_pos = self._file.tell() - if line == '\001\001\001\001' + os.linesep: - stops.append(line_pos - len(os.linesep)) + if line == b'\001\001\001\001' + linesep: + stops.append(line_pos - len(linesep)) break - elif line == '': + elif not line: stops.append(line_pos) break - elif line == '': + elif not line: break self._toc = dict(enumerate(zip(starts, stops))) self._next_key = len(self._toc) @@ -912,9 +947,9 @@ class MH(Mailbox): Mailbox.__init__(self, path, factory, create) if not os.path.exists(self._path): if create: - os.mkdir(self._path, 0700) + os.mkdir(self._path, 0o700) os.close(os.open(os.path.join(self._path, '.mh_sequences'), - os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0600)) + os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)) else: raise NoSuchMailboxError(self._path) self._locked = False @@ -958,7 +993,7 @@ class MH(Mailbox): path = os.path.join(self._path, str(key)) try: f = open(path, 'rb+') - except IOError, e: + except IOError as e: if e.errno == errno.ENOENT: raise KeyError('No message with key: %s' % key) else: @@ -972,7 +1007,7 @@ class MH(Mailbox): path = os.path.join(self._path, str(key)) try: f = open(path, 'rb+') - except IOError, e: + except IOError as e: if e.errno == errno.ENOENT: raise KeyError('No message with key: %s' % key) else: @@ -995,10 +1030,10 @@ class MH(Mailbox): """Return a Message representation or raise a KeyError.""" try: if self._locked: - f = open(os.path.join(self._path, str(key)), 'r+') + f = open(os.path.join(self._path, str(key)), 'rb+') else: - f = open(os.path.join(self._path, str(key)), 'r') - except IOError, e: + f = open(os.path.join(self._path, str(key)), 'rb') + except IOError as e: if e.errno == errno.ENOENT: raise KeyError('No message with key: %s' % key) else: @@ -1013,19 +1048,19 @@ class MH(Mailbox): _unlock_file(f) finally: f.close() - for name, key_list in self.get_sequences().iteritems(): + for name, key_list in self.get_sequences().items(): if key in key_list: msg.add_sequence(name) return msg - def get_string(self, key): - """Return a string representation or raise a KeyError.""" + def get_bytes(self, key): + """Return a bytes representation or raise a KeyError.""" try: if self._locked: - f = open(os.path.join(self._path, str(key)), 'r+') + f = open(os.path.join(self._path, str(key)), 'rb+') else: - f = open(os.path.join(self._path, str(key)), 'r') - except IOError, e: + f = open(os.path.join(self._path, str(key)), 'rb') + except IOError as e: if e.errno == errno.ENOENT: raise KeyError('No message with key: %s' % key) else: @@ -1034,7 +1069,7 @@ class MH(Mailbox): if self._locked: _lock_file(f) try: - return f.read() + return f.read().replace(linesep, b'\n') finally: if self._locked: _unlock_file(f) @@ -1045,7 +1080,7 @@ class MH(Mailbox): """Return a file-like representation or raise a KeyError.""" try: f = open(os.path.join(self._path, str(key)), 'rb') - except IOError, e: + except IOError as e: if e.errno == errno.ENOENT: raise KeyError('No message with key: %s' % key) else: @@ -1057,13 +1092,13 @@ class MH(Mailbox): return iter(sorted(int(entry) for entry in os.listdir(self._path) if entry.isdigit())) - def has_key(self, key): + def __contains__(self, key): """Return True if the keyed message exists, False otherwise.""" return os.path.exists(os.path.join(self._path, str(key))) def __len__(self): """Return a count of messages in the mailbox.""" - return len(list(self.iterkeys())) + return len(list(self.keys())) def lock(self): """Lock the mailbox.""" @@ -1151,10 +1186,10 @@ class MH(Mailbox): f = open(os.path.join(self._path, '.mh_sequences'), 'r+') try: os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC)) - for name, keys in sequences.iteritems(): + for name, keys in sequences.items(): if len(keys) == 0: continue - f.write('%s:' % name) + f.write(name + ':') prev = None completing = False for key in sorted(set(keys)): @@ -1180,7 +1215,7 @@ class MH(Mailbox): sequences = self.get_sequences() prev = 0 changes = [] - for key in self.iterkeys(): + for key in self.keys(): if key - 1 != prev: changes.append((key, prev + 1)) if hasattr(os, 'link'): @@ -1204,7 +1239,7 @@ class MH(Mailbox): """Inspect a new MHMessage and update sequences appropriately.""" pending_sequences = message.get_sequences() all_sequences = self.get_sequences() - for name, key_list in all_sequences.iteritems(): + for name, key_list in all_sequences.items(): if name in pending_sequences: key_list.append(key) elif key in key_list: @@ -1249,50 +1284,55 @@ class Babyl(_singlefileMailbox): """Return a Message representation or raise a KeyError.""" start, stop = self._lookup(key) self._file.seek(start) - self._file.readline() # Skip '1,' line specifying labels. - original_headers = StringIO.StringIO() + self._file.readline() # Skip b'1,' line specifying labels. + original_headers = io.BytesIO() while True: line = self._file.readline() - if line == '*** EOOH ***' + os.linesep or line == '': + if line == b'*** EOOH ***' + linesep or not line: break - original_headers.write(line.replace(os.linesep, '\n')) - visible_headers = StringIO.StringIO() + original_headers.write(line.replace(linesep, b'\n')) + visible_headers = io.BytesIO() while True: line = self._file.readline() - if line == os.linesep or line == '': + if line == linesep or not line: break - visible_headers.write(line.replace(os.linesep, '\n')) - body = self._file.read(stop - self._file.tell()).replace(os.linesep, - '\n') + visible_headers.write(line.replace(linesep, b'\n')) + # Read up to the stop, or to the end + n = stop - self._file.tell() + assert n >= 0 + body = self._file.read(n) + body = body.replace(linesep, b'\n') msg = BabylMessage(original_headers.getvalue() + body) msg.set_visible(visible_headers.getvalue()) if key in self._labels: msg.set_labels(self._labels[key]) return msg - def get_string(self, key): + def get_bytes(self, key): """Return a string representation or raise a KeyError.""" start, stop = self._lookup(key) self._file.seek(start) - self._file.readline() # Skip '1,' line specifying labels. - original_headers = StringIO.StringIO() + self._file.readline() # Skip b'1,' line specifying labels. + original_headers = io.BytesIO() while True: line = self._file.readline() - if line == '*** EOOH ***' + os.linesep or line == '': + if line == b'*** EOOH ***' + linesep or not line: break - original_headers.write(line.replace(os.linesep, '\n')) + original_headers.write(line.replace(linesep, b'\n')) while True: line = self._file.readline() - if line == os.linesep or line == '': + if line == linesep or not line: break - return original_headers.getvalue() + \ - self._file.read(stop - self._file.tell()).replace(os.linesep, - '\n') + headers = original_headers.getvalue() + n = stop - self._file.tell() + assert n >= 0 + data = self._file.read(n) + data = data.replace(linesep, b'\n') + return headers + data def get_file(self, key): """Return a file-like representation or raise a KeyError.""" - return StringIO.StringIO(self.get_string(key).replace('\n', - os.linesep)) + return io.BytesIO(self.get_bytes(key).replace(b'\n', linesep)) def get_labels(self): """Return a list of user-defined labels in the mailbox.""" @@ -1313,19 +1353,19 @@ class Babyl(_singlefileMailbox): line_pos = next_pos line = self._file.readline() next_pos = self._file.tell() - if line == '\037\014' + os.linesep: + if line == b'\037\014' + linesep: if len(stops) < len(starts): - stops.append(line_pos - len(os.linesep)) + stops.append(line_pos - len(linesep)) starts.append(next_pos) labels = [label.strip() for label - in self._file.readline()[1:].split(',') - if label.strip() != ''] + in self._file.readline()[1:].split(b',') + if label.strip()] label_lists.append(labels) - elif line == '\037' or line == '\037' + os.linesep: + elif line == b'\037' or line == b'\037' + linesep: if len(stops) < len(starts): - stops.append(line_pos - len(os.linesep)) - elif line == '': - stops.append(line_pos - len(os.linesep)) + stops.append(line_pos - len(linesep)) + elif not line: + stops.append(line_pos - len(linesep)) break self._toc = dict(enumerate(zip(starts, stops))) self._labels = dict(enumerate(label_lists)) @@ -1335,17 +1375,21 @@ class Babyl(_singlefileMailbox): def _pre_mailbox_hook(self, f): """Called before writing the mailbox to file f.""" - f.write('BABYL OPTIONS:%sVersion: 5%sLabels:%s%s\037' % - (os.linesep, os.linesep, ','.join(self.get_labels()), - os.linesep)) + babyl = b'BABYL OPTIONS:' + linesep + babyl += b'Version: 5' + linesep + labels = self.get_labels() + labels = (label.encode() for label in labels) + babyl += b'Labels:' + b','.join(labels) + linesep + babyl += b'\037' + f.write(babyl) def _pre_message_hook(self, f): """Called before writing each message to file f.""" - f.write('\014' + os.linesep) + f.write(b'\014' + linesep) def _post_message_hook(self, f): """Called after writing each message to file f.""" - f.write(os.linesep + '\037') + f.write(linesep + b'\037') def _install_message(self, message): """Write message contents and return (start, stop).""" @@ -1358,78 +1402,97 @@ class Babyl(_singlefileMailbox): special_labels.append(label) else: labels.append(label) - self._file.write('1') + self._file.write(b'1') for label in special_labels: - self._file.write(', ' + label) - self._file.write(',,') + self._file.write(b', ' + label.encode()) + self._file.write(b',,') for label in labels: - self._file.write(' ' + label + ',') - self._file.write(os.linesep) + self._file.write(b' ' + label.encode() + b',') + self._file.write(linesep) else: - self._file.write('1,,' + os.linesep) + self._file.write(b'1,,' + linesep) if isinstance(message, email.message.Message): - orig_buffer = StringIO.StringIO() - orig_generator = email.generator.Generator(orig_buffer, False, 0) + orig_buffer = io.BytesIO() + orig_generator = email.generator.BytesGenerator(orig_buffer, False, 0) orig_generator.flatten(message) orig_buffer.seek(0) while True: line = orig_buffer.readline() - self._file.write(line.replace('\n', os.linesep)) - if line == '\n' or line == '': + self._file.write(line.replace(b'\n', linesep)) + if line == b'\n' or not line: break - self._file.write('*** EOOH ***' + os.linesep) + self._file.write(b'*** EOOH ***' + linesep) if isinstance(message, BabylMessage): - vis_buffer = StringIO.StringIO() - vis_generator = email.generator.Generator(vis_buffer, False, 0) + vis_buffer = io.BytesIO() + vis_generator = email.generator.BytesGenerator(vis_buffer, False, 0) vis_generator.flatten(message.get_visible()) while True: line = vis_buffer.readline() - self._file.write(line.replace('\n', os.linesep)) - if line == '\n' or line == '': + self._file.write(line.replace(b'\n', linesep)) + if line == b'\n' or not line: break else: orig_buffer.seek(0) while True: line = orig_buffer.readline() - self._file.write(line.replace('\n', os.linesep)) - if line == '\n' or line == '': + self._file.write(line.replace(b'\n', linesep)) + if line == b'\n' or not line: break while True: buffer = orig_buffer.read(4096) # Buffer size is arbitrary. - if buffer == '': + if not buffer: break - self._file.write(buffer.replace('\n', os.linesep)) - elif isinstance(message, str): - body_start = message.find('\n\n') + 2 + self._file.write(buffer.replace(b'\n', linesep)) + elif isinstance(message, (bytes, str, io.StringIO)): + if isinstance(message, io.StringIO): + warnings.warn("Use of StringIO input is deprecated, " + "use BytesIO instead", DeprecationWarning, 3) + message = message.getvalue() + if isinstance(message, str): + message = self._string_to_bytes(message) + body_start = message.find(b'\n\n') + 2 if body_start - 2 != -1: - self._file.write(message[:body_start].replace('\n', - os.linesep)) - self._file.write('*** EOOH ***' + os.linesep) - self._file.write(message[:body_start].replace('\n', - os.linesep)) - self._file.write(message[body_start:].replace('\n', - os.linesep)) + self._file.write(message[:body_start].replace(b'\n', linesep)) + self._file.write(b'*** EOOH ***' + linesep) + self._file.write(message[:body_start].replace(b'\n', linesep)) + self._file.write(message[body_start:].replace(b'\n', linesep)) else: - self._file.write('*** EOOH ***' + os.linesep + os.linesep) - self._file.write(message.replace('\n', os.linesep)) + self._file.write(b'*** EOOH ***' + linesep + linesep) + self._file.write(message.replace(b'\n', linesep)) elif hasattr(message, 'readline'): + if hasattr(message, 'buffer'): + warnings.warn("Use of text mode files is deprecated, " + "use a binary mode file instead", DeprecationWarning, 3) + message = message.buffer original_pos = message.tell() first_pass = True while True: line = message.readline() - self._file.write(line.replace('\n', os.linesep)) - if line == '\n' or line == '': + # Universal newline support. + if line.endswith(b'\r\n'): + line = line[:-2] + b'\n' + elif line.endswith(b'\r'): + line = line[:-1] + b'\n' + self._file.write(line.replace(b'\n', linesep)) + if line == b'\n' or not line: if first_pass: first_pass = False - self._file.write('*** EOOH ***' + os.linesep) + self._file.write(b'*** EOOH ***' + linesep) message.seek(original_pos) else: break while True: - buffer = message.read(4096) # Buffer size is arbitrary. - if buffer == '': + line = message.readline() + if not line: break - self._file.write(buffer.replace('\n', os.linesep)) + # Universal newline support. + if line.endswith(b'\r\n'): + line = line[:-2] + linesep + elif line.endswith(b'\r'): + line = line[:-1] + linesep + elif line.endswith(b'\n'): + line = line[:-1] + linesep + self._file.write(line) else: raise TypeError('Invalid message type: %s' % type(message)) stop = self._file.tell() @@ -1445,10 +1508,14 @@ class Message(email.message.Message): self._become_message(copy.deepcopy(message)) if isinstance(message, Message): message._explain_to(self) + elif isinstance(message, bytes): + self._become_message(email.message_from_bytes(message)) elif isinstance(message, str): self._become_message(email.message_from_string(message)) - elif hasattr(message, "read"): + elif isinstance(message, io.TextIOWrapper): self._become_message(email.message_from_file(message)) + elif hasattr(message, "read"): + self._become_message(email.message_from_binary_file(message)) elif message is None: email.message.Message.__init__(self) else: @@ -1506,7 +1573,7 @@ class MaildirMessage(Message): def remove_flag(self, flag): """Unset the given string flag(s) without changing others.""" - if self.get_flags() != '': + if self.get_flags(): self.set_flags(''.join(set(self.get_flags()) - set(flag))) def get_date(self): @@ -1712,7 +1779,7 @@ class MHMessage(Message): if not sequence in self._sequences: self._sequences.append(sequence) else: - raise TypeError('sequence must be a string: %s' % type(sequence)) + raise TypeError('sequence type must be str: %s' % type(sequence)) def remove_sequence(self, sequence): """Remove sequence from the list of sequences including the message.""" @@ -1872,6 +1939,10 @@ class _ProxyFile: """Read bytes.""" return self._read(size, self._file.read) + def read1(self, size=None): + """Read bytes.""" + return self._read(size, self._file.read1) + def readline(self, size=None): """Read a line.""" return self._read(size, self._file.readline) @@ -1889,7 +1960,11 @@ class _ProxyFile: def __iter__(self): """Iterate over lines.""" - return iter(self.readline, "") + while True: + line = self.readline() + if not line: + raise StopIteration + yield line def tell(self): """Return the position.""" @@ -1918,6 +1993,33 @@ class _ProxyFile: self._pos = self._file.tell() return result + def __enter__(self): + """Context manager protocol support.""" + return self + + def __exit__(self, *exc): + self.close() + + def readable(self): + return self._file.readable() + + def writable(self): + return self._file.writable() + + def seekable(self): + return self._file.seekable() + + def flush(self): + return self._file.flush() + + @property + def closed(self): + if not hasattr(self, '_file'): + return True + if not hasattr(self._file, 'closed'): + return False + return self._file.closed + class _PartialFile(_ProxyFile): """A read-only wrapper of part of a file.""" @@ -1946,7 +2048,7 @@ class _PartialFile(_ProxyFile): """Read size bytes using read_method, honoring start and stop.""" remaining = self._stop - self._pos if remaining <= 0: - return '' + return b'' if size is None or size < 0 or size > remaining: size = remaining return _ProxyFile._read(self, size, read_method) @@ -1965,7 +2067,7 @@ def _lock_file(f, dotlock=True): if fcntl: try: fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError, e: + except IOError as e: if e.errno in (errno.EAGAIN, errno.EACCES, errno.EROFS): raise ExternalClashError('lockf: lock unavailable: %s' % f.name) @@ -1975,7 +2077,7 @@ def _lock_file(f, dotlock=True): try: pre_lock = _create_temporary(f.name + '.lock') pre_lock.close() - except IOError, e: + except IOError as e: if e.errno in (errno.EACCES, errno.EROFS): return # Without write access, just skip dotlocking. else: @@ -1988,7 +2090,7 @@ def _lock_file(f, dotlock=True): else: os.rename(pre_lock.name, f.name + '.lock') dotlock_done = True - except OSError, e: + except OSError as e: if e.errno == errno.EEXIST or \ (os.name == 'os2' and e.errno == errno.EACCES): os.remove(pre_lock.name) @@ -2012,7 +2114,7 @@ def _unlock_file(f): def _create_carefully(path): """Create a file if it doesn't exist and open for reading and writing.""" - fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0666) + fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0o666) try: return open(path, 'rb+') finally: @@ -2035,184 +2137,6 @@ def _sync_close(f): _sync_flush(f) f.close() -## Start: classes from the original module (for backward compatibility). - -# Note that the Maildir class, whose name is unchanged, itself offers a next() -# method for backward compatibility. - -class _Mailbox: - - def __init__(self, fp, factory=rfc822.Message): - self.fp = fp - self.seekp = 0 - self.factory = factory - - def __iter__(self): - return iter(self.next, None) - - def next(self): - while 1: - self.fp.seek(self.seekp) - try: - self._search_start() - except EOFError: - self.seekp = self.fp.tell() - return None - start = self.fp.tell() - self._search_end() - self.seekp = stop = self.fp.tell() - if start != stop: - break - return self.factory(_PartialFile(self.fp, start, stop)) - -# Recommended to use PortableUnixMailbox instead! -class UnixMailbox(_Mailbox): - - def _search_start(self): - while 1: - pos = self.fp.tell() - line = self.fp.readline() - if not line: - raise EOFError - if line[:5] == 'From ' and self._isrealfromline(line): - self.fp.seek(pos) - return - - def _search_end(self): - self.fp.readline() # Throw away header line - while 1: - pos = self.fp.tell() - line = self.fp.readline() - if not line: - return - if line[:5] == 'From ' and self._isrealfromline(line): - self.fp.seek(pos) - return - - # An overridable mechanism to test for From-line-ness. You can either - # specify a different regular expression or define a whole new - # _isrealfromline() method. Note that this only gets called for lines - # starting with the 5 characters "From ". - # - # BAW: According to - #http://home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html - # the only portable, reliable way to find message delimiters in a BSD (i.e - # Unix mailbox) style folder is to search for "\n\nFrom .*\n", or at the - # beginning of the file, "^From .*\n". While _fromlinepattern below seems - # like a good idea, in practice, there are too many variations for more - # strict parsing of the line to be completely accurate. - # - # _strict_isrealfromline() is the old version which tries to do stricter - # parsing of the From_ line. _portable_isrealfromline() simply returns - # true, since it's never called if the line doesn't already start with - # "From ". - # - # This algorithm, and the way it interacts with _search_start() and - # _search_end() may not be completely correct, because it doesn't check - # that the two characters preceding "From " are \n\n or the beginning of - # the file. Fixing this would require a more extensive rewrite than is - # necessary. For convenience, we've added a PortableUnixMailbox class - # which does no checking of the format of the 'From' line. - - _fromlinepattern = (r"From \s*[^\s]+\s+\w\w\w\s+\w\w\w\s+\d?\d\s+" - r"\d?\d:\d\d(:\d\d)?(\s+[^\s]+)?\s+\d\d\d\d\s*" - r"[^\s]*\s*" - "$") - _regexp = None - - def _strict_isrealfromline(self, line): - if not self._regexp: - import re - self._regexp = re.compile(self._fromlinepattern) - return self._regexp.match(line) - - def _portable_isrealfromline(self, line): - return True - - _isrealfromline = _strict_isrealfromline - - -class PortableUnixMailbox(UnixMailbox): - _isrealfromline = UnixMailbox._portable_isrealfromline - - -class MmdfMailbox(_Mailbox): - - def _search_start(self): - while 1: - line = self.fp.readline() - if not line: - raise EOFError - if line[:5] == '\001\001\001\001\n': - return - - def _search_end(self): - while 1: - pos = self.fp.tell() - line = self.fp.readline() - if not line: - return - if line == '\001\001\001\001\n': - self.fp.seek(pos) - return - - -class MHMailbox: - - def __init__(self, dirname, factory=rfc822.Message): - import re - pat = re.compile('^[1-9][0-9]*$') - self.dirname = dirname - # the three following lines could be combined into: - # list = map(long, filter(pat.match, os.listdir(self.dirname))) - list = os.listdir(self.dirname) - list = filter(pat.match, list) - list = map(long, list) - list.sort() - # This only works in Python 1.6 or later; - # before that str() added 'L': - self.boxes = map(str, list) - self.boxes.reverse() - self.factory = factory - - def __iter__(self): - return iter(self.next, None) - - def next(self): - if not self.boxes: - return None - fn = self.boxes.pop() - fp = open(os.path.join(self.dirname, fn)) - msg = self.factory(fp) - try: - msg._mh_msgno = fn - except (AttributeError, TypeError): - pass - return msg - - -class BabylMailbox(_Mailbox): - - def _search_start(self): - while 1: - line = self.fp.readline() - if not line: - raise EOFError - if line == '*** EOOH ***\n': - return - - def _search_end(self): - while 1: - pos = self.fp.tell() - line = self.fp.readline() - if not line: - return - if line == '\037\014\n' or line == '\037': - self.fp.seek(pos) - return - -## End: classes from the original module (for backward compatibility). - class Error(Exception): """Raised for module-specific errors.""" |