diff options
45 files changed, 932 insertions, 846 deletions
diff --git a/.travis.yml b/.travis.yml index 84c05c4d..3e7b5035 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,34 +14,14 @@ dist: trusty python: # We can add more version strings here when we support other python # versions. - - "2.7" - "3.5" - "3.6" # We start two containers in parallel, one to check and build the docs and the # other to run the test suite. env: + - JOB=docs - JOB=tests - # This job is temporarily included in the build matrix directly in order to - # only check the docs on python2 for now. When we finished switching to - # python3 we can put this back here and remove matrix.include. - #- JOB=docs - -# Until the switch to python3 is complete we allow the tests to fail with -# python3. When merging a working python3 version wen can remove this and the -# python version 2.7 above. -jobs: - allow_failures: - - python: "3.5" - - python: "3.6" - -# Check the docs only on python2 until we really support python3. See "env" -# above and -# https://docs.travis-ci.com/user/customizing-the-build/#Explicitly-Including-Jobs -matrix: - include: - - python: 2.7 - env: JOB=docs addons: apt: @@ -1,3 +1,6 @@ +0.8: +* Port to python 3. Python 2.x no longer supported + 0.7: * info: missing html mailcap entry now reported as mail body text * feature: Allow regex special characters in tagstrings diff --git a/alot/account.py b/alot/account.py index 00c65753..fe304ac6 100644 --- a/alot/account.py +++ b/alot/account.py @@ -65,16 +65,16 @@ class Address(object): usage treat user names as case-insensitve. Therefore we also, by default, treat the user name as case insenstive. - :param unicode user: The "user name" portion of the address. - :param unicode domain: The domain name portion of the address. + :param str user: The "user name" portion of the address. + :param str domain: The domain name portion of the address. :param bool case_sensitive: If False (the default) the user name portion of the address will be compared to the other user name portion without regard to case. If True then it will. """ def __init__(self, user, domain, case_sensitive=False): - assert isinstance(user, unicode), 'Username must be unicode' - assert isinstance(domain, unicode), 'Domain name must be unicode' + assert isinstance(user, str), 'Username must be str' + assert isinstance(domain, str), 'Domain name must be str' self.username = user self.domainname = domain self.case_sensitive = case_sensitive @@ -83,27 +83,24 @@ class Address(object): def from_string(cls, address, case_sensitive=False): """Alternate constructor for building from a string. - :param unicode address: An email address in <user>@<domain> form + :param str address: An email address in <user>@<domain> form :param bool case_sensitive: passed directly to the constructor argument of the same name. :returns: An account from the given arguments :rtype: :class:`Account` """ - assert isinstance(address, unicode), 'address must be unicode' - username, domainname = address.split(u'@') + assert isinstance(address, str), 'address must be str' + username, domainname = address.split('@') return cls(username, domainname, case_sensitive=case_sensitive) def __repr__(self): - return u'Address({!r}, {!r}, case_sensitive={})'.format( + return 'Address({!r}, {!r}, case_sensitive={})'.format( self.username, self.domainname, - unicode(self.case_sensitive)) - - def __unicode__(self): - return u'{}@{}'.format(self.username, self.domainname) + str(self.case_sensitive)) def __str__(self): - return u'{}@{}'.format(self.username, self.domainname).encode('utf-8') + return '{}@{}'.format(self.username, self.domainname) def __cmp(self, other, comparitor): """Shared helper for rich comparison operators. @@ -113,22 +110,17 @@ class Address(object): If the username is not considered case sensitive then lower the username of both self and the other, and handle that the other can be - either another :class:`~alot.account.Address`, or a `unicode` instance. + either another :class:`~alot.account.Address`, or a `str` instance. :param other: The other address to compare against - :type other: unicode or ~alot.account.Address + :type other: str or ~alot.account.Address :param callable comparitor: A function with the a signature - (unicode, unicode) -> bool that will compare the two instance. + (str, str) -> bool that will compare the two instance. The intention is to use functions from the operator module. """ - if isinstance(other, unicode): - try: - ouser, odomain = other.split(u'@') - except ValueError: - ouser, odomain = u'', u'' - elif isinstance(other, str): + if isinstance(other, str): try: - ouser, odomain = other.decode('utf-8').split(u'@') + ouser, odomain = other.split('@') except ValueError: ouser, odomain = '', '' else: @@ -145,13 +137,13 @@ class Address(object): comparitor(self.domainname.lower(), odomain.lower())) def __eq__(self, other): - if not isinstance(other, (Address, basestring)): - raise TypeError('Address must be compared to Address or basestring') + if not isinstance(other, (Address, str)): + raise TypeError('Address must be compared to Address or str') return self.__cmp(other, operator.eq) def __ne__(self, other): - if not isinstance(other, (Address, basestring)): - raise TypeError('Address must be compared to Address or basestring') + if not isinstance(other, (Address, str)): + raise TypeError('Address must be compared to Address or str') # != is the only rich comparitor that cannot be implemented using 'and' # in self.__cmp, so it's implemented as not ==. return not self.__cmp(other, operator.eq) @@ -221,9 +213,11 @@ class Account(object): replied_tags = replied_tags or [] passed_tags = passed_tags or [] - self.address = Address.from_string(address, case_sensitive=case_sensitive_username) - self.aliases = [Address.from_string(a, case_sensitive=case_sensitive_username) - for a in (aliases or [])] + self.address = Address.from_string( + address, case_sensitive=case_sensitive_username) + self.aliases = [ + Address.from_string(a, case_sensitive=case_sensitive_username) + for a in (aliases or [])] self.alias_regexp = alias_regexp self.realname = realname self.encrypt_to_self = encrypt_to_self diff --git a/alot/buffers.py b/alot/buffers.py index d1c6583b..bfff3897 100644 --- a/alot/buffers.py +++ b/alot/buffers.py @@ -147,7 +147,7 @@ class EnvelopeBuffer(Buffer): hidden = settings.get('envelope_headers_blacklist') # build lines lines = [] - for (k, vlist) in self.envelope.headers.iteritems(): + for (k, vlist) in self.envelope.headers.items(): if (k not in hidden) or self.all_headers: for value in vlist: lines.append((k, value)) @@ -662,7 +662,7 @@ class TagListBuffer(Buffer): lines = list() displayedtags = sorted((t for t in self.tags if self.filtfun(t)), - key=unicode.lower) + key=str.lower) for (num, b) in enumerate(displayedtags): if (num % 2) == 0: attr = settings.get_theming_attribute('taglist', 'line_even') diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index 0aa95cf2..0485aa00 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -145,8 +145,8 @@ class SaveCommand(Command): ui.apply_command(globals.FlushCommand()) ui.apply_command(commands.globals.BufferCloseCommand()) except DatabaseError as e: - logging.error(e) - ui.notify('could not index message:\n%s' % e, + logging.error(str(e)) + ui.notify('could not index message:\n%s' % str(e), priority='error', block=True) else: @@ -629,13 +629,13 @@ class TagCommand(Command): def __init__(self, tags=u'', action='add', **kwargs): """ :param tags: comma separated list of tagstrings to set - :type tags: unicode + :type tags: str :param action: adds tags if 'add', removes them if 'remove', adds tags and removes all other if 'set' or toggle individually if 'toggle' :type action: str """ - assert isinstance(tags, unicode), 'tags should be a unicode string' + assert isinstance(tags, str), 'tags should be a unicode string' self.tagsstring = tags self.action = action Command.__init__(self, **kwargs) diff --git a/alot/commands/globals.py b/alot/commands/globals.py index 4e0ed506..4e272b8e 100644 --- a/alot/commands/globals.py +++ b/alot/commands/globals.py @@ -12,7 +12,7 @@ import glob import logging import os import subprocess -from io import StringIO +from io import BytesIO import urwid from twisted.internet.defer import inlineCallbacks @@ -211,7 +211,7 @@ class ExternalCommand(Command): """ logging.debug({'spawn': spawn}) # make sure cmd is a list of str - if isinstance(cmd, unicode): + if isinstance(cmd, str): # convert cmdstring to list: in case shell==True, # Popen passes only the first item in the list to $SHELL cmd = [cmd] if shell else split_commandstring(cmd) @@ -249,9 +249,11 @@ class ExternalCommand(Command): # set standard input for subcommand stdin = None if self.stdin is not None: - # wrap strings in StringIO so that they behave like files - if isinstance(self.stdin, unicode): - stdin = StringIO(self.stdin) + # wrap strings in StrinIO so that they behaves like a file + if isinstance(self.stdin, str): + # XXX: is utf-8 always safe to use here, or do we need to check + # the terminal encoding first? + stdin = BytesIO(self.stdin.encode('utf-8')) else: stdin = self.stdin @@ -269,16 +271,19 @@ class ExternalCommand(Command): def thread_code(*_): try: - proc = subprocess.Popen(self.cmdlist, shell=self.shell, - stdin=subprocess.PIPE if stdin else None, - stderr=subprocess.PIPE) + proc = subprocess.Popen( + self.cmdlist, shell=self.shell, + stdin=subprocess.PIPE if stdin else None, + stderr=subprocess.PIPE) except OSError as e: return str(e) _, err = proc.communicate(stdin.read() if stdin else None) if proc.returncode == 0: return 'success' - return err.strip() + if err: + return err.decode(urwid.util.detected_encoding) + return '' if self.in_thread: d = threads.deferToThread(thread_code) @@ -381,7 +386,7 @@ class CallCommand(Command): hooks = settings.hooks if hooks: env = {'ui': ui, 'settings': settings} - for k, v in env.iteritems(): + for k, v in env.items(): if k not in hooks.__dict__: hooks.__dict__[k] = v @@ -619,8 +624,8 @@ class HelpCommand(Command): globalmaps, modemaps = settings.get_keybindings(ui.mode) # build table - maxkeylength = len(max((modemaps).keys() + globalmaps.keys(), - key=len)) + maxkeylength = len( + max(list(modemaps.keys()) + list(globalmaps.keys()), key=len)) keycolumnwidth = maxkeylength + 2 linewidgets = [] @@ -628,7 +633,7 @@ class HelpCommand(Command): if modemaps: txt = (section_att, '\n%s-mode specific maps' % ui.mode) linewidgets.append(urwid.Text(txt)) - for (k, v) in modemaps.iteritems(): + for (k, v) in modemaps.items(): line = urwid.Columns([('fixed', keycolumnwidth, urwid.Text((text_att, k))), urwid.Text((text_att, v))]) @@ -636,7 +641,7 @@ class HelpCommand(Command): # global maps linewidgets.append(urwid.Text((section_att, '\nglobal maps'))) - for (k, v) in globalmaps.iteritems(): + for (k, v) in globalmaps.items(): if k not in modemaps: line = urwid.Columns( [('fixed', keycolumnwidth, urwid.Text((text_att, k))), @@ -685,10 +690,12 @@ class HelpCommand(Command): class ComposeCommand(Command): """compose a new email""" - def __init__(self, envelope=None, headers=None, template=None, sender=u'', - tags=None, subject=u'', to=None, cc=None, bcc=None, attach=None, - omit_signature=False, spawn=None, rest=None, encrypt=False, - **kwargs): + def __init__( + self, + envelope=None, headers=None, template=None, sender=u'', + tags=None, subject=u'', to=None, cc=None, bcc=None, attach=None, + omit_signature=False, spawn=None, rest=None, encrypt=False, + **kwargs): """ :param envelope: use existing envelope :type envelope: :class:`~alot.db.envelope.Envelope` @@ -770,16 +777,14 @@ class ComposeCommand(Command): return try: with open(path, 'rb') as f: - blob = f.read() - encoding = helper.guess_encoding(blob) - logging.debug('template encoding: `%s`' % encoding) - self.envelope.parse_template(blob.decode(encoding)) + template = helper.try_decode(f.read()) + self.envelope.parse_template(template) except Exception as e: ui.notify(str(e), priority='error') return # set forced headers - for key, value in self.headers.iteritems(): + for key, value in self.headers.items(): self.envelope.add(key, value) # set forced headers for separate parameters @@ -844,10 +849,9 @@ class ComposeCommand(Command): else: with open(sig) as f: sigcontent = f.read() - enc = helper.guess_encoding(sigcontent) mimetype = helper.guess_mimetype(sigcontent) if mimetype.startswith('text'): - sigcontent = helper.string_decode(sigcontent, enc) + sigcontent = helper.try_decode(sigcontent) self.envelope.body += '\n' + sigcontent else: ui.notify('could not locate signature: %s' % sig, diff --git a/alot/commands/thread.py b/alot/commands/thread.py index 17fc6c3b..ad854a5a 100644 --- a/alot/commands/thread.py +++ b/alot/commands/thread.py @@ -14,7 +14,8 @@ from email.utils import getaddresses, parseaddr, formataddr from email.message import Message from twisted.internet.defer import inlineCallbacks -from io import BytesIO +import urwid +from io import StringIO from . import Command, registerCommand from .globals import ExternalCommand @@ -71,30 +72,34 @@ def determine_sender(mail, action='reply'): # pick the most important account that has an address in candidates # and use that accounts realname and the address found here for account in my_accounts: - acc_addresses = [re.escape(unicode(a)) for a in account.get_addresses()] + acc_addresses = [ + re.escape(str(a)) for a in account.get_addresses()] if account.alias_regexp is not None: acc_addresses.append(account.alias_regexp) for alias in acc_addresses: regex = re.compile( - u'^' + unicode(alias) + u'$', - flags=re.IGNORECASE if not account.address.case_sensitive else 0) + u'^' + str(alias) + u'$', + flags=( + re.IGNORECASE if not account.address.case_sensitive + else 0)) for seen_name, seen_address in candidate_addresses: - if regex.match(seen_address): - logging.debug("match!: '%s' '%s'", seen_address, alias) - if settings.get(action + '_force_realname'): - realname = account.realname - else: - realname = seen_name - if settings.get(action + '_force_address'): - address = account.address - else: - address = seen_address - - logging.debug('using realname: "%s"', realname) - logging.debug('using address: %s', address) - - from_value = formataddr((realname, address)) - return from_value, account + if not regex.match(seen_address): + continue + logging.debug("match!: '%s' '%s'", seen_address, alias) + if settings.get(action + '_force_realname'): + realname = account.realname + else: + realname = seen_name + if settings.get(action + '_force_address'): + address = account.address + else: + address = seen_address + + logging.debug('using realname: "%s"', realname) + logging.debug('using address: %s', address) + + from_value = formataddr((realname, str(address))) + return from_value, account # revert to default account if nothing found account = my_accounts[0] @@ -103,7 +108,7 @@ def determine_sender(mail, action='reply'): logging.debug('using realname: "%s"', realname) logging.debug('using address: %s', address) - from_value = formataddr((realname, address)) + from_value = formataddr((realname, str(address))) return from_value, account @@ -241,6 +246,7 @@ class ReplyCommand(Command): # X-BeenThere is needed by sourceforge ML also winehq # X-Mailing-List is also standart and is used by git-send-mail to = mail['Reply-To'] or mail['X-BeenThere'] or mail['X-Mailing-List'] + # Some mail server (gmail) will not resend you own mail, so you # have to deal with the one in sent if to is None: @@ -301,7 +307,7 @@ class ReplyCommand(Command): new_value = [] for name, address in getaddresses(value): if address not in my_addresses: - new_value.append(formataddr((name, address))) + new_value.append(formataddr((name, str(address)))) return new_value @staticmethod @@ -313,7 +319,7 @@ class ReplyCommand(Command): res = dict() for name, address in getaddresses(recipients): res[address] = name - urecipients = [formataddr((n, a)) for a, n in res.iteritems()] + urecipients = [formataddr((n, str(a))) for a, n in res.items()] return sorted(urecipients) @@ -689,7 +695,7 @@ class PipeCommand(Command): :type field_key: str """ Command.__init__(self, **kwargs) - if isinstance(cmd, unicode): + if isinstance(cmd, str): cmd = split_commandstring(cmd) self.cmd = cmd self.whole_thread = all @@ -759,13 +765,14 @@ class PipeCommand(Command): # do the monkey for mail in pipestrings: + encoded_mail = mail.encode(urwid.util.detected_encoding) if self.background: logging.debug('call in background: %s', self.cmd) proc = subprocess.Popen(self.cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = proc.communicate(mail) + out, err = proc.communicate(encoded_mail) if self.notify_stdout: ui.notify(out) else: @@ -777,7 +784,7 @@ class PipeCommand(Command): stdin=subprocess.PIPE, # stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = proc.communicate(mail) + out, err = proc.communicate(encoded_mail) if err: ui.notify(err, priority='error') return @@ -993,7 +1000,7 @@ class OpenAttachmentCommand(Command): def afterwards(): os.unlink(tempfile_name) else: - handler_stdin = BytesIO() + handler_stdin = StringIO() self.attachment.write(handler_stdin) # create handler command list diff --git a/alot/completion.py b/alot/completion.py index 48824909..e338ee0b 100644 --- a/alot/completion.py +++ b/alot/completion.py @@ -277,7 +277,7 @@ class AccountCompleter(StringlistCompleter): def __init__(self, **kwargs): accounts = settings.get_accounts() - resultlist = [email.utils.formataddr((a.realname, a.address)) + resultlist = [email.utils.formataddr((a.realname, str(a.address))) for a in accounts] StringlistCompleter.__init__(self, resultlist, match_anywhere=True, **kwargs) diff --git a/alot/crypto.py b/alot/crypto.py index 34bdccb5..e7e0bd36 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -1,6 +1,5 @@ -# encoding=utf-8 # Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com> -# Copyright © 2017 Dylan Baker <dylan@pnwbakers.com> +# Copyright © 2017-2018 Dylan Baker <dylan@pnwbakers.com> # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file from __future__ import absolute_import @@ -113,8 +112,9 @@ def get_key(keyid, validate=False, encrypt=False, sign=False, else: raise e # pragma: nocover if signed_only and not check_uid_validity(key, keyid): - raise GPGProblem('Cannot find a trusworthy key for "{}".'.format(keyid), - code=GPGCode.NOT_FOUND) + raise GPGProblem( + 'Cannot find a trusworthy key for "{}".'.format(keyid), + code=GPGCode.NOT_FOUND) return key @@ -144,7 +144,7 @@ def detached_signature_for(plaintext_str, keys): A detached signature in GPG speak is a separate blob of data containing a signature for the specified plaintext. - :param str plaintext_str: text to sign + :param bytes plaintext_str: bytestring to sign :param keys: list of one or more key to sign with. :type keys: list[gpg.gpgme._gpgme_key] :returns: A list of signature and the signed blob of data @@ -160,7 +160,7 @@ def detached_signature_for(plaintext_str, keys): def encrypt(plaintext_str, keys): """Encrypt data and return the encrypted form. - :param str plaintext_str: the mail to encrypt + :param bytes plaintext_str: the mail to encrypt :param key: optionally, a list of keys to encrypt with :type key: list[gpg.gpgme.gpgme_key_t] or None :returns: encrypted mail @@ -192,8 +192,8 @@ def bad_signatures_to_str(error): def verify_detached(message, signature): """Verifies whether the message is authentic by checking the signature. - :param str message: The message to be verified, in canonical form. - :param str signature: the OpenPGP signature to verify + :param bytes message: The message to be verified, in canonical form. + :param bytes signature: the OpenPGP signature to verify :returns: a list of signatures :rtype: list[gpg.results.Signature] :raises: :class:`~alot.errors.GPGProblem` if the verification fails @@ -212,7 +212,7 @@ def decrypt_verify(encrypted): """Decrypts the given ciphertext string and returns both the signatures (if any) and the plaintext. - :param str encrypted: the mail to decrypt + :param bytes encrypted: the mail to decrypt :returns: the signatures and decrypted plaintext data :rtype: tuple[list[gpg.resuit.Signature], str] :raises: :class:`~alot.errors.GPGProblem` if the decryption fails diff --git a/alot/db/attachment.py b/alot/db/attachment.py index b35092e5..1181a125 100644 --- a/alot/db/attachment.py +++ b/alot/db/attachment.py @@ -68,11 +68,11 @@ class Attachment(object): if os.path.isdir(path): if filename: basename = os.path.basename(filename) - file_ = open(os.path.join(path, basename), "w") + file_ = open(os.path.join(path, basename), "wb") else: file_ = tempfile.NamedTemporaryFile(delete=False, dir=path) else: - file_ = open(path, "w") # this throws IOErrors for invalid path + file_ = open(path, "wb") # this throws IOErrors for invalid path self.write(file_) file_.close() return file_.name diff --git a/alot/db/envelope.py b/alot/db/envelope.py index 90a58654..89a99ffa 100644 --- a/alot/db/envelope.py +++ b/alot/db/envelope.py @@ -162,7 +162,7 @@ class Envelope(object): if isinstance(attachment, Attachment): self.attachments.append(attachment) - elif isinstance(attachment, basestring): + elif isinstance(attachment, str): path = os.path.expanduser(attachment) part = helper.mimewrap(path, filename, ctype) self.attachments.append(Attachment(part)) @@ -193,7 +193,7 @@ class Envelope(object): inner_msg = textpart if self.sign: - plaintext = helper.email_as_string(inner_msg) + plaintext = helper.email_as_bytes(inner_msg) logging.debug('signing plaintext: %s', plaintext) try: @@ -223,9 +223,10 @@ class Envelope(object): # wrap signature in MIMEcontainter stype = 'pgp-signature; name="signature.asc"' - signature_mime = MIMEApplication(_data=signature_str, - _subtype=stype, - _encoder=encode_7or8bit) + signature_mime = MIMEApplication( + _data=signature_str.decode('ascii'), + _subtype=stype, + _encoder=encode_7or8bit) signature_mime['Content-Description'] = 'signature' signature_mime.set_charset('us-ascii') @@ -237,12 +238,12 @@ class Envelope(object): unencrypted_msg = inner_msg if self.encrypt: - plaintext = helper.email_as_string(unencrypted_msg) + plaintext = helper.email_as_bytes(unencrypted_msg) logging.debug('encrypting plaintext: %s', plaintext) try: - encrypted_str = crypto.encrypt(plaintext, - self.encrypt_keys.values()) + encrypted_str = crypto.encrypt( + plaintext, list(self.encrypt_keys.values())) except gpg.errors.GPGMEError as e: raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_ENCRYPT) @@ -255,9 +256,10 @@ class Envelope(object): _encoder=encode_7or8bit) encryption_mime.set_charset('us-ascii') - encrypted_mime = MIMEApplication(_data=encrypted_str, - _subtype='octet-stream', - _encoder=encode_7or8bit) + encrypted_mime = MIMEApplication( + _data=encrypted_str.decode('ascii'), + _subtype='octet-stream', + _encoder=encode_7or8bit) encrypted_mime.set_charset('us-ascii') outer_msg.attach(encryption_mime) outer_msg.attach(encrypted_mime) @@ -279,7 +281,7 @@ class Envelope(object): headers['User-Agent'] = [uastring] # copy headers from envelope to mail - for k, vlist in headers.iteritems(): + for k, vlist in headers.items(): for v in vlist: outer_msg[k] = encode_header(k, v) diff --git a/alot/db/manager.py b/alot/db/manager.py index 93a38b81..057c72d0 100644 --- a/alot/db/manager.py +++ b/alot/db/manager.py @@ -145,8 +145,7 @@ class DBManager(object): msg.freeze() logging.debug('freeze') for tag in tags: - msg.add_tag(tag.encode(DB_ENC), - sync_maildir_flags=sync) + msg.add_tag(tag, sync_maildir_flags=sync) logging.debug('added tags ') msg.thaw() logging.debug('thaw') @@ -161,18 +160,14 @@ class DBManager(object): for msg in query.search_messages(): msg.freeze() if cmd == 'tag': - for tag in tags: - msg.add_tag(tag.encode(DB_ENC), - sync_maildir_flags=sync) + strategy = msg.add_tag if cmd == 'set': msg.remove_all_tags() - for tag in tags: - msg.add_tag(tag.encode(DB_ENC), - sync_maildir_flags=sync) + strategy = msg.add_tag elif cmd == 'untag': - for tag in tags: - msg.remove_tag(tag.encode(DB_ENC), - sync_maildir_flags=sync) + strategy = msg.remove_tag + for tag in tags: + strategy(tag, sync_maildir_flags=sync) msg.thaw() logging.debug('ended atomic') @@ -195,7 +190,7 @@ class DBManager(object): except (XapianError, NotmuchError) as e: logging.exception(e) self.writequeue.appendleft(current_item) - raise DatabaseError(unicode(e)) + raise DatabaseError(str(e)) except DatabaseLockedError as e: logging.debug('index temporarily locked') self.writequeue.appendleft(current_item) diff --git a/alot/db/message.py b/alot/db/message.py index c4ea5f8a..2e1fef54 100644 --- a/alot/db/message.py +++ b/alot/db/message.py @@ -10,7 +10,8 @@ from datetime import datetime from notmuch import NullPointerError -from .utils import extract_body, message_from_file +from . import utils +from .utils import extract_body from .utils import decode_header from .attachment import Attachment from .. import helper @@ -64,7 +65,7 @@ class Message(object): self._from = sender elif 'draft' in self._tags: acc = settings.get_accounts()[0] - self._from = '"{}" <{}>'.format(acc.realname, unicode(acc.address)) + self._from = '"{}" <{}>'.format(acc.realname, str(acc.address)) else: self._from = '"Unknown" <>' @@ -101,8 +102,8 @@ class Message(object): "Message file is no longer accessible:\n%s" % path if not self._email: try: - with open(path) as f: - self._email = message_from_file(f) + with open(path, 'rb') as f: + self._email = utils.message_from_bytes(f.read()) except IOError: self._email = email.message_from_string(warning) return self._email diff --git a/alot/db/thread.py b/alot/db/thread.py index 3d9144db..25b75fb1 100644 --- a/alot/db/thread.py +++ b/alot/db/thread.py @@ -86,7 +86,7 @@ class Thread(object): """ tags = set(list(self._tags)) if intersection: - for m in self.get_messages().iterkeys(): + for m in self.get_messages().keys(): tags = tags.intersection(set(m.get_tags())) return tags @@ -157,7 +157,7 @@ class Thread(object): if self._authors is None: # Sort messages with date first (by date ascending), and those # without a date last. - msgs = sorted(self.get_messages().iterkeys(), + msgs = sorted(self.get_messages().keys(), key=lambda m: m.get_date() or datetime.max) orderby = settings.get('thread_authors_order_by') @@ -257,7 +257,7 @@ class Thread(object): """ mid = msg.get_message_id() msg_hash = self.get_messages() - for m in msg_hash.iterkeys(): + for m in msg_hash.keys(): if m.get_message_id() == mid: return msg_hash[m] return None diff --git a/alot/db/utils.py b/alot/db/utils.py index e574cd2e..c0fc09f3 100644 --- a/alot/db/utils.py +++ b/alot/db/utils.py @@ -15,7 +15,11 @@ import tempfile import re import logging import mailcap -from io import BytesIO +import io +import base64 +import quopri + +from urwid.util import detected_encoding from .. import crypto from .. import helper @@ -41,15 +45,14 @@ def add_signature_headers(mail, sigs, error_msg): :param mail: :class:`email.message.Message` the message to entitle :param sigs: list of :class:`gpg.results.Signature` - :param error_msg: `str` containing an error message, the empty - string indicating no error + :param error_msg: An error message if there is one, or None + :type error_msg: :class:`str` or `None` ''' - sig_from = u'' + sig_from = '' sig_known = True uid_trusted = False - if isinstance(error_msg, str): - error_msg = error_msg.decode('utf-8') + assert error_msg is None or isinstance(error_msg, str) if not sigs: error_msg = error_msg or u'no signature found' @@ -58,22 +61,22 @@ def add_signature_headers(mail, sigs, error_msg): key = crypto.get_key(sigs[0].fpr) for uid in key.uids: if crypto.check_uid_validity(key, uid.email): - sig_from = uid.uid.decode('utf-8') + sig_from = uid.uid uid_trusted = True break else: # No trusted uid found, since we did not break from the loop. - sig_from = key.uids[0].uid.decode('utf-8') + sig_from = key.uids[0].uid except GPGProblem: - sig_from = sigs[0].fpr.decode('utf-8') + sig_from = sigs[0].fpr sig_known = False if error_msg: - msg = u'Invalid: {}'.format(error_msg) + msg = 'Invalid: {}'.format(error_msg) elif uid_trusted: - msg = u'Valid: {}'.format(sig_from) + msg = 'Valid: {}'.format(sig_from) else: - msg = u'Untrusted: {}'.format(sig_from) + msg = 'Untrusted: {}'.format(sig_from) mail.add_header(X_SIGNATURE_VALID_HEADER, 'False' if (error_msg or not sig_known) else 'True') @@ -112,7 +115,7 @@ def _handle_signatures(original, message, params): :param params: the message parameters as returned by :func:`get_params` :type params: dict[str, str] """ - malformed = False + malformed = None if len(message.get_payload()) != 2: malformed = u'expected exactly two messages, got {0}'.format( len(message.get_payload())) @@ -133,10 +136,10 @@ def _handle_signatures(original, message, params): if not malformed: try: sigs = crypto.verify_detached( - helper.email_as_string(message.get_payload(0)), - message.get_payload(1).get_payload()) + helper.email_as_bytes(message.get_payload(0)), + message.get_payload(1).get_payload(decode=True)) except GPGProblem as e: - malformed = unicode(e) + malformed = str(e) add_signature_headers(original, sigs, malformed) @@ -170,15 +173,17 @@ def _handle_encrypted(original, message): malformed = u'expected Content-Type: {0}, got: {1}'.format(want, ct) if not malformed: + # This should be safe because PGP uses US-ASCII characters only + payload = message.get_payload(1).get_payload().encode('ascii') try: - sigs, d = crypto.decrypt_verify(message.get_payload(1).get_payload()) + sigs, d = crypto.decrypt_verify(payload) except GPGProblem as e: # signature verification failures end up here too if the combined # method is used, currently this prevents the interpretation of the # recovered plain text mail. maybe that's a feature. - malformed = unicode(e) + malformed = str(e) else: - n = message_from_string(d) + n = message_from_bytes(d) # add the decrypted message to message. note that n contains all # the attachments, no need to walk over n here. @@ -208,7 +213,7 @@ def _handle_encrypted(original, message): if malformed: msg = u'Malformed OpenPGP message: {0}'.format(malformed) - content = email.message_from_string(msg.encode('utf-8')) + content = email.message_from_string(msg) content.set_charset('utf-8') original.attach(content) @@ -265,14 +270,24 @@ def message_from_file(handle): def message_from_string(s): '''Reads a mail from the given string. This is the equivalent of :func:`email.message_from_string` which does nothing but to wrap - the given string in a BytesIO object and to call + the given string in a StringIO object and to call :func:`email.message_from_file`. Please refer to the documentation of :func:`message_from_file` for details. ''' - return message_from_file(BytesIO(s)) + return message_from_file(io.StringIO(s)) + + +def message_from_bytes(bytestring): + """Create a Message from bytes. + + Attempt to guess the encoding of the bytestring. + + :param bytes bytestring: an email message as raw bytes + """ + return message_from_file(io.StringIO(helper.try_decode(bytestring))) def extract_headers(mail, headers=None): @@ -297,9 +312,6 @@ def extract_headers(mail, headers=None): return headertext - - - def render_part(part, field_key='copiousoutput'): """ renders a non-multipart email part into displayable plaintext by piping its @@ -307,7 +319,7 @@ def render_part(part, field_key='copiousoutput'): the mailcap entry for this part's ctype. """ ctype = part.get_content_type() - raw_payload = part.get_payload(decode=True) + raw_payload = remove_cte(part) rendered_payload = None # get mime handler _, entry = settings.mailcap_find_match(ctype, key=field_key) @@ -350,6 +362,58 @@ def render_part(part, field_key='copiousoutput'): return rendered_payload +def remove_cte(part, as_string=False): + """Decodes any Content-Transfer-Encodings. + + Can return a string for display, or bytes to be passed to an external + program. + + :param email.Message part: The part to decode + :param bool as_string: If true return a str, otherwise return bytes + :returns: The mail with any Content-Transfer-Encoding removed + :rtype: Union[str, bytes] + """ + enc = part.get_content_charset() or 'ascii' + cte = str(part.get('content-transfer-encoding', '7bit')).lower() + payload = part.get_payload() + if cte == '8bit': + # Python's mail library may decode 8bit as raw-unicode-escape, so + # we need to encode that back to bytes so we can decode it using + # the correct encoding, or it might not, in which case assume that + # the str representation we got is correct. + raw_payload = payload.encode('raw-unicode-escape') + if not as_string: + return raw_payload + try: + return raw_payload.decode(enc) + except LookupError: + # In this case the email has an unknown encoding, fall back to + # guessing + return helper.try_decode(raw_payload) + except UnicodeDecodeError: + if not as_string: + return raw_payload + return helper.try_decode(raw_payload) + elif cte in ['7bit', 'binary']: + if as_string: + return payload + return payload.encode('utf-8') + else: + if cte == 'quoted-printable': + raw_payload = quopri.decodestring(payload.encode('ascii')) + elif cte == 'base64': + raw_payload = base64.b64decode(payload) + else: + raise Exception( + 'Unknown Content-Transfer-Encoding {}'.format(cte)) + # message.get_payload(decode=True) also handles a number of unicode + # encodindigs. maybe those are useful? + if not as_string: + return raw_payload + return raw_payload.decode(enc) + raise Exception('Unreachable') + + def extract_body(mail, types=None, field_key='copiousoutput'): """Returns a string view of a Message. @@ -396,9 +460,7 @@ def extract_body(mail, types=None, field_key='copiousoutput'): continue if ctype == 'text/plain': - enc = part.get_content_charset() or 'ascii' - raw_payload = string_decode(part.get_payload(decode=True), enc) - body_parts.append(string_sanitize(raw_payload)) + body_parts.append(string_sanitize(remove_cte(part, as_string=True))) else: rendered_payload = render_part(part) if rendered_payload: # handler had output @@ -420,22 +482,13 @@ def decode_header(header, normalize=False): :type header: str :param normalize: replace trailing spaces after newlines :type normalize: bool - :rtype: unicode + :rtype: str """ - - # If the value isn't ascii as RFC2822 prescribes, - # we just return the unicode bytestring as is - value = string_decode(header) # convert to unicode - try: - value = value.encode('ascii') - except UnicodeEncodeError: - return value - # some mailers send out incorrectly escaped headers # and double quote the escaped realname part again. remove those # RFC: 2047 regex = r'"(=\?.+?\?.+?\?[^ ?]+\?=)"' - value = re.sub(regex, r'\1', value) + value = re.sub(regex, r'\1', header) logging.debug("unquoted header: |%s|", value) # otherwise we interpret RFC2822 encoding escape sequences @@ -444,7 +497,7 @@ def decode_header(header, normalize=False): for v, enc in valuelist: v = string_decode(v, enc) decoded_list.append(string_sanitize(v)) - value = u' '.join(decoded_list) + value = ''.join(decoded_list) if normalize: value = re.sub(r'\n\s+', r' ', value) return value diff --git a/alot/helper.py b/alot/helper.py index 5fab4819..e621f751 100644 --- a/alot/helper.py +++ b/alot/helper.py @@ -10,7 +10,7 @@ from datetime import timedelta from datetime import datetime from collections import deque from io import BytesIO -from cStringIO import StringIO +from io import StringIO import logging import mimetypes import os @@ -25,6 +25,7 @@ from email.mime.image import MIMEImage from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart +import chardet import urwid import magic from twisted.internet import reactor @@ -40,9 +41,6 @@ def split_commandline(s, comments=False, posix=True): s = s.replace('\\', '\\\\') s = s.replace('\'', '\\\'') s = s.replace('\"', '\\\"') - # encode s to utf-8 for shlex - if isinstance(s, unicode): - s = s.encode('utf-8') lex = shlex.shlex(s, posix=posix) lex.whitespace_split = True lex.whitespace = ';' @@ -57,8 +55,7 @@ def split_commandstring(cmdstring): and the like. This simply calls shlex.split but works also with unicode bytestrings. """ - if isinstance(cmdstring, unicode): - cmdstring = cmdstring.encode('utf-8', errors='ignore') + assert isinstance(cmdstring, str) return shlex.split(cmdstring) @@ -118,10 +115,10 @@ def string_decode(string, enc='ascii'): if enc is None: enc = 'ascii' try: - string = unicode(string, enc, errors='replace') + string = str(string, enc, errors='replace') except LookupError: # malformed enc string string = string.decode('ascii', errors='replace') - except TypeError: # already unicode + except TypeError: # already str pass return string @@ -280,8 +277,11 @@ def call_cmd(cmdlist, stdin=None): :param stdin: string to pipe to the process :type stdin: str :return: triple of stdout, stderr, return value of the shell command - :rtype: str, str, int + :rtype: str, str, intd """ + termenc = urwid.util.detected_encoding + if stdin: + stdin = stdin.encode(termenc) try: proc = subprocess.Popen( cmdlist, @@ -296,8 +296,8 @@ def call_cmd(cmdlist, stdin=None): out, err = proc.communicate(stdin) ret = proc.returncode - out = string_decode(out, urwid.util.detected_encoding) - err = string_decode(err, urwid.util.detected_encoding) + out = string_decode(out, termenc) + err = string_decode(err, termenc) return out, err, ret @@ -312,6 +312,8 @@ def call_cmd_async(cmdlist, stdin=None, env=None): return value of the shell command :rtype: `twisted.internet.defer.Deferred` """ + termenc = urwid.util.detected_encoding + cmdlist = [s.encode(termenc) for s in cmdlist] class _EverythingGetter(ProcessProtocol): def __init__(self, deferred): @@ -322,7 +324,6 @@ def call_cmd_async(cmdlist, stdin=None, env=None): self.errReceived = self.errBuf.write def processEnded(self, status): - termenc = urwid.util.detected_encoding out = string_decode(self.outBuf.getvalue(), termenc) err = string_decode(self.errBuf.getvalue(), termenc) if status.value.exitCode == 0: @@ -343,7 +344,7 @@ def call_cmd_async(cmdlist, stdin=None, env=None): args=cmdlist) if stdin: logging.debug('writing to stdin') - proc.write(stdin) + proc.write(stdin.encode(termenc)) proc.closeStdin() return d @@ -386,34 +387,28 @@ def guess_mimetype(blob): def guess_encoding(blob): - """ - uses file magic to determine the encoding of the given data blob. + """Use chardet to guess the encoding of a given data blob - :param blob: file content as read by file.read() - :type blob: data + :param blob: A blob of bytes + :type blob: bytes :returns: encoding :rtype: str """ - # this is a bit of a hack to support different versions of python magic. - # Hopefully at some point this will no longer be necessary - # - # the version with open() is the bindings shipped with the file source from - # http://darwinsys.com/file/ - this is what is used by the python-magic - # package on Debian/Ubuntu. However it is not available on pypi/via pip. - # - # the version with from_buffer() is available at - # https://github.com/ahupp/python-magic and directly installable via pip. - # - # for more detail see https://github.com/pazz/alot/pull/588 - if hasattr(magic, 'open'): - m = magic.open(magic.MAGIC_MIME_ENCODING) - m.load() - return m.buffer(blob) - elif hasattr(magic, 'from_buffer'): - m = magic.Magic(mime_encoding=True) - return m.from_buffer(blob) - else: - raise Exception('Unknown magic API') + info = chardet.detect(blob) + logging.debug('Encoding %s with confidence %f', + info['encoding'], info['confidence']) + return info['encoding'] + + +def try_decode(blob): + """Guess the encoding of blob and try to decode it into a str. + + :param bytes blob: The bytes to decode + :returns: the decoded blob + :rtype: str + """ + assert isinstance(blob, bytes), 'cannot decode a str or non-bytes object' + return blob.decode(guess_encoding(blob)) def libmagic_version_at_least(version): @@ -627,6 +622,34 @@ def email_as_string(mail): return as_string +def email_as_bytes(mail): + string = email_as_string(mail) + charset = mail.get_charset() + if charset: + charset = str(charset) + else: + charsets = set(mail.get_charsets()) + if None in charsets: + # None is equal to US-ASCII + charsets.discard(None) + charsets.add('ascii') + + if len(charsets) == 1: + charset = list(charsets)[0] + elif 'ascii' in charsets: + # If we get here and the assert triggers it means that different + # parts of the email are encoded differently. I don't think we're + # likely to see that, but it's possible + if not {'utf-8', 'ascii', 'us-ascii'}.issuperset(charsets): + raise RuntimeError( + "different encodings detected: {}".format(charsets)) + charset = 'utf-8' # It's a strict super-set + else: + charset = 'utf-8' + + return string.encode(charset) + + def get_xdg_env(env_name, fallback): """ Used for XDG_* env variables to return fallback if unset *or* empty """ env = os.environ.get(env_name) diff --git a/alot/settings/manager.py b/alot/settings/manager.py index 9cd89256..56e6eec8 100644 --- a/alot/settings/manager.py +++ b/alot/settings/manager.py @@ -38,9 +38,9 @@ class SettingsManager(object): :param notmuch_rc: path to notmuch's config file :type notmuch_rc: str """ - assert alot_rc is None or (isinstance(alot_rc, basestring) and + assert alot_rc is None or (isinstance(alot_rc, str) and os.path.exists(alot_rc)) - assert notmuch_rc is None or (isinstance(notmuch_rc, basestring) and + assert notmuch_rc is None or (isinstance(notmuch_rc, str) and os.path.exists(notmuch_rc)) self.hooks = None self._mailcaps = mailcap.getcaps() @@ -176,12 +176,12 @@ class SettingsManager(object): value = section[key] - if isinstance(value, (str, unicode)): + if isinstance(value, str): section[key] = expand_environment_and_home(value) elif isinstance(value, (list, tuple)): new = list() for item in value: - if isinstance(item, (str, unicode)): + if isinstance(item, str): new.append(expand_environment_and_home(item)) else: new.append(item) @@ -395,7 +395,7 @@ class SettingsManager(object): def get_mapped_input_keysequences(self, mode='global', prefix=u''): # get all bindings in this mode globalmaps, modemaps = self.get_keybindings(mode) - candidates = globalmaps.keys() + modemaps.keys() + candidates = list(globalmaps.keys()) + list(modemaps.keys()) if prefix is not None: prefixes = prefix + ' ' cand = [c for c in candidates if c.startswith(prefixes)] @@ -433,7 +433,7 @@ class SettingsManager(object): if value and value != '': globalmaps[key] = value # get rid of empty commands left in mode bindings - for k, v in modemaps.items(): + for k, v in list(modemaps.items()): if not v: del modemaps[k] @@ -504,7 +504,7 @@ class SettingsManager(object): def get_addresses(self): """returns addresses of known accounts including all their aliases""" - return self._accountmap.keys() + return list(self._accountmap.keys()) def get_addressbooks(self, order=None, append_remaining=True): """returns list of all defined :class:`AddressBook` objects""" @@ -529,7 +529,7 @@ class SettingsManager(object): def represent_datetime(self, d): """ - turns a given datetime obj into a unicode string representation. + turns a given datetime obj into a string representation. This will: 1) look if a fixed 'timestamp_format' is given in the config @@ -103,11 +103,12 @@ class UI(object): self._recipients_hist_file, size=size) # set up main loop - self.mainloop = urwid.MainLoop(self.root_widget, - handle_mouse=settings.get('handle_mouse'), - event_loop=urwid.TwistedEventLoop(), - unhandled_input=self._unhandled_input, - input_filter=self._input_filter) + self.mainloop = urwid.MainLoop( + self.root_widget, + handle_mouse=settings.get('handle_mouse'), + event_loop=urwid.TwistedEventLoop(), + unhandled_input=self._unhandled_input, + input_filter=self._input_filter) # Create a defered that calls the loop_hook loop_hook = settings.get_hook('loop_hook') @@ -317,7 +318,7 @@ class UI(object): def cerror(e): logging.error(e) - self.notify('completion error: %s' % e.message, + self.notify('completion error: %s' % str(e), priority='error') self.update() @@ -329,7 +330,7 @@ class UI(object): edit_text=text, history=history, on_error=cerror) - for _ in xrange(tab): # hit some tabs + for _ in range(tab): # hit some tabs editpart.keypress((0,), 'tab') # build promptwidget @@ -529,9 +530,8 @@ class UI(object): :rtype: :class:`twisted.defer.Deferred` """ choices = choices or {'y': 'yes', 'n': 'no'} - choices_to_return = choices_to_return or {} - assert select is None or select in choices.itervalues() - assert cancel is None or cancel in choices.itervalues() + assert select is None or select in choices.values() + assert cancel is None or cancel in choices.values() assert msg_position in ['left', 'above'] d = defer.Deferred() # create return deferred diff --git a/alot/utils/argparse.py b/alot/utils/argparse.py index ff19030c..9822882d 100644 --- a/alot/utils/argparse.py +++ b/alot/utils/argparse.py @@ -52,7 +52,7 @@ def _path_factory(check): @functools.wraps(check) def validator(paths): - if isinstance(paths, basestring): + if isinstance(paths, str): check(paths) elif isinstance(paths, collections.Sequence): for path in paths: diff --git a/alot/utils/configobj.py b/alot/utils/configobj.py index fa9de2ce..78201690 100644 --- a/alot/utils/configobj.py +++ b/alot/utils/configobj.py @@ -5,7 +5,7 @@ from __future__ import absolute_import import mailbox import re -from urlparse import urlparse +from urllib.parse import urlparse from validate import VdtTypeError from validate import is_list diff --git a/alot/widgets/globals.py b/alot/widgets/globals.py index e4253e15..67ff984b 100644 --- a/alot/widgets/globals.py +++ b/alot/widgets/globals.py @@ -54,7 +54,7 @@ class ChoiceWidget(urwid.Text): self.separator = separator items = [] - for k, v in choices.iteritems(): + for k, v in choices.items(): if v == select and select is not None: items += ['[', k, ']:', v] else: @@ -130,7 +130,7 @@ class CompleteEdit(urwid.Edit): self.historypos = None self.focus_in_clist = 0 - if not isinstance(edit_text, unicode): + if not isinstance(edit_text, str): edit_text = string_decode(edit_text) self.start_completion_pos = len(edit_text) self.completions = None diff --git a/docs/source/api/conf.py b/docs/source/api/conf.py index 101fce21..c1673859 100644 --- a/docs/source/api/conf.py +++ b/docs/source/api/conf.py @@ -3,7 +3,8 @@ # alot documentation build configuration file, created by # sphinx-quickstart on Tue Aug 9 15:00:51 2011. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its +# containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -55,8 +56,9 @@ from alot import __version__,__author__ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. @@ -98,7 +100,8 @@ release = __version__ # directories to ignore when looking for source files. exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. +# The reST default role (used for this markup: `text`) to use for all +# documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. @@ -208,7 +211,8 @@ htmlhelp_basename = 'alotdoc' #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, author, documentclass +# [howto/manual]). latex_documents = [ ('index', 'alot.tex', u'alot Documentation', u'Patrick Totzke', 'manual'), @@ -250,7 +254,7 @@ man_pages = [ autodoc_member_order = 'bysource' autoclass_content = 'both' intersphinx_mapping = { - 'python': ('http://docs.python.org/3.2', None), + 'python': ('http://docs.python.org/3.5', None), 'notmuch': ('http://packages.python.org/notmuch', None), 'urwid': ('http://urwid.readthedocs.org/en/latest', None), } diff --git a/docs/source/configuration/accounts_table b/docs/source/configuration/accounts_table index 2215df51..db727898 100644 --- a/docs/source/configuration/accounts_table +++ b/docs/source/configuration/accounts_table @@ -13,24 +13,6 @@ :type: string -.. _realname: - -.. describe:: realname - - used to format the (proposed) From-header in outgoing mails - - :type: string - -.. _aliases: - -.. describe:: aliases - - used to clear your addresses/ match account when formatting replies - - :type: string list - :default: , - - .. _alias-regexp: .. describe:: alias_regexp @@ -41,28 +23,29 @@ :default: None -.. _sendmail-command: +.. _aliases: -.. describe:: sendmail_command +.. describe:: aliases - sendmail command. This is the shell command used to send out mails via the sendmail protocol + used to clear your addresses/ match account when formatting replies - :type: string - :default: "sendmail -t" + :type: string list + :default: , -.. _sent-box: +.. _case-sensitive-username: -.. describe:: sent_box +.. describe:: case_sensitive_username - where to store outgoing mails, e.g. `maildir:///home/you/mail/Sent`. - You can use mbox, maildir, mh, babyl and mmdf in the protocol part of the URL. + Whether the server treats the address as case-senstive or + case-insensitve (True for the former, False for the latter) - .. note:: If you want to add outgoing mails automatically to the notmuch index - you must use maildir in a path within your notmuch database path. + .. note:: The vast majority (if not all) SMTP servers in modern use + treat usernames as case insenstive, you should only set + this if you know that you need it. - :type: mail_container - :default: None + :type: boolean + :default: False .. _draft-box: @@ -80,34 +63,65 @@ :default: None -.. _sent-tags: +.. _draft-tags: -.. describe:: sent_tags +.. describe:: draft_tags - list of tags to automatically add to outgoing messages + list of tags to automatically add to draft messages :type: string list - :default: sent + :default: draft -.. _draft-tags: +.. _encrypt-by-default: -.. describe:: draft_tags +.. describe:: encrypt_by_default - list of tags to automatically add to draft messages + Alot will try to GPG encrypt outgoing messages by default when this + is set to `all` or `trusted`. If set to `all` the message will be + encrypted for all recipients for who a key is available in the key + ring. If set to `trusted` it will be encrypted to all + recipients if a trusted key is available for all recipients (one + where the user id for the key is signed with a trusted signature). - :type: string list - :default: draft + .. note:: If the message will not be encrypted by default you can + still use the :ref:`toggleencrypt + <cmd.envelope.toggleencrypt>`, :ref:`encrypt + <cmd.envelope.encrypt>` and :ref:`unencrypt + <cmd.envelope.unencrypt>` commands to encrypt it. + .. deprecated:: 0.4 + The values `True` and `False` are interpreted as `all` and + `none` respectively. `0`, `1`, `true`, `True`, `false`, + `False`, `yes`, `Yes`, `no`, `No`, will be removed before + 1.0, please move to `all`, `none`, or `trusted`. + :type: option, one of ['all', 'none', 'trusted', 'True', 'False', 'true', 'false', 'Yes', 'No', 'yes', 'no', '1', '0'] + :default: none -.. _replied-tags: -.. describe:: replied_tags +.. _encrypt-to-self: - list of tags to automatically add to replied messages +.. describe:: encrypt_to_self - :type: string list - :default: replied + If this is true when encrypting a message it will also be encrypted + with the key defined for this account. + + .. warning:: + + Before 0.6 this was controlled via gpg.conf. + + :type: boolean + :default: True + + +.. _gpg-key: + +.. describe:: gpg_key + + The GPG key ID you want to use with this account. + + :type: string + :default: None .. _passed-tags: @@ -120,111 +134,97 @@ :default: passed -.. _signature: +.. _realname: -.. describe:: signature +.. describe:: realname - path to signature file that gets attached to all outgoing mails from this account, optionally - renamed to :ref:`signature_filename <signature-filename>`. + used to format the (proposed) From-header in outgoing mails :type: string - :default: None - -.. _signature-as-attachment: +.. _replied-tags: -.. describe:: signature_as_attachment +.. describe:: replied_tags - attach signature file if set to True, append its content (mimetype text) - to the body text if set to False. + list of tags to automatically add to replied messages - :type: boolean - :default: False + :type: string list + :default: replied -.. _signature-filename: +.. _sendmail-command: -.. describe:: signature_filename +.. describe:: sendmail_command - signature file's name as it appears in outgoing mails if - :ref:`signature_as_attachment <signature-as-attachment>` is set to True + sendmail command. This is the shell command used to send out mails via the sendmail protocol :type: string - :default: None - - -.. _sign-by-default: + :default: "sendmail -t" -.. describe:: sign_by_default - Outgoing messages will be GPG signed by default if this is set to True. +.. _sent-box: - :type: boolean - :default: False +.. describe:: sent_box + where to store outgoing mails, e.g. `maildir:///home/you/mail/Sent`. + You can use mbox, maildir, mh, babyl and mmdf in the protocol part of the URL. -.. _encrypt-by-default: + .. note:: If you want to add outgoing mails automatically to the notmuch index + you must use maildir in a path within your notmuch database path. -.. describe:: encrypt_by_default + :type: mail_container + :default: None - Alot will try to GPG encrypt outgoing messages by default when this - is set to `all` or `trusted`. If set to `all` the message will be - encrypted for all recipients for who a key is available in the key - ring. If set to `trusted` it will be encrypted to all - recipients if a trusted key is available for all recipients (one - where the user id for the key is signed with a trusted signature). - .. note:: If the message will not be encrypted by default you can - still use the :ref:`toggleencrypt - <cmd.envelope.toggleencrypt>`, :ref:`encrypt - <cmd.envelope.encrypt>` and :ref:`unencrypt - <cmd.envelope.unencrypt>` commands to encrypt it. - .. deprecated:: 0.4 - The values `True` and `False` are interpreted as `all` and - `none` respectively. `0`, `1`, `true`, `True`, `false`, - `False`, `yes`, `Yes`, `no`, `No`, will be removed before - 1.0, please move to `all`, `none`, or `trusted`. +.. _sent-tags: - :type: option, one of ['all', 'none', 'trusted', 'True', 'False', 'true', 'false', 'Yes', 'No', 'yes', 'no', '1', '0'] - :default: none +.. describe:: sent_tags + list of tags to automatically add to outgoing messages -.. _encrypt-to-self: + :type: string list + :default: sent -.. describe:: encrypt_to_self - If this is true when encrypting a message it will also be encrypted - with the key defined for this account. +.. _sign-by-default: - .. warning:: +.. describe:: sign_by_default - Before 0.6 this was controlled via gpg.conf. + Outgoing messages will be GPG signed by default if this is set to True. :type: boolean - :default: True + :default: False -.. _gpg-key: +.. _signature: -.. describe:: gpg_key +.. describe:: signature - The GPG key ID you want to use with this account. + path to signature file that gets attached to all outgoing mails from this account, optionally + renamed to :ref:`signature_filename <signature-filename>`. :type: string :default: None -.. _case-sensitive-username: - -.. describe:: case_sensitive_username +.. _signature-as-attachment: - Whether the server treats the address as case-senstive or - case-insensitve (True for the former, False for the latter) +.. describe:: signature_as_attachment - .. note:: The vast majority (if not all) SMTP servers in modern use - treat usernames as case insenstive, you should only set - this if you know that you need it. + attach signature file if set to True, append its content (mimetype text) + to the body text if set to False. :type: boolean :default: False + +.. _signature-filename: + +.. describe:: signature_filename + + signature file's name as it appears in outgoing mails if + :ref:`signature_as_attachment <signature-as-attachment>` is set to True + + :type: string + :default: None + diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 05847347..9ad30a8b 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -67,7 +67,9 @@ FAQ .. _faq_7: -7. Why doesn't alot run on python3? +7. I thought alot ran on Python 2? - We're on it. Check out the `py3k milestone <https://github.com/pazz/alot/issues?q=is%3Aopen+is%3Aissue+milestone%3A%22full+py3k+compatibility%22>`_ + It used to. When we made the transition to Python 3 we didn't maintain + Python 2 support. If you still need Python 2 support the 0.7 release is your + best bet. diff --git a/docs/source/generate_commands.py b/docs/source/generate_commands.py index c3db693f..5eda4978 100755 --- a/docs/source/generate_commands.py +++ b/docs/source/generate_commands.py @@ -65,8 +65,8 @@ def rstify_parser(parser): for index, a in enumerate(parser._positionals._group_actions): out += " %s: %s" % (index, a.help) if a.choices: - out += ". valid choices are: %s." % ','.join(['\`%s\`' % s for s - in a.choices]) + out += ". valid choices are: %s." % ','.join( + ['\`%s\`' % s for s in a.choices]) if a.default: out += ". defaults to: '%s'." % a.default out += '\n' @@ -100,7 +100,7 @@ def get_mode_docs(): if __name__ == "__main__": modes = [] - for mode, modecommands in COMMANDS.items(): + for mode, modecommands in sorted(COMMANDS.items()): modefilename = mode+'.rst' modefile = open(os.path.join(HERE, 'usage', 'modes', modefilename), 'w') @@ -115,7 +115,7 @@ if __name__ == "__main__": header = 'Global Commands' modefile.write('%s\n%s\n' % (header, '-' * len(header))) modefile.write('The following commands are available globally\n\n') - for cmdstring, struct in modecommands.items(): + for cmdstring, struct in sorted(modecommands.items()): cls, parser, forced_args = struct labelline = '.. _cmd.%s.%s:\n\n' % (mode, cmdstring.replace('_', '-')) diff --git a/docs/source/generate_configs.py b/docs/source/generate_configs.py index a5427792..88726060 100755 --- a/docs/source/generate_configs.py +++ b/docs/source/generate_configs.py @@ -22,15 +22,13 @@ NOTE = """ """ -def rewrite_entries(config, path, specpath, sec=None, sort=False): +def rewrite_entries(config, path, specpath, sec=None): file = open(path, 'w') file.write(NOTE % specpath) if sec is None: sec = config - if sort: - sec.scalars.sort() - for entry in sec.scalars: + for entry in sorted(sec.scalars): v = Validator() etype, eargs, ekwargs, default = v._parse_check(sec[entry]) if default is not None: @@ -72,7 +70,7 @@ if __name__ == "__main__": alotrc_table_file = os.path.join(HERE, 'configuration', 'alotrc_table') rewrite_entries(config.configspec, alotrc_table_file, - 'defaults/alot.rc.spec', sort=True) + 'defaults/alot.rc.spec') rewrite_entries(config, os.path.join(HERE, 'configuration', 'accounts_table'), diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 8e30de1a..2d3fe44c 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -4,14 +4,14 @@ Installation .. rubric:: dependencies Alot depends on recent versions of notmuch and urwid. Note that due to restrictions -on argparse and subprocess, you need to run *`3.0` > python ≥ `2.7`* (see :ref:`faq <faq_7>`). +on argparse and subprocess, you need to run *`python ≥ `3.5`* (see :ref:`faq <faq_7>`). A full list of dependencies is below: * `libmagic and python bindings <http://darwinsys.com/file/>`_, ≥ `5.04` * `configobj <http://www.voidspace.org.uk/python/configobj.html>`_, ≥ `4.7.0` * `twisted <http://twistedmatrix.com/trac/>`_, ≥ `10.2.0`: * `libnotmuch <http://notmuchmail.org/>`_ and it's python bindings, ≥ `0.13` -* `urwid <http://excess.org/urwid/>`_ toolkit, ≥ `1.1.0` +* `urwid <http://excess.org/urwid/>`_ toolkit, ≥ `1.3.0` * `urwidtrees <https://github.com/pazz/urwidtrees>`_, ≥ `1.0` * `gpg <http://www.gnupg.org/related_software/gpgme>`_ and it's python bindings, ≥ `1.9.0` diff --git a/docs/source/usage/modes/envelope.rst b/docs/source/usage/modes/envelope.rst index 8644a6c1..ee857cc4 100644 --- a/docs/source/usage/modes/envelope.rst +++ b/docs/source/usage/modes/envelope.rst @@ -5,26 +5,25 @@ Commands in `envelope` mode --------------------------- The following commands are available in envelope mode -.. _cmd.envelope.unencrypt: - -.. describe:: unencrypt +.. _cmd.envelope.attach: - remove request to encrypt message before sending +.. describe:: attach + attach files to the mail -.. _cmd.envelope.set: + argument + file(s) to attach (accepts wildcads) -.. describe:: set - set header value +.. _cmd.envelope.edit: - positional arguments - 0: header to refine - 1: value +.. describe:: edit + edit mail optional arguments - :---append: keep previous values. + :---spawn: spawn editor in new terminal. + :---refocus: refocus envelope after editing (Defaults to: 'True'). .. _cmd.envelope.encrypt: @@ -38,32 +37,15 @@ The following commands are available in envelope mode optional arguments :---trusted: only add trusted keys. -.. _cmd.envelope.togglesign: +.. _cmd.envelope.refine: -.. describe:: togglesign +.. describe:: refine - toggle sign status + prompt to change the value of a header argument - which key id to use - - -.. _cmd.envelope.toggleheaders: - -.. describe:: toggleheaders - - toggle display of all headers - - -.. _cmd.envelope.edit: - -.. describe:: edit - - edit mail + header to refine - optional arguments - :---spawn: spawn editor in new terminal. - :---refocus: refocus envelope after editing (Defaults to: 'True'). .. _cmd.envelope.retag: @@ -75,14 +57,21 @@ The following commands are available in envelope mode comma separated list of tags -.. _cmd.envelope.tag: +.. _cmd.envelope.rmencrypt: -.. describe:: tag +.. describe:: rmencrypt - add tags to message + do not encrypt to given recipient key argument - comma separated list of tags + keyid of the key to encrypt with + + +.. _cmd.envelope.save: + +.. describe:: save + + save draft .. _cmd.envelope.send: @@ -92,6 +81,20 @@ The following commands are available in envelope mode send mail +.. _cmd.envelope.set: + +.. describe:: set + + set header value + + positional arguments + 0: header to refine + 1: value + + + optional arguments + :---append: keep previous values. + .. _cmd.envelope.sign: .. describe:: sign @@ -102,99 +105,96 @@ The following commands are available in envelope mode which key id to use -.. _cmd.envelope.untag: +.. _cmd.envelope.tag: -.. describe:: untag +.. describe:: tag - remove tags from message + add tags to message argument comma separated list of tags -.. _cmd.envelope.attach: +.. _cmd.envelope.toggleencrypt: -.. describe:: attach +.. describe:: toggleencrypt - attach files to the mail + toggle if message should be encrypted before sendout argument - file(s) to attach (accepts wildcads) - + keyid of the key to encrypt with -.. _cmd.envelope.unattach: + optional arguments + :---trusted: only add trusted keys. -.. describe:: unattach +.. _cmd.envelope.toggleheaders: - remove attachments from current envelope +.. describe:: toggleheaders - argument - which attached file to remove + toggle display of all headers -.. _cmd.envelope.rmencrypt: +.. _cmd.envelope.togglesign: -.. describe:: rmencrypt +.. describe:: togglesign - do not encrypt to given recipient key + toggle sign status argument - keyid of the key to encrypt with + which key id to use -.. _cmd.envelope.refine: +.. _cmd.envelope.toggletags: -.. describe:: refine +.. describe:: toggletags - prompt to change the value of a header + flip presence of tags on message argument - header to refine + comma separated list of tags -.. _cmd.envelope.toggleencrypt: +.. _cmd.envelope.unattach: -.. describe:: toggleencrypt +.. describe:: unattach - toggle if message should be encrypted before sendout + remove attachments from current envelope argument - keyid of the key to encrypt with + which attached file to remove - optional arguments - :---trusted: only add trusted keys. -.. _cmd.envelope.save: +.. _cmd.envelope.unencrypt: -.. describe:: save +.. describe:: unencrypt - save draft + remove request to encrypt message before sending -.. _cmd.envelope.unsign: +.. _cmd.envelope.unset: -.. describe:: unsign +.. describe:: unset - mark mail not to be signed before sending + remove header field + argument + header to refine -.. _cmd.envelope.toggletags: -.. describe:: toggletags +.. _cmd.envelope.unsign: - flip presence of tags on message +.. describe:: unsign - argument - comma separated list of tags + mark mail not to be signed before sending -.. _cmd.envelope.unset: +.. _cmd.envelope.untag: -.. describe:: unset +.. describe:: untag - remove header field + remove tags from message argument - header to refine + comma separated list of tags diff --git a/docs/source/usage/modes/global.rst b/docs/source/usage/modes/global.rst index dcbe4040..6e79c67a 100644 --- a/docs/source/usage/modes/global.rst +++ b/docs/source/usage/modes/global.rst @@ -15,52 +15,18 @@ The following commands are available globally :---redraw: redraw current buffer after command has finished. :---force: never ask for confirmation. -.. _cmd.global.bprevious: - -.. describe:: bprevious - - focus previous buffer - - -.. _cmd.global.search: - -.. describe:: search - - open a new search buffer. Search obeys the notmuch - :ref:`search.exclude_tags <search.exclude_tags>` setting. - - argument - search string - - optional arguments - :---sort: sort order. Valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. - -.. _cmd.global.repeat: - -.. describe:: repeat - - Repeats the command executed last time - - -.. _cmd.global.prompt: - -.. describe:: prompt - - prompts for commandline and interprets it upon select +.. _cmd.global.bnext: - argument - initial content +.. describe:: bnext + focus next buffer -.. _cmd.global.help: -.. describe:: help +.. _cmd.global.bprevious: - display help for a command. Use 'bindings' to display all keybings - interpreted in current mode.' +.. describe:: bprevious - argument - command or 'bindings' + focus previous buffer .. _cmd.global.buffer: @@ -73,49 +39,21 @@ The following commands are available globally buffer index to focus -.. _cmd.global.move: - -.. describe:: move +.. _cmd.global.bufferlist: - move focus in current buffer +.. describe:: bufferlist - argument - up, down, [half]page up, [half]page down, first, last + open a list of active buffers -.. _cmd.global.shellescape: +.. _cmd.global.call: -.. describe:: shellescape +.. describe:: call - run external command + Executes python code argument - command line to execute - - optional arguments - :---spawn: run in terminal window. - :---thread: run in separate thread. - :---refocus: refocus current buffer after command has finished. - -.. _cmd.global.refresh: - -.. describe:: refresh - - refresh the current buffer - - -.. _cmd.global.reload: - -.. describe:: reload - - Reload all configuration files - - -.. _cmd.global.pyshell: - -.. describe:: pyshell - - open an interactive python shell for introspection + python command string to call .. _cmd.global.compose: @@ -158,29 +96,91 @@ The following commands are available globally flush write operations or retry until committed -.. _cmd.global.bufferlist: +.. _cmd.global.help: -.. describe:: bufferlist +.. describe:: help - open a list of active buffers + display help for a command. Use 'bindings' to display all keybings + interpreted in current mode.' + + argument + command or 'bindings' -.. _cmd.global.call: +.. _cmd.global.move: -.. describe:: call +.. describe:: move - Executes python code + move focus in current buffer argument - python command string to call + up, down, [half]page up, [half]page down, first, last -.. _cmd.global.bnext: +.. _cmd.global.prompt: -.. describe:: bnext +.. describe:: prompt - focus next buffer + prompts for commandline and interprets it upon select + + argument + initial content + + +.. _cmd.global.pyshell: +.. describe:: pyshell + + open an interactive python shell for introspection + + +.. _cmd.global.refresh: + +.. describe:: refresh + + refresh the current buffer + + +.. _cmd.global.reload: + +.. describe:: reload + + Reload all configuration files + + +.. _cmd.global.repeat: + +.. describe:: repeat + + Repeats the command executed last time + + +.. _cmd.global.search: + +.. describe:: search + + open a new search buffer. Search obeys the notmuch + :ref:`search.exclude_tags <search.exclude_tags>` setting. + + argument + search string + + optional arguments + :---sort: sort order. Valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. + +.. _cmd.global.shellescape: + +.. describe:: shellescape + + run external command + + argument + command line to execute + + optional arguments + :---spawn: run in terminal window. + :---thread: run in separate thread. + :---refocus: refocus current buffer after command has finished. .. _cmd.global.taglist: diff --git a/docs/source/usage/modes/search.rst b/docs/source/usage/modes/search.rst index 93a59eff..c95d1a01 100644 --- a/docs/source/usage/modes/search.rst +++ b/docs/source/usage/modes/search.rst @@ -5,37 +5,33 @@ Commands in `search` mode ------------------------- The following commands are available in search mode -.. _cmd.search.sort: +.. _cmd.search.move: -.. describe:: sort +.. describe:: move - set sort order + move focus in search buffer argument - sort order. valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. + last -.. _cmd.search.untag: +.. _cmd.search.refine: -.. describe:: untag +.. describe:: refine - remove tags from all messages in the thread that match the query + refine query argument - comma separated list of tags + search string optional arguments - :---no-flush: postpone a writeout to the index (Defaults to: 'True'). - :---all: retag all messages in search result. - -.. _cmd.search.move: + :---sort: sort order. Valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. -.. describe:: move +.. _cmd.search.refineprompt: - move focus in search buffer +.. describe:: refineprompt - argument - last + prompt to change this buffers querystring .. _cmd.search.retag: @@ -51,44 +47,42 @@ The following commands are available in search mode :---no-flush: postpone a writeout to the index (Defaults to: 'True'). :---all: retag all messages in search result. -.. _cmd.search.refineprompt: - -.. describe:: refineprompt +.. _cmd.search.retagprompt: - prompt to change this buffers querystring +.. describe:: retagprompt + prompt to retag selected threads' tags -.. _cmd.search.tag: -.. describe:: tag +.. _cmd.search.select: - add tags to all messages in the thread that match the current query +.. describe:: select - argument - comma separated list of tags + open thread in a new buffer - optional arguments - :---no-flush: postpone a writeout to the index (Defaults to: 'True'). - :---all: retag all messages in search result. -.. _cmd.search.refine: +.. _cmd.search.sort: -.. describe:: refine +.. describe:: sort - refine query + set sort order argument - search string + sort order. valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. - optional arguments - :---sort: sort order. Valid choices are: \`oldest_first\`,\`newest_first\`,\`message_id\`,\`unsorted\`. -.. _cmd.search.retagprompt: +.. _cmd.search.tag: -.. describe:: retagprompt +.. describe:: tag - prompt to retag selected threads' tags + add tags to all messages in the thread that match the current query + + argument + comma separated list of tags + optional arguments + :---no-flush: postpone a writeout to the index (Defaults to: 'True'). + :---all: retag all messages in search result. .. _cmd.search.toggletags: @@ -102,10 +96,16 @@ The following commands are available in search mode optional arguments :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.search.select: +.. _cmd.search.untag: -.. describe:: select +.. describe:: untag - open thread in a new buffer + remove tags from all messages in the thread that match the query + + argument + comma separated list of tags + optional arguments + :---no-flush: postpone a writeout to the index (Defaults to: 'True'). + :---all: retag all messages in search result. diff --git a/docs/source/usage/modes/thread.rst b/docs/source/usage/modes/thread.rst index 29a7850a..447e543a 100644 --- a/docs/source/usage/modes/thread.rst +++ b/docs/source/usage/modes/thread.rst @@ -5,24 +5,12 @@ Commands in `thread` mode ------------------------- The following commands are available in thread mode -.. _cmd.thread.pipeto: - -.. describe:: pipeto +.. _cmd.thread.bounce: - pipe message(s) to stdin of a shellcommand +.. describe:: bounce - argument - shellcommand to pipe to + directly re-send selected message - optional arguments - :---all: pass all messages. - :---format: output format. Valid choices are: \`raw\`,\`decoded\`,\`id\`,\`filepath\` (Defaults to: 'raw'). - :---separately: call command once for each message. - :---background: don't stop the interface. - :---add_tags: add 'Tags' header to the message. - :---shell: let the shell interpret the command. - :---notify_stdout: display cmd's stdout as notification. - :---field_key: mailcap field key for decoding (Defaults to: 'copiousoutput'). .. _cmd.thread.editnew: @@ -33,15 +21,25 @@ The following commands are available in thread mode optional arguments :---spawn: open editor in new window. -.. _cmd.thread.move: +.. _cmd.thread.fold: -.. describe:: move +.. describe:: fold - move focus in current buffer + fold message(s) argument - up, down, [half]page up, [half]page down, first, last, parent, first reply, last reply, next sibling, previous sibling, next, previous, next unfolded, previous unfolded, next NOTMUCH_QUERY, previous NOTMUCH_QUERY + query used to filter messages to affect + +.. _cmd.thread.forward: + +.. describe:: forward + + forward message + + optional arguments + :---attach: attach original mail. + :---spawn: open editor in new window. .. _cmd.thread.indent: @@ -53,28 +51,34 @@ The following commands are available in thread mode None -.. _cmd.thread.toggleheaders: +.. _cmd.thread.move: -.. describe:: toggleheaders +.. describe:: move - display all headers + move focus in current buffer argument - query used to filter messages to affect + up, down, [half]page up, [half]page down, first, last, parent, first reply, last reply, next sibling, previous sibling, next, previous, next unfolded, previous unfolded, next NOTMUCH_QUERY, previous NOTMUCH_QUERY -.. _cmd.thread.retag: +.. _cmd.thread.pipeto: -.. describe:: retag +.. describe:: pipeto - set message(s) tags. + pipe message(s) to stdin of a shellcommand argument - comma separated list of tags + shellcommand to pipe to optional arguments - :---all: tag all messages in thread. - :---no-flush: postpone a writeout to the index (Defaults to: 'True'). + :---all: pass all messages. + :---format: output format. Valid choices are: \`raw\`,\`decoded\`,\`id\`,\`filepath\` (Defaults to: 'raw'). + :---separately: call command once for each message. + :---background: don't stop the interface. + :---add_tags: add 'Tags' header to the message. + :---shell: let the shell interpret the command. + :---notify_stdout: display cmd's stdout as notification. + :---field_key: mailcap field key for decoding (Defaults to: 'copiousoutput'). .. _cmd.thread.print: @@ -88,28 +92,31 @@ The following commands are available in thread mode :---separately: call print command once for each message. :---add_tags: add 'Tags' header to the message. -.. _cmd.thread.bounce: - -.. describe:: bounce +.. _cmd.thread.remove: - directly re-send selected message +.. describe:: remove + remove message(s) from the index -.. _cmd.thread.togglesource: + optional arguments + :---all: remove whole thread. -.. describe:: togglesource +.. _cmd.thread.reply: - display message source +.. describe:: reply - argument - query used to filter messages to affect + reply to message + optional arguments + :---all: reply to all. + :---list: reply to list. + :---spawn: open editor in new window. -.. _cmd.thread.untag: +.. _cmd.thread.retag: -.. describe:: untag +.. describe:: retag - remove tags from message(s) + set message(s) tags. argument comma separated list of tags @@ -118,14 +125,25 @@ The following commands are available in thread mode :---all: tag all messages in thread. :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.thread.fold: +.. _cmd.thread.save: -.. describe:: fold +.. describe:: save - fold message(s) + save attachment(s) argument - query used to filter messages to affect + path to save to + + optional arguments + :---all: save all attachments. + +.. _cmd.thread.select: + +.. describe:: select + + select focussed element. The fired action depends on the focus: + - if message summary, this toggles visibility of the message, + - if attachment line, this opens the attachment .. _cmd.thread.tag: @@ -141,63 +159,54 @@ The following commands are available in thread mode :---all: tag all messages in thread. :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.thread.remove: +.. _cmd.thread.toggleheaders: -.. describe:: remove +.. describe:: toggleheaders - remove message(s) from the index + display all headers - optional arguments - :---all: remove whole thread. + argument + query used to filter messages to affect -.. _cmd.thread.unfold: -.. describe:: unfold +.. _cmd.thread.togglesource: - unfold message(s) +.. describe:: togglesource + + display message source argument query used to filter messages to affect -.. _cmd.thread.forward: - -.. describe:: forward - - forward message - - optional arguments - :---attach: attach original mail. - :---spawn: open editor in new window. +.. _cmd.thread.toggletags: -.. _cmd.thread.reply: +.. describe:: toggletags -.. describe:: reply + flip presence of tags on message(s) - reply to message + argument + comma separated list of tags optional arguments - :---all: reply to all. - :---list: reply to list. - :---spawn: open editor in new window. + :---all: tag all messages in thread. + :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.thread.save: +.. _cmd.thread.unfold: -.. describe:: save +.. describe:: unfold - save attachment(s) + unfold message(s) argument - path to save to + query used to filter messages to affect - optional arguments - :---all: save all attachments. -.. _cmd.thread.toggletags: +.. _cmd.thread.untag: -.. describe:: toggletags +.. describe:: untag - flip presence of tags on message(s) + remove tags from message(s) argument comma separated list of tags @@ -206,12 +215,3 @@ The following commands are available in thread mode :---all: tag all messages in thread. :---no-flush: postpone a writeout to the index (Defaults to: 'True'). -.. _cmd.thread.select: - -.. describe:: select - - select focussed element. The fired action depends on the focus: - - if message summary, this toggles visibility of the message, - - if attachment line, this opens the attachment - - diff --git a/extra/colour_picker.py b/extra/colour_picker.py index 8c28a2ee..2174d997 100755 --- a/extra/colour_picker.py +++ b/extra/colour_picker.py @@ -1,8 +1,9 @@ #!/usr/bin/python # # COLOUR PICKER. -# This is a lightly modified version of urwids palette_test.py example script as -# found at https://raw.github.com/wardi/urwid/master/examples/palette_test.py +# This is a lightly modified version of urwids palette_test.py example +# script as found at +# https://raw.github.com/wardi/urwid/master/examples/palette_test.py # # This version simply omits resetting the screens default colour palette, # and therefore displays the colour attributes as alot would render them in @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 from setuptools import setup, find_packages import alot @@ -17,10 +17,13 @@ setup( 'Environment :: Console :: Curses', 'Framework :: Twisted', 'Intended Audience :: End Users/Desktop', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + ( + 'License :: OSI Approved' + ':: GNU General Public License v3 or later (GPLv3+)'), 'Operating System :: POSIX', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 2 :: Only', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3 :: Only', 'Topic :: Communications :: Email :: Email Clients (MUA)', 'Topic :: Database :: Front-Ends', ], @@ -47,12 +50,13 @@ setup( 'twisted>=10.2.0', 'python-magic', 'configobj>=4.7.0', - 'gpg' + 'gpg', + 'chardet', ], tests_require=[ 'mock', ], provides=['alot'], test_suite="tests", - python_requires=">=2.7", + python_requires=">=3.5", ) diff --git a/tests/account_test.py b/tests/account_test.py index cfc51bf8..99df9632 100644 --- a/tests/account_test.py +++ b/tests/account_test.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from __future__ import absolute_import + import unittest from alot import account @@ -32,138 +32,126 @@ class TestAccount(unittest.TestCase): def test_get_address(self): """Tests address without aliases.""" - acct = _AccountTestClass(address=u"foo@example.com") - self.assertListEqual(acct.get_addresses(), [u'foo@example.com']) + acct = _AccountTestClass(address="foo@example.com") + self.assertListEqual(acct.get_addresses(), ['foo@example.com']) def test_get_address_with_aliases(self): """Tests address with aliases.""" - acct = _AccountTestClass(address=u"foo@example.com", - aliases=[u'bar@example.com']) + acct = _AccountTestClass(address="foo@example.com", + aliases=['bar@example.com']) self.assertListEqual(acct.get_addresses(), - [u'foo@example.com', u'bar@example.com']) + ['foo@example.com', 'bar@example.com']) def test_deprecated_encrypt_by_default(self): """Tests that depreacted values are still accepted.""" - for each in [u'true', u'yes', u'1']: - acct = _AccountTestClass(address=u'foo@example.com', + for each in ['true', 'yes', '1']: + acct = _AccountTestClass(address='foo@example.com', encrypt_by_default=each) - self.assertEqual(acct.encrypt_by_default, u'all') - for each in [u'false', u'no', u'0']: - acct = _AccountTestClass(address=u'foo@example.com', + self.assertEqual(acct.encrypt_by_default, 'all') + for each in ['false', 'no', '0']: + acct = _AccountTestClass(address='foo@example.com', encrypt_by_default=each) - self.assertEqual(acct.encrypt_by_default, u'none') + self.assertEqual(acct.encrypt_by_default, 'none') class TestAddress(unittest.TestCase): """Tests for the Address class.""" - def test_constructor_bytes(self): - with self.assertRaises(AssertionError): - account.Address(b'username', b'domainname') - - def test_from_string_bytes(self): - with self.assertRaises(AssertionError): - account.Address.from_string(b'user@example.com') - def test_from_string(self): - addr = account.Address.from_string(u'user@example.com') - self.assertEqual(addr.username, u'user') - self.assertEqual(addr.domainname, u'example.com') - - def test_unicode(self): - addr = account.Address(u'ušer', u'example.com') - self.assertEqual(unicode(addr), u'ušer@example.com') + addr = account.Address.from_string('user@example.com') + self.assertEqual(addr.username, 'user') + self.assertEqual(addr.domainname, 'example.com') def test_str(self): - addr = account.Address(u'ušer', u'example.com') - self.assertEqual(str(addr), u'ušer@example.com'.encode('utf-8')) + addr = account.Address('ušer', 'example.com') + self.assertEqual(str(addr), 'ušer@example.com') def test_eq_unicode(self): - addr = account.Address(u'ušer', u'example.com') - self.assertEqual(addr, u'ušer@example.com') + addr = account.Address('ušer', 'example.com') + self.assertEqual(addr, 'ušer@example.com') def test_eq_address(self): - addr = account.Address(u'ušer', u'example.com') - addr2 = account.Address(u'ušer', u'example.com') + addr = account.Address('ušer', 'example.com') + addr2 = account.Address('ušer', 'example.com') self.assertEqual(addr, addr2) def test_ne_unicode(self): - addr = account.Address(u'ušer', u'example.com') - self.assertNotEqual(addr, u'user@example.com') + addr = account.Address('ušer', 'example.com') + self.assertNotEqual(addr, 'user@example.com') def test_ne_address(self): - addr = account.Address(u'ušer', u'example.com') - addr2 = account.Address(u'user', u'example.com') + addr = account.Address('ušer', 'example.com') + addr2 = account.Address('user', 'example.com') self.assertNotEqual(addr, addr2) def test_eq_unicode_case(self): - addr = account.Address(u'UŠer', u'example.com') - self.assertEqual(addr, u'ušer@example.com') + addr = account.Address('UŠer', 'example.com') + self.assertEqual(addr, 'ušer@example.com') def test_ne_unicode_case(self): - addr = account.Address(u'ušer', u'example.com') - self.assertEqual(addr, u'uŠer@example.com') + addr = account.Address('ušer', 'example.com') + self.assertEqual(addr, 'uŠer@example.com') def test_ne_address_case(self): - addr = account.Address(u'ušer', u'example.com') - addr2 = account.Address(u'uŠer', u'example.com') + addr = account.Address('ušer', 'example.com') + addr2 = account.Address('uŠer', 'example.com') self.assertEqual(addr, addr2) def test_eq_address_case(self): - addr = account.Address(u'UŠer', u'example.com') - addr2 = account.Address(u'ušer', u'example.com') + addr = account.Address('UŠer', 'example.com') + addr2 = account.Address('ušer', 'example.com') self.assertEqual(addr, addr2) def test_eq_unicode_case_sensitive(self): - addr = account.Address(u'UŠer', u'example.com', case_sensitive=True) - self.assertNotEqual(addr, u'ušer@example.com') + addr = account.Address('UŠer', 'example.com', case_sensitive=True) + self.assertNotEqual(addr, 'ušer@example.com') def test_eq_address_case_sensitive(self): - addr = account.Address(u'UŠer', u'example.com', case_sensitive=True) - addr2 = account.Address(u'ušer', u'example.com') + addr = account.Address('UŠer', 'example.com', case_sensitive=True) + addr2 = account.Address('ušer', 'example.com') self.assertNotEqual(addr, addr2) def test_eq_str(self): - addr = account.Address(u'user', u'example.com', case_sensitive=True) + addr = account.Address('user', 'example.com', case_sensitive=True) with self.assertRaises(TypeError): addr == 1 # pylint: disable=pointless-statement def test_ne_str(self): - addr = account.Address(u'user', u'example.com', case_sensitive=True) + addr = account.Address('user', 'example.com', case_sensitive=True) with self.assertRaises(TypeError): addr != 1 # pylint: disable=pointless-statement def test_repr(self): - addr = account.Address(u'user', u'example.com', case_sensitive=True) + addr = account.Address('user', 'example.com', case_sensitive=True) self.assertEqual( repr(addr), - "Address(u'user', u'example.com', case_sensitive=True)") + "Address('user', 'example.com', case_sensitive=True)") def test_domain_name_ne(self): - addr = account.Address(u'user', u'example.com') - self.assertNotEqual(addr, u'user@example.org') + addr = account.Address('user', 'example.com') + self.assertNotEqual(addr, 'user@example.org') def test_domain_name_eq_case(self): - addr = account.Address(u'user', u'example.com') - self.assertEqual(addr, u'user@Example.com') + addr = account.Address('user', 'example.com') + self.assertEqual(addr, 'user@Example.com') def test_domain_name_ne_unicode(self): - addr = account.Address(u'user', u'éxample.com') - self.assertNotEqual(addr, u'user@example.com') + addr = account.Address('user', 'éxample.com') + self.assertNotEqual(addr, 'user@example.com') def test_domain_name_eq_unicode(self): - addr = account.Address(u'user', u'éxample.com') - self.assertEqual(addr, u'user@Éxample.com') + addr = account.Address('user', 'éxample.com') + self.assertEqual(addr, 'user@Éxample.com') def test_domain_name_eq_case_sensitive(self): - addr = account.Address(u'user', u'example.com', case_sensitive=True) - self.assertEqual(addr, u'user@Example.com') + addr = account.Address('user', 'example.com', case_sensitive=True) + self.assertEqual(addr, 'user@Example.com') def test_domain_name_eq_unicode_sensitive(self): - addr = account.Address(u'user', u'éxample.com', case_sensitive=True) - self.assertEqual(addr, u'user@Éxample.com') + addr = account.Address('user', 'éxample.com', case_sensitive=True) + self.assertEqual(addr, 'user@Éxample.com') def test_cmp_empty(self): - addr = account.Address(u'user', u'éxample.com') - self.assertNotEqual(addr, u'') + addr = account.Address('user', 'éxample.com') + self.assertNotEqual(addr, '') diff --git a/tests/addressbook/abook_test.py b/tests/addressbook/abook_test.py index 32be2cbe..c5686caf 100644 --- a/tests/addressbook/abook_test.py +++ b/tests/addressbook/abook_test.py @@ -29,7 +29,7 @@ class TestAbookAddressBook(unittest.TestCase): name = you email = you@other.domain, you@example.com """ - with tempfile.NamedTemporaryFile(delete=False) as tmp: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp: tmp.write(data) path = tmp.name self.addCleanup(os.unlink, path) diff --git a/tests/commands/envelope_test.py b/tests/commands/envelope_test.py index c48a0d91..ee1c0acc 100644 --- a/tests/commands/envelope_test.py +++ b/tests/commands/envelope_test.py @@ -315,7 +315,7 @@ class TestSignCommand(unittest.TestCase): """) # Allow settings.reload to work by not deleting the file until the end - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(config) self.addCleanup(os.unlink, f.name) diff --git a/tests/commands/global_test.py b/tests/commands/global_test.py index e3d91f8a..850c9447 100644 --- a/tests/commands/global_test.py +++ b/tests/commands/global_test.py @@ -48,7 +48,8 @@ class TestComposeCommand(unittest.TestCase): return envelope @staticmethod - def _make_account_mock(sign_by_default=True, gpg_key=mock.sentinel.gpg_key): + def _make_account_mock( + sign_by_default=True, gpg_key=mock.sentinel.gpg_key): account = mock.Mock() account.sign_by_default = sign_by_default account.gpg_key = gpg_key @@ -136,8 +137,9 @@ class TestComposeCommand(unittest.TestCase): cmd = g_commands.ComposeCommand(template=f.name) # Crutch to exit the giant `apply` method early. - with mock.patch('alot.commands.globals.settings.get_account_by_address', - mock.Mock(side_effect=Stop)): + with mock.patch( + 'alot.commands.globals.settings.get_account_by_address', + mock.Mock(side_effect=Stop)): try: yield cmd.apply(mock.Mock()) except Stop: @@ -172,7 +174,8 @@ class TestExternalCommand(unittest.TestCase): def test_no_spawn_stdin_attached(self): ui = utilities.make_ui() - cmd = g_commands.ExternalCommand(u"test -t 0", stdin=u'0', refocus=False) + cmd = g_commands.ExternalCommand( + u"test -t 0", stdin=u'0', refocus=False) cmd.apply(ui) ui.notify.assert_called_once_with('', priority='error') @@ -182,7 +185,8 @@ class TestExternalCommand(unittest.TestCase): cmd.apply(ui) ui.notify.assert_called_once_with('', priority='error') - @mock.patch('alot.commands.globals.settings.get', mock.Mock(return_value='')) + @mock.patch( + 'alot.commands.globals.settings.get', mock.Mock(return_value='')) @mock.patch.dict(os.environ, {'DISPLAY': ':0'}) def test_spawn_no_stdin_success(self): ui = utilities.make_ui() @@ -190,7 +194,8 @@ class TestExternalCommand(unittest.TestCase): cmd.apply(ui) ui.notify.assert_not_called() - @mock.patch('alot.commands.globals.settings.get', mock.Mock(return_value='')) + @mock.patch( + 'alot.commands.globals.settings.get', mock.Mock(return_value='')) @mock.patch.dict(os.environ, {'DISPLAY': ':0'}) def test_spawn_stdin_success(self): ui = utilities.make_ui() @@ -200,7 +205,8 @@ class TestExternalCommand(unittest.TestCase): cmd.apply(ui) ui.notify.assert_not_called() - @mock.patch('alot.commands.globals.settings.get', mock.Mock(return_value='')) + @mock.patch( + 'alot.commands.globals.settings.get', mock.Mock(return_value='')) @mock.patch.dict(os.environ, {'DISPLAY': ':0'}) def test_spawn_failure(self): ui = utilities.make_ui() diff --git a/tests/commands/thread_test.py b/tests/commands/thread_test.py index 6897a953..81328410 100644 --- a/tests/commands/thread_test.py +++ b/tests/commands/thread_test.py @@ -163,7 +163,7 @@ class TestDetermineSender(unittest.TestCase): expected = (u'to@example.com', account2) self._test(accounts=[account1, account2, account3], expected=expected) - def test_force_realname_includes_real_name_in_returned_address_if_defined(self): + def test_force_realname_has_real_name_in_returned_address_if_defined(self): account1 = _AccountTestClass(address=u'foo@example.com') account2 = _AccountTestClass(address=u'to@example.com', realname='Bar') account3 = _AccountTestClass(address=u'baz@example.com') @@ -179,7 +179,7 @@ class TestDetermineSender(unittest.TestCase): self._test(accounts=[account1, account2, account3], expected=expected, force_realname=True) - def test_with_force_address_main_address_is_used_regardless_of_matching_address(self): + def test_with_force_address_main_address_is_always_used(self): # In python 3.4 this and the next test could be written as subtests. account1 = _AccountTestClass(address=u'foo@example.com') account2 = _AccountTestClass(address=u'bar@example.com', diff --git a/tests/commands/utils_tests.py b/tests/commands/utils_tests.py index 5320aadd..c17473dc 100644 --- a/tests/commands/utils_tests.py +++ b/tests/commands/utils_tests.py @@ -57,7 +57,8 @@ def setUpModule(): with gpg.core.Context(armor=True) as ctx: # Add the public and private keys. They have no password - search_dir = os.path.join(os.path.dirname(__file__), '../static/gpg-keys') + search_dir = os.path.join( + os.path.dirname(__file__), '../static/gpg-keys') for each in os.listdir(search_dir): if os.path.splitext(each)[1] == '.gpg': with open(os.path.join(search_dir, each)) as f: @@ -106,7 +107,8 @@ class TestGetKeys(unittest.TestCase): @inlineCallbacks def test_get_keys_ambiguous(self): """Test gettings keys when when the key is ambiguous.""" - key = crypto.get_key(FPR, validate=True, encrypt=True, signed_only=False) + key = crypto.get_key( + FPR, validate=True, encrypt=True, signed_only=False) ui = utilities.make_ui() # Creat a ui.choice object that can satisfy twisted, but can also be @@ -141,8 +143,8 @@ class TestSetEncrypt(unittest.TestCase): envelope['To'] = 'ambig@example.com, test@example.com' yield utils.set_encrypt(ui, envelope) self.assertTrue(envelope.encrypt) - self.assertEqual( - [f.fpr for f in envelope.encrypt_keys.itervalues()], + self.assertCountEqual( + [f.fpr for f in envelope.encrypt_keys.values()], [crypto.get_key(FPR).fpr, crypto.get_key(EXTRA_FPRS[0]).fpr]) @inlineCallbacks @@ -152,8 +154,8 @@ class TestSetEncrypt(unittest.TestCase): envelope['Cc'] = 'ambig@example.com, test@example.com' yield utils.set_encrypt(ui, envelope) self.assertTrue(envelope.encrypt) - self.assertEqual( - [f.fpr for f in envelope.encrypt_keys.itervalues()], + self.assertCountEqual( + [f.fpr for f in envelope.encrypt_keys.values()], [crypto.get_key(FPR).fpr, crypto.get_key(EXTRA_FPRS[0]).fpr]) @inlineCallbacks @@ -163,8 +165,8 @@ class TestSetEncrypt(unittest.TestCase): envelope['Cc'] = 'foo@example.com, test@example.com' yield utils.set_encrypt(ui, envelope) self.assertTrue(envelope.encrypt) - self.assertEqual( - [f.fpr for f in envelope.encrypt_keys.itervalues()], + self.assertCountEqual( + [f.fpr for f in envelope.encrypt_keys.values()], [crypto.get_key(FPR).fpr]) @inlineCallbacks diff --git a/tests/completion_test.py b/tests/completion_test.py index 5b335779..15a80a89 100644 --- a/tests/completion_test.py +++ b/tests/completion_test.py @@ -57,7 +57,8 @@ class AbooksCompleterTest(unittest.TestCase): self.assertTupleEqual(actual[0], expected[0]) def test_empty_real_name_returns_plain_email_address(self): - actual = self.__class__.example_abook_completer.complete("real-name", 9) + actual = self.__class__.example_abook_completer.complete( + "real-name", 9) expected = [("no-real-name@example.com", 24)] self._assert_only_one_list_entry(actual, expected) @@ -79,7 +80,8 @@ class AbooksCompleterTest(unittest.TestCase): def test_real_name_double_quotes(self): actual = self.__class__.example_abook_completer.complete("dquote", 6) expected = [("", 0)] - expected = [(r""""double \"quote\" person" <dquote@example.com>""", 46)] + expected = [ + (r""""double \"quote\" person" <dquote@example.com>""", 46)] self._assert_only_one_list_entry(actual, expected) def test_real_name_with_quotes_and_comma(self): diff --git a/tests/crypto_test.py b/tests/crypto_test.py index d481d64e..c3db1055 100644 --- a/tests/crypto_test.py +++ b/tests/crypto_test.py @@ -12,6 +12,7 @@ import unittest import gpg import mock +import urwid from alot import crypto from alot.errors import GPGProblem, GPGCode @@ -57,7 +58,9 @@ def tearDownModule(): # Kill any gpg-agent's that have been opened lookfor = 'gpg-agent --homedir {}'.format(os.environ['GNUPGHOME']) - out = subprocess.check_output(['ps', 'xo', 'pid,cmd'], stderr=DEVNULL) + out = subprocess.check_output( + ['ps', 'xo', 'pid,cmd'], + stderr=DEVNULL).decode(urwid.util.detected_encoding) for each in out.strip().split('\n'): pid, cmd = each.strip().split(' ', 1) if cmd.startswith(lookfor): @@ -109,9 +112,10 @@ class TestHashAlgorithmHelper(unittest.TestCase): class TestDetachedSignatureFor(unittest.TestCase): def test_valid_signature_generated(self): - to_sign = "this is some text.\nit is more than nothing.\n" + to_sign = b"this is some text.\nit is more than nothing.\n" with gpg.core.Context() as ctx: - _, detached = crypto.detached_signature_for(to_sign, [ctx.get_key(FPR)]) + _, detached = crypto.detached_signature_for( + to_sign, [ctx.get_key(FPR)]) with tempfile.NamedTemporaryFile(delete=False) as f: f.write(detached) @@ -131,9 +135,10 @@ class TestDetachedSignatureFor(unittest.TestCase): class TestVerifyDetached(unittest.TestCase): def test_verify_signature_good(self): - to_sign = "this is some text.\nIt's something\n." + to_sign = b"this is some text.\nIt's something\n." with gpg.core.Context() as ctx: - _, detached = crypto.detached_signature_for(to_sign, [ctx.get_key(FPR)]) + _, detached = crypto.detached_signature_for( + to_sign, [ctx.get_key(FPR)]) try: crypto.verify_detached(to_sign, detached) @@ -141,10 +146,11 @@ class TestVerifyDetached(unittest.TestCase): raise AssertionError def test_verify_signature_bad(self): - to_sign = "this is some text.\nIt's something\n." - similar = "this is some text.\r\n.It's something\r\n." + to_sign = b"this is some text.\nIt's something\n." + similar = b"this is some text.\r\n.It's something\r\n." with gpg.core.Context() as ctx: - _, detached = crypto.detached_signature_for(to_sign, [ctx.get_key(FPR)]) + _, detached = crypto.detached_signature_for( + to_sign, [ctx.get_key(FPR)]) with self.assertRaises(GPGProblem): crypto.verify_detached(similar, detached) @@ -178,7 +184,8 @@ class TestValidateKey(unittest.TestCase): def test_encrypt(self): with self.assertRaises(GPGProblem) as caught: - crypto.validate_key(utilities.make_key(can_encrypt=False), encrypt=True) + crypto.validate_key( + utilities.make_key(can_encrypt=False), encrypt=True) self.assertEqual(caught.exception.code, GPGCode.KEY_CANNOT_ENCRYPT) @@ -284,7 +291,8 @@ class TestGetKey(unittest.TestCase): # once. with gpg.core.Context() as ctx: expected = ctx.get_key(FPR).uids[0].uid - actual = crypto.get_key(FPR, validate=True, encrypt=True, sign=True).uids[0].uid + actual = crypto.get_key( + FPR, validate=True, encrypt=True, sign=True).uids[0].uid self.assertEqual(expected, actual) def test_missing_key(self): @@ -304,7 +312,8 @@ class TestGetKey(unittest.TestCase): except GPGProblem as e: raise AssertionError(e) - @mock.patch('alot.crypto.check_uid_validity', mock.Mock(return_value=False)) + @mock.patch( + 'alot.crypto.check_uid_validity', mock.Mock(return_value=False)) def test_signed_only_false(self): with self.assertRaises(GPGProblem) as e: crypto.get_key(FPR, signed_only=True) @@ -360,7 +369,7 @@ class TestGetKey(unittest.TestCase): class TestEncrypt(unittest.TestCase): def test_encrypt(self): - to_encrypt = "this is a string\nof data." + to_encrypt = b"this is a string\nof data." encrypted = crypto.encrypt(to_encrypt, keys=[crypto.get_key(FPR)]) with tempfile.NamedTemporaryFile(delete=False) as f: @@ -368,15 +377,15 @@ class TestEncrypt(unittest.TestCase): enc_file = f.name self.addCleanup(os.unlink, enc_file) - dec = subprocess.check_output(['gpg', '--decrypt', enc_file], - stderr=DEVNULL) + dec = subprocess.check_output( + ['gpg', '--decrypt', enc_file], stderr=DEVNULL) self.assertEqual(to_encrypt, dec) class TestDecrypt(unittest.TestCase): def test_decrypt(self): - to_encrypt = "this is a string\nof data." + to_encrypt = b"this is a string\nof data." encrypted = crypto.encrypt(to_encrypt, keys=[crypto.get_key(FPR)]) _, dec = crypto.decrypt_verify(encrypted) self.assertEqual(to_encrypt, dec) diff --git a/tests/db/thread_test.py b/tests/db/thread_test.py index 678a5e59..8e4a5003 100644 --- a/tests/db/thread_test.py +++ b/tests/db/thread_test.py @@ -45,7 +45,7 @@ class TestThreadGetAuthor(unittest.TestCase): m.get_author = mock.Mock(return_value=a) get_messages.append(m) gm = mock.Mock() - gm.iterkeys = mock.Mock(return_value=get_messages) + gm.keys = mock.Mock(return_value=get_messages) cls.__patchers.extend([ mock.patch('alot.db.thread.Thread.get_messages', diff --git a/tests/db/utils_test.py b/tests/db/utils_test.py index 3e7ef9d3..484562fc 100644 --- a/tests/db/utils_test.py +++ b/tests/db/utils_test.py @@ -6,6 +6,7 @@ from __future__ import absolute_import import base64 +import codecs import email import email.header import email.mime.application @@ -171,6 +172,7 @@ class TestEncodeHeader(unittest.TestCase): expected = email.header.Header('value') self.assertEqual(actual, expected) + @unittest.expectedFailure def test_unicode_chars_are_encoded(self): actual = utils.encode_header('x-key', u'välüe') expected = email.header.Header('=?utf-8?b?dsOkbMO8ZQ==?=') @@ -243,10 +245,10 @@ class TestDecodeHeader(unittest.TestCase): :rtype: str """ string = unicode_string.encode(encoding) - output = '=?' + encoding + '?Q?' + output = b'=?' + encoding.encode('ascii') + b'?Q?' for byte in string: - output += '=' + byte.encode('hex').upper() - return output + '?=' + output += b'=' + codecs.encode(bytes([byte]), 'hex').upper() + return (output + b'?=').decode('ascii') @staticmethod def _base64(unicode_string, encoding): @@ -260,8 +262,10 @@ class TestDecodeHeader(unittest.TestCase): :rtype: str """ string = unicode_string.encode(encoding) - b64 = base64.encodestring(string).strip() - return '=?' + encoding + '?B?' + b64 + '?=' + b64 = base64.encodebytes(string).strip() + result_bytes = b'=?' + encoding.encode('utf-8') + b'?B?' + b64 + b'?=' + result = result_bytes.decode('ascii') + return result def _test(self, teststring, expected): @@ -315,7 +319,11 @@ class TestDecodeHeader(unittest.TestCase): ' again: ' + self._quote(part, 'utf-8') + \ ' latin1: ' + self._base64(part, 'iso-8859-1') + \ ' and ' + self._quote(part, 'iso-8859-1') - expected = u'utf-8: ÄÖÜäöü again: ÄÖÜäöü latin1: ÄÖÜäöü and ÄÖÜäöü' + expected = ( + u'utf-8: ÄÖÜäöü ' + u'again: ÄÖÜäöü ' + u'latin1: ÄÖÜäöü and ÄÖÜäöü' + ) self._test(text, expected) def test_tabs_are_expanded_to_align_with_eigth_spaces(self): @@ -377,18 +385,19 @@ class TestAddSignatureHeaders(unittest.TestCase): self.assertIn((utils.X_SIGNATURE_VALID_HEADER, u'True'), mail.headers) self.assertIn( - (utils.X_SIGNATURE_MESSAGE_HEADER, u'Untrusted: mocked'), mail.headers) + (utils.X_SIGNATURE_MESSAGE_HEADER, u'Untrusted: mocked'), + mail.headers) def test_unicode_as_bytes(self): mail = self.FakeMail() key = make_key() - key.uids = [make_uid('andreá@example.com', - uid=u'Andreá'.encode('utf-8'))] + key.uids = [make_uid('andreá@example.com', uid=u'Andreá')] mail = self.check(key, True) self.assertIn((utils.X_SIGNATURE_VALID_HEADER, u'True'), mail.headers) self.assertIn( - (utils.X_SIGNATURE_MESSAGE_HEADER, u'Valid: Andreá'), mail.headers) + (utils.X_SIGNATURE_MESSAGE_HEADER, u'Valid: Andreá'), + mail.headers) def test_error_message_unicode(self): mail = self.check(mock.Mock(), mock.Mock(), u'error message') @@ -397,13 +406,6 @@ class TestAddSignatureHeaders(unittest.TestCase): (utils.X_SIGNATURE_MESSAGE_HEADER, u'Invalid: error message'), mail.headers) - def test_error_message_bytes(self): - mail = self.check(mock.Mock(), mock.Mock(), b'error message') - self.assertIn((utils.X_SIGNATURE_VALID_HEADER, u'False'), mail.headers) - self.assertIn( - (utils.X_SIGNATURE_MESSAGE_HEADER, u'Invalid: error message'), - mail.headers) - def test_get_key_fails(self): mail = self.FakeMail() with mock.patch('alot.db.utils.crypto.get_key', @@ -433,7 +435,8 @@ class TestMessageFromFile(TestCaseClassCleanup): with open(os.path.join(search_dir, each)) as f: ctx.op_import(f) - cls.keys = [ctx.get_key("DD19862809A7573A74058FF255937AFBB156245D")] + cls.keys = [ + ctx.get_key("DD19862809A7573A74058FF255937AFBB156245D")] def test_erase_alot_header_signature_valid(self): """Alot uses special headers for passing certain kinds of information, @@ -442,13 +445,13 @@ class TestMessageFromFile(TestCaseClassCleanup): """ m = email.message.Message() m.add_header(utils.X_SIGNATURE_VALID_HEADER, 'Bad') - message = utils.message_from_file(io.BytesIO(m.as_string())) + message = utils.message_from_file(io.StringIO(m.as_string())) self.assertIs(message.get(utils.X_SIGNATURE_VALID_HEADER), None) def test_erase_alot_header_message(self): m = email.message.Message() m.add_header(utils.X_SIGNATURE_MESSAGE_HEADER, 'Bad') - message = utils.message_from_file(io.BytesIO(m.as_string())) + message = utils.message_from_file(io.StringIO(m.as_string())) self.assertIs(message.get(utils.X_SIGNATURE_MESSAGE_HEADER), None) def test_plain_mail(self): @@ -456,15 +459,15 @@ class TestMessageFromFile(TestCaseClassCleanup): m['Subject'] = 'test' m['From'] = 'me' m['To'] = 'Nobody' - message = utils.message_from_file(io.BytesIO(m.as_string())) + message = utils.message_from_file(io.StringIO(m.as_string())) self.assertEqual(message.get_payload(), 'This is some text') def _make_signed(self): """Create a signed message that is multipart/signed.""" - text = 'This is some text' + text = b'This is some text' t = email.mime.text.MIMEText(text, 'plain', 'utf-8') _, sig = crypto.detached_signature_for( - helper.email_as_string(t), self.keys) + helper.email_as_bytes(t), self.keys) s = email.mime.application.MIMEApplication( sig, 'pgp-signature', email.encoders.encode_7or8bit) m = email.mime.multipart.MIMEMultipart('signed', None, [t, s]) @@ -475,34 +478,35 @@ class TestMessageFromFile(TestCaseClassCleanup): def test_signed_headers_included(self): """Headers are added to the message.""" m = self._make_signed() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) def test_signed_valid(self): """Test that the signature is valid.""" m = self._make_signed() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertEqual(m[utils.X_SIGNATURE_VALID_HEADER], 'True') def test_signed_correct_from(self): """Test that the signature is valid.""" m = self._make_signed() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) # Don't test for valid/invalid since that might change - self.assertIn('ambig <ambig@example.com>', m[utils.X_SIGNATURE_MESSAGE_HEADER]) + self.assertIn( + 'ambig <ambig@example.com>', m[utils.X_SIGNATURE_MESSAGE_HEADER]) def test_signed_wrong_mimetype_second_payload(self): m = self._make_signed() m.get_payload(1).set_type('text/plain') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('expected Content-Type: ', m[utils.X_SIGNATURE_MESSAGE_HEADER]) def test_signed_wrong_micalg(self): m = self._make_signed() m.set_param('micalg', 'foo') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('expected micalg=pgp-...', m[utils.X_SIGNATURE_MESSAGE_HEADER]) @@ -524,7 +528,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ m = self._make_signed() m.set_param('micalg', 'PGP-SHA1') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('expected micalg=pgp-', m[utils.X_SIGNATURE_MESSAGE_HEADER]) @@ -539,7 +543,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ m = self._make_signed() m.attach(email.mime.text.MIMEText('foo')) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('expected exactly two messages, got 3', m[utils.X_SIGNATURE_MESSAGE_HEADER]) @@ -551,9 +555,9 @@ class TestMessageFromFile(TestCaseClassCleanup): if signed: t = self._make_signed() else: - text = 'This is some text' + text = b'This is some text' t = email.mime.text.MIMEText(text, 'plain', 'utf-8') - enc = crypto.encrypt(t.as_string(), self.keys) + enc = crypto.encrypt(helper.email_as_bytes(t), self.keys) e = email.mime.application.MIMEApplication( enc, 'octet-stream', email.encoders.encode_7or8bit) @@ -570,12 +574,12 @@ class TestMessageFromFile(TestCaseClassCleanup): # of the mail, rather than replacing the whole encrypted payload with # it's unencrypted equivalent m = self._make_encrypted() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertEqual(len(m.get_payload()), 3) def test_encrypted_unsigned_is_decrypted(self): m = self._make_encrypted() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) # Check using m.walk, since we're not checking for ordering, just # existence. self.assertIn('This is some text', [n.get_payload() for n in m.walk()]) @@ -585,13 +589,13 @@ class TestMessageFromFile(TestCaseClassCleanup): that there is a signature. """ m = self._make_encrypted() - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertNotIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertNotIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) def test_encrypted_signed_is_decrypted(self): m = self._make_encrypted(True) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('This is some text', [n.get_payload() for n in m.walk()]) def test_encrypted_signed_headers(self): @@ -599,23 +603,24 @@ class TestMessageFromFile(TestCaseClassCleanup): there is a signature. """ m = self._make_encrypted(True) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) - self.assertIn('ambig <ambig@example.com>', m[utils.X_SIGNATURE_MESSAGE_HEADER]) + self.assertIn( + 'ambig <ambig@example.com>', m[utils.X_SIGNATURE_MESSAGE_HEADER]) # TODO: tests for the RFC 2440 style combined signed/encrypted blob def test_encrypted_wrong_mimetype_first_payload(self): m = self._make_encrypted() m.get_payload(0).set_type('text/plain') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('Malformed OpenPGP message:', m.get_payload(2).get_payload()) def test_encrypted_wrong_mimetype_second_payload(self): m = self._make_encrypted() m.get_payload(1).set_type('text/plain') - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('Malformed OpenPGP message:', m.get_payload(2).get_payload()) @@ -625,7 +630,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ s = self._make_signed() m = email.mime.multipart.MIMEMultipart('mixed', None, [s]) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) @@ -635,7 +640,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ s = self._make_encrypted() m = email.mime.multipart.MIMEMultipart('mixed', None, [s]) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('This is some text', [n.get_payload() for n in m.walk()]) self.assertNotIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertNotIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) @@ -647,7 +652,7 @@ class TestMessageFromFile(TestCaseClassCleanup): """ s = self._make_encrypted(True) m = email.mime.multipart.MIMEMultipart('mixed', None, [s]) - m = utils.message_from_file(io.BytesIO(m.as_string())) + m = utils.message_from_file(io.StringIO(m.as_string())) self.assertIn('This is some text', [n.get_payload() for n in m.walk()]) self.assertIn(utils.X_SIGNATURE_VALID_HEADER, m) self.assertIn(utils.X_SIGNATURE_MESSAGE_HEADER, m) @@ -728,7 +733,8 @@ class TestExtractBody(unittest.TestCase): @mock.patch('alot.db.utils.settings.mailcap_find_match', mock.Mock(return_value=(None, {'view': 'cat'}))) def test_prefer_html(self): - expected = '<!DOCTYPE html><html><body>This is an html email</body></html>' + expected = ( + '<!DOCTYPE html><html><body>This is an html email</body></html>') mail = self._make_mixed_plain_html() actual = utils.extract_body(mail) @@ -755,7 +761,8 @@ class TestExtractBody(unittest.TestCase): '<!DOCTYPE html><html><body>This is an html email</body></html>', 'html')) actual = utils.extract_body(mail) - expected = '<!DOCTYPE html><html><body>This is an html email</body></html>' + expected = ( + '<!DOCTYPE html><html><body>This is an html email</body></html>') self.assertEqual(actual, expected) @@ -768,7 +775,8 @@ class TestExtractBody(unittest.TestCase): '<!DOCTYPE html><html><body>This is an html email</body></html>', 'html')) actual = utils.extract_body(mail) - expected = '<!DOCTYPE html><html><body>This is an html email</body></html>' + expected = ( + '<!DOCTYPE html><html><body>This is an html email</body></html>') self.assertEqual(actual, expected) diff --git a/tests/helper_test.py b/tests/helper_test.py index 4d003921..1d20caa8 100644 --- a/tests/helper_test.py +++ b/tests/helper_test.py @@ -143,13 +143,13 @@ class TestSplitCommandstring(unittest.TestCase): self.assertListEqual(actual, expected) def test_bytes(self): - base = b'echo "foo bar"' - expected = [b'echo', b'foo bar'] + base = 'echo "foo bar"' + expected = ['echo', 'foo bar'] self._test(base, expected) def test_unicode(self): - base = u'echo "foo €"' - expected = [b'echo', u'foo €'.encode('utf-8')] + base = 'echo "foo €"' + expected = ['echo', 'foo €'] self._test(base, expected) @@ -221,20 +221,20 @@ class TestPrettyDatetime(unittest.TestCase): p.stop() def test_just_now(self): - for i in (self.random.randint(0, 60) for _ in xrange(5)): + for i in (self.random.randint(0, 60) for _ in range(5)): test = self.now - datetime.timedelta(seconds=i) actual = helper.pretty_datetime(test) self.assertEquals(actual, u'just now') def test_x_minutes_ago(self): - for i in (self.random.randint(60, 3600) for _ in xrange(10)): + for i in (self.random.randint(60, 3600) for _ in range(10)): test = self.now - datetime.timedelta(seconds=i) actual = helper.pretty_datetime(test) self.assertEquals( actual, u'{}min ago'.format((self.now - test).seconds // 60)) def test_x_hours_ago(self): - for i in (self.random.randint(3600, 3600 * 6) for _ in xrange(10)): + for i in (self.random.randint(3600, 3600 * 6) for _ in range(10)): test = self.now - datetime.timedelta(seconds=i) actual = helper.pretty_datetime(test) self.assertEquals( @@ -251,7 +251,7 @@ class TestPrettyDatetime(unittest.TestCase): expected = test.strftime('%I:%M%p').lower() else: expected = test.strftime('%H:%M') - expected = expected.decode('utf-8') + expected = expected return expected def test_future_seconds(self): diff --git a/tests/settings/manager_test.py b/tests/settings/manager_test.py index 42f944db..d04e244d 100644 --- a/tests/settings/manager_test.py +++ b/tests/settings/manager_test.py @@ -22,7 +22,7 @@ from .. import utilities class TestSettingsManager(unittest.TestCase): def test_reading_synchronize_flags_from_notmuch_config(self): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = true @@ -34,7 +34,7 @@ class TestSettingsManager(unittest.TestCase): self.assertTrue(actual) def test_parsing_notmuch_config_with_non_bool_synchronize_flag_fails(self): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = not bool @@ -45,7 +45,7 @@ class TestSettingsManager(unittest.TestCase): SettingsManager(notmuch_rc=f.name) def test_reload_notmuch_config(self): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = false @@ -53,7 +53,7 @@ class TestSettingsManager(unittest.TestCase): self.addCleanup(os.unlink, f.name) manager = SettingsManager(notmuch_rc=f.name) - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = true @@ -72,7 +72,7 @@ class TestSettingsManager(unittest.TestCase): defaults not being loaded if there isn't an alot config files, and thus calls like `get_theming_attribute` fail with strange exceptions. """ - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [maildir] synchronize_flags = true @@ -86,7 +86,7 @@ class TestSettingsManager(unittest.TestCase): # todo: For py3, don't mock the logger, use assertLogs unknown_settings = ['templates_dir', 'unknown_section', 'unknown_1', 'unknown_2'] - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ {x[0]} = /templates/dir [{x[1]}] @@ -110,7 +110,7 @@ class TestSettingsManager(unittest.TestCase): unknown_settings, mock_logger.info.call_args_list)) def test_read_notmuch_config_doesnt_exist(self): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [accounts] [[default]] @@ -125,7 +125,7 @@ class TestSettingsManager(unittest.TestCase): def test_dont_choke_on_regex_special_chars_in_tagstring(self): tag = 'to**do' - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ [tags] [[{tag}]] @@ -171,7 +171,7 @@ class TestSettingsManagerExpandEnvironment(unittest.TestCase): user_setting = '/path/to/template/dir' with mock.patch.dict('os.environ', {self.xdg_name: self.xdg_custom}): - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write('template_dir = {}'.format(user_setting)) self.addCleanup(os.unlink, f.name) @@ -188,7 +188,7 @@ class TestSettingsManagerExpandEnvironment(unittest.TestCase): with mock.patch.dict('os.environ', {self.xdg_name: self.xdg_custom, 'foo': foo_env}): foo_in_config = 'foo_set_in_config' - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(textwrap.dedent("""\ foo = {} template_dir = ${{XDG_CONFIG_HOME}}/$foo/%(foo)s/${{foo}} @@ -221,7 +221,7 @@ class TestSettingsManagerGetAccountByAddress(utilities.TestCaseClassCleanup): """) # Allow settings.reload to work by not deleting the file until the end - with tempfile.NamedTemporaryFile(delete=False) as f: + with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: f.write(config) cls.addClassCleanup(os.unlink, f.name) @@ -244,7 +244,7 @@ class TestSettingsManagerGetAccountByAddress(utilities.TestCaseClassCleanup): def test_doesnt_exist_no_default(self): with tempfile.NamedTemporaryFile() as f: - f.write('') + f.write(b'') settings = SettingsManager(alot_rc=f.name) with self.assertRaises(NoMatchingAccount): settings.get_account_by_address('that_guy@example.com', |