summaryrefslogtreecommitdiff
path: root/alot
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2017-09-02 08:35:01 +0100
committerGitHub <noreply@github.com>2017-09-02 08:35:01 +0100
commit668925817cd8aeed7ef9926969d49c4586c26adf (patch)
tree0cd16d2279a971d0b5d650f03ee3f6ce0e592852 /alot
parent3a3898f2ce976fbb800f55764675c4962e7adf28 (diff)
parentc09196eb92af61ce0efa2f2ea47f42856ef87ac9 (diff)
Merge branch 'master' into fix/spelling
Diffstat (limited to 'alot')
-rw-r--r--alot/account.py149
-rw-r--r--alot/addressbook/abook.py3
-rw-r--r--alot/commands/__init__.py1
-rw-r--r--alot/commands/envelope.py9
-rw-r--r--alot/commands/globals.py9
-rw-r--r--alot/commands/thread.py6
-rw-r--r--alot/crypto.py44
-rw-r--r--alot/db/manager.py3
-rw-r--r--alot/db/message.py9
-rw-r--r--alot/db/utils.py29
-rw-r--r--alot/defaults/alot.rc.spec8
-rw-r--r--alot/settings/manager.py12
-rw-r--r--alot/settings/utils.py1
-rw-r--r--alot/utils/cached_property.py48
-rw-r--r--alot/widgets/globals.py4
-rw-r--r--alot/widgets/search.py5
16 files changed, 250 insertions, 90 deletions
diff --git a/alot/account.py b/alot/account.py
index 30f0e8da..a224c109 100644
--- a/alot/account.py
+++ b/alot/account.py
@@ -1,4 +1,6 @@
+# encoding=utf-8
# Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com>
+# Copyright © 2017 Dylan Baker
# 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
@@ -7,12 +9,152 @@ import abc
import glob
import logging
import mailbox
+import operator
import os
from .helper import call_cmd_async
from .helper import split_commandstring
+class Address(object):
+
+ """A class that represents an email address.
+
+ This class implements a number of RFC requirements (as explained in detail
+ below) specifically in the comparison of email addresses to each other.
+
+ This class abstracts the requirements of RFC 5321 § 2.4 on the user name
+ portion of the email:
+
+ local-part of a mailbox MUST BE treated as case sensitive. Therefore,
+ SMTP implementations MUST take care to preserve the case of mailbox
+ local-parts. In particular, for some hosts, the user "smith" is
+ different from the user "Smith". However, exploiting the case
+ sensitivity of mailbox local-parts impedes interoperability and is
+ discouraged. Mailbox domains follow normal DNS rules and are hence not
+ case sensitive.
+
+ This is complicated by § 2.3.11 of the same RFC:
+
+ The standard mailbox naming convention is defined to be
+ "local-part@domain"; contemporary usage permits a much broader set of
+ applications than simple "user names". Consequently, and due to a long
+ history of problems when intermediate hosts have attempted to optimize
+ transport by modifying them, the local-part MUST be interpreted and
+ assigned semantics only by the host specified in the domain part of the
+ address.
+
+ And also the restrictions that RFC 1035 § 3.1 places on the domain name:
+
+ Name servers and resolvers must compare [domains] in a case-insensitive
+ manner
+
+ Because of RFC 6531 § 3.2, we take special care to ensure that unicode
+ names will work correctly:
+
+ An SMTP server that announces the SMTPUTF8 extension MUST be prepared
+ to accept a UTF-8 string [RFC3629] in any position in which RFC 5321
+ specifies that a <mailbox> can appear. Although the characters in the
+ <local-part> are permitted to contain non-ASCII characters, the actual
+ parsing of the <local-part> and the delimiters used are unchanged from
+ the base email specification [RFC5321]
+
+ What this means is that the username can be either case-insensitive or not,
+ but only the receiving SMTP server can know what it's own rules are. The
+ consensus is that the vast majority (all?) of the SMTP servers in modern
+ 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 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'
+ self.username = user
+ self.domainname = domain
+ self.case_sensitive = case_sensitive
+
+ @classmethod
+ 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 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'@')
+ return cls(username, domainname, case_sensitive=case_sensitive)
+
+ def __repr__(self):
+ return u'Address({!r}, {!r}, case_sensitive={})'.format(
+ self.username,
+ self.domainname,
+ unicode(self.case_sensitive))
+
+ def __unicode__(self):
+ return u'{}@{}'.format(self.username, self.domainname)
+
+ def __str__(self):
+ return u'{}@{}'.format(self.username, self.domainname).encode('utf-8')
+
+ def __cmp(self, other, comparitor):
+ """Shared helper for rich comparison operators.
+
+ This allows the comparison operators to be relatively simple and share
+ the complex logic.
+
+ 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.
+
+ :param other: The other address to compare against
+ :type other: unicode or ~alot.account.Address
+ :param callable comparitor: A function with the a signature
+ (unicode, unicode) -> bool that will compare the two instance.
+ The intention is to use functions from the operator module.
+ """
+ if isinstance(other, unicode):
+ ouser, odomain = other.split(u'@')
+ elif isinstance(other, str):
+ ouser, odomain = other.decode('utf-8').split(u'@')
+ else:
+ ouser = other.username
+ odomain = other.domainname
+
+ if not self.case_sensitive:
+ ouser = ouser.lower()
+ username = self.username.lower()
+ else:
+ username = self.username
+
+ return (comparitor(username, ouser) and
+ 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')
+ 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')
+ # != 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)
+
+ def __hash__(self):
+ return hash((self.username.lower(), self.domainname.lower(),
+ self.case_sensitive))
+
+
class SendingMailFailed(RuntimeError):
pass
@@ -59,7 +201,7 @@ class Account(object):
signature_filename=None, signature_as_attachment=False,
sent_box=None, sent_tags=None, draft_box=None,
draft_tags=None, abook=None, sign_by_default=False,
- encrypt_by_default=u"none",
+ encrypt_by_default=u"none", case_sensitive_username=False,
**_):
sent_tags = sent_tags or []
if 'sent' not in sent_tags:
@@ -68,8 +210,9 @@ class Account(object):
if 'draft' not in draft_tags:
draft_tags.append('draft')
- self.address = address
- self.aliases = 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.gpg_key = gpg_key
diff --git a/alot/addressbook/abook.py b/alot/addressbook/abook.py
index b620fbac..41b5103e 100644
--- a/alot/addressbook/abook.py
+++ b/alot/addressbook/abook.py
@@ -16,7 +16,8 @@ class AbookAddressBook(AddressBook):
:type path: str
"""
AddressBook.__init__(self, **kwargs)
- DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', 'defaults')
+ DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..',
+ 'defaults')
self._spec = os.path.join(DEFAULTSPATH, 'abook_contacts.spec')
path = os.path.expanduser(path)
self._config = read_config(path, self._spec)
diff --git a/alot/commands/__init__.py b/alot/commands/__init__.py
index 0638926a..bcd27c07 100644
--- a/alot/commands/__init__.py
+++ b/alot/commands/__init__.py
@@ -34,6 +34,7 @@ class CommandCanceled(Exception):
"""
pass
+
COMMANDS = {
'search': {},
'envelope': {},
diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py
index 9681a020..f497ecc2 100644
--- a/alot/commands/envelope.py
+++ b/alot/commands/envelope.py
@@ -124,9 +124,8 @@ class SaveCommand(Command):
return
if account.draft_box is None:
- ui.notify(
- 'abort: account <%s> has no draft_box set.' % envelope.get('From'),
- priority='error')
+ msg = 'abort: Account for {} has no draft_box'
+ ui.notify(msg.format(account.address), priority='error')
return
mail = envelope.construct_mail()
@@ -511,8 +510,8 @@ class SignCommand(Command):
return
if not acc.gpg_key:
envelope.sign = False
- ui.notify('Account for {} has no gpg key'.format(acc.address),
- priority='error')
+ msg = 'Account for {} has no gpg key'
+ ui.notify(msg.format(acc.address), priority='error')
return
envelope.sign_key = acc.gpg_key
else:
diff --git a/alot/commands/globals.py b/alot/commands/globals.py
index 565bd742..57ddfcb6 100644
--- a/alot/commands/globals.py
+++ b/alot/commands/globals.py
@@ -78,7 +78,8 @@ class ExitCommand(Command):
if ui.db_was_locked:
msg = 'Database locked. Exit without saving?'
- if (yield ui.choice(msg, msg_position='left', cancel='no')) == 'no':
+ response = yield ui.choice(msg, msg_position='left', cancel='no')
+ if response == 'no':
return
ui.exit()
@@ -855,7 +856,8 @@ class ComposeCommand(Command):
self.envelope.sign = account.sign_by_default
self.envelope.sign_key = account.gpg_key
else:
- msg = 'Cannot find gpg key for account {}'.format(account.address)
+ msg = 'Cannot find gpg key for account {}'
+ msg = msg.format(account.address)
logging.warning(msg)
ui.notify(msg, priority='error')
@@ -910,7 +912,8 @@ class ComposeCommand(Command):
logging.debug("Trying to encrypt message because "
"account.encrypt_by_default=%s",
account.encrypt_by_default)
- yield set_encrypt(ui, self.envelope, block_error=self.encrypt, signed_only=True)
+ yield set_encrypt(ui, self.envelope, block_error=self.encrypt,
+ signed_only=True)
else:
logging.debug("No encryption by default, encrypt_by_default=%s",
account.encrypt_by_default)
diff --git a/alot/commands/thread.py b/alot/commands/thread.py
index 4c04664f..b0eff761 100644
--- a/alot/commands/thread.py
+++ b/alot/commands/thread.py
@@ -71,11 +71,13 @@ 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(a) for a in account.get_addresses()]
+ acc_addresses = [re.escape(unicode(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('^' + alias + '$', flags=re.IGNORECASE)
+ regex = re.compile(
+ u'^' + unicode(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)
diff --git a/alot/crypto.py b/alot/crypto.py
index 52eb8c58..6e3e8fa6 100644
--- a/alot/crypto.py
+++ b/alot/crypto.py
@@ -81,31 +81,22 @@ def get_key(keyid, validate=False, encrypt=False, sign=False,
valid_key = None
- # Catching exceptions for list_keys
- try:
- for k in list_keys(hint=keyid):
- try:
- validate_key(k, encrypt=encrypt, sign=sign)
- except GPGProblem:
- # if the key is invalid for given action skip it
- continue
-
- if valid_key:
- # we have already found one valid key and now we find
- # another? We really received an ambiguous keyid
- raise GPGProblem(
- "More than one key found matching this filter. "
- "Please be more specific "
- "(use a key ID like 4AC8EE1D).",
- code=GPGCode.AMBIGUOUS_NAME)
- valid_key = k
- except gpg.errors.GPGMEError as e:
- # This if will be triggered if there is no key matching at all.
- if e.getcode() == gpg.errors.AMBIGUOUS_NAME:
+ for k in list_keys(hint=keyid):
+ try:
+ validate_key(k, encrypt=encrypt, sign=sign)
+ except GPGProblem:
+ # if the key is invalid for given action skip it
+ continue
+
+ if valid_key:
+ # we have already found one valid key and now we find
+ # another? We really received an ambiguous keyid
raise GPGProblem(
- 'Can not find any key for "{}".'.format(keyid),
- code=GPGCode.NOT_FOUND)
- raise
+ "More than one key found matching this filter. "
+ "Please be more specific "
+ "(use a key ID like 4AC8EE1D).",
+ code=GPGCode.AMBIGUOUS_NAME)
+ valid_key = k
if not valid_key:
# there were multiple keys found but none of them are valid for
@@ -120,7 +111,7 @@ def get_key(keyid, validate=False, encrypt=False, sign=False,
'Can not find usable key for "{}".'.format(keyid),
code=GPGCode.NOT_FOUND)
else:
- raise e
+ 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)
@@ -161,7 +152,8 @@ def detached_signature_for(plaintext_str, keys):
"""
ctx = gpg.core.Context(armor=True)
ctx.signers = keys
- (sigblob, sign_result) = ctx.sign(plaintext_str, mode=gpg.constants.SIG_MODE_DETACH)
+ (sigblob, sign_result) = ctx.sign(plaintext_str,
+ mode=gpg.constants.SIG_MODE_DETACH)
return sign_result.signatures, sigblob
diff --git a/alot/db/manager.py b/alot/db/manager.py
index d71812d1..dfd681d0 100644
--- a/alot/db/manager.py
+++ b/alot/db/manager.py
@@ -377,7 +377,8 @@ class DBManager(object):
:param sort: Sort order. one of ['oldest_first', 'newest_first',
'message_id', 'unsorted']
:type query: str
- :param exclude_tags: Tags to exclude by default unless included in the search
+ :param exclude_tags: Tags to exclude by default unless included in the
+ search
:type exclude_tags: list of str
:returns: a pipe together with the process that asynchronously
writes to it.
diff --git a/alot/db/message.py b/alot/db/message.py
index f8dcb74f..9d7437eb 100644
--- a/alot/db/message.py
+++ b/alot/db/message.py
@@ -66,17 +66,17 @@ class Message(object):
def __eq__(self, other):
if isinstance(other, type(self)):
- return self.get_message_id() == other.get_message_id()
+ return self._id == other.get_message_id()
return NotImplemented
def __ne__(self, other):
if isinstance(other, type(self)):
- return self.get_message_id() != other.get_message_id()
+ return self._id != other.get_message_id()
return NotImplemented
def __lt__(self, other):
if isinstance(other, type(self)):
- return self.get_message_id() < other.get_message_id()
+ return self._id < other.get_message_id()
return NotImplemented
def get_email(self):
@@ -116,8 +116,7 @@ class Message(object):
def get_tags(self):
"""returns tags attached to this message as list of strings"""
- l = sorted(self._tags)
- return l
+ return sorted(self._tags)
def get_thread(self):
"""returns the :class:`~alot.db.Thread` this msg belongs to"""
diff --git a/alot/db/utils.py b/alot/db/utils.py
index 2db850ce..379fd6a2 100644
--- a/alot/db/utils.py
+++ b/alot/db/utils.py
@@ -45,6 +45,8 @@ def add_signature_headers(mail, sigs, error_msg):
string indicating no error
'''
sig_from = u''
+ sig_known = True
+ uid_trusted = False
if isinstance(error_msg, str):
error_msg = error_msg.decode('utf-8')
@@ -60,13 +62,11 @@ def add_signature_headers(mail, sigs, error_msg):
uid_trusted = True
break
else:
- # No trusted uid found, we did not break but drop from the
- # for loop.
- uid_trusted = False
+ # No trusted uid found, since we did not break from the loop.
sig_from = key.uids[0].uid.decode('utf-8')
- except:
+ except GPGProblem:
sig_from = sigs[0].fpr.decode('utf-8')
- uid_trusted = False
+ sig_known = False
if error_msg:
msg = u'Invalid: {}'.format(error_msg)
@@ -75,7 +75,8 @@ def add_signature_headers(mail, sigs, error_msg):
else:
msg = u'Untrusted: {}'.format(sig_from)
- mail.add_header(X_SIGNATURE_VALID_HEADER, 'False' if error_msg else 'True')
+ mail.add_header(X_SIGNATURE_VALID_HEADER,
+ 'False' if (error_msg or not sig_known) else 'True')
mail.add_header(X_SIGNATURE_MESSAGE_HEADER, msg)
@@ -297,16 +298,20 @@ def extract_headers(mail, headers=None):
def extract_body(mail, types=None, field_key='copiousoutput'):
- """
- returns a body text string for given mail.
- If types is `None`, `text/*` is used:
- The exact preferred type is specified by the prefer_plaintext config option
- which defaults to text/html.
+ """Returns a string view of a Message.
+
+ If the `types` argument is set then any encoding types there will be used
+ as the prefered encoding to extract. If `types` is None then
+ :ref:`prefer_plaintext <prefer-plaintext>` will be consulted; if it is True
+ then text/plain parts will be returned, if it is false then text/html will
+ be returned if present or text/plain if there are no text/html parts.
:param mail: the mail to use
:type mail: :class:`email.Message`
:param types: mime content types to use for body string
- :type types: list of str
+ :type types: list[str]
+ :returns: The combined text of any parts to be used
+ :rtype: str
"""
preferred = 'text/plain' if settings.get(
diff --git a/alot/defaults/alot.rc.spec b/alot/defaults/alot.rc.spec
index 97de329d..eeae4348 100644
--- a/alot/defaults/alot.rc.spec
+++ b/alot/defaults/alot.rc.spec
@@ -355,6 +355,14 @@ thread_focus_linewise = boolean(default=True)
# use your default key.
gpg_key = gpg_key_hint(default=None)
+ # Whether the server treats the address as case-senstive or
+ # case-insensitve (True for the former, False for the latter)
+ #
+ # .. 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.
+ case_sensitive_username = boolean(default=False)
+
# address book for this account
[[[abook]]]
# type identifier for address book
diff --git a/alot/settings/manager.py b/alot/settings/manager.py
index cf26f941..c71fba42 100644
--- a/alot/settings/manager.py
+++ b/alot/settings/manager.py
@@ -25,7 +25,8 @@ from .theme import Theme
DEFAULTSPATH = os.path.join(os.path.dirname(__file__), '..', 'defaults')
-DATA_DIRS = os.environ.get('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':')
+DATA_DIRS = os.environ.get('XDG_DATA_DIRS',
+ '/usr/local/share:/usr/share').split(':')
class SettingsManager(object):
@@ -37,8 +38,10 @@ 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 os.path.exists(alot_rc))
- assert notmuch_rc is None or (isinstance(notmuch_rc, basestring) and os.path.exists(notmuch_rc))
+ assert alot_rc is None or (isinstance(alot_rc, basestring) and
+ os.path.exists(alot_rc))
+ assert notmuch_rc is None or (isinstance(notmuch_rc, basestring) and
+ os.path.exists(notmuch_rc))
self.hooks = None
self._mailcaps = mailcap.getcaps()
self._notmuchconfig = None
@@ -62,7 +65,8 @@ class SettingsManager(object):
Implementation Detail: this is the same code called by the constructor
to set bindings at alot startup.
"""
- self._bindings = ConfigObj(os.path.join(DEFAULTSPATH, 'default.bindings'))
+ self._bindings = ConfigObj(os.path.join(DEFAULTSPATH,
+ 'default.bindings'))
self.read_notmuch_config()
self.read_config()
diff --git a/alot/settings/utils.py b/alot/settings/utils.py
index ea56b264..d87157c3 100644
--- a/alot/settings/utils.py
+++ b/alot/settings/utils.py
@@ -9,6 +9,7 @@ from urwid import AttrSpec
from .errors import ConfigError
+
def read_config(configpath=None, specpath=None, checks=None):
"""
get a (validated) config object for given config file path.
diff --git a/alot/utils/cached_property.py b/alot/utils/cached_property.py
index e6187283..680cd6f4 100644
--- a/alot/utils/cached_property.py
+++ b/alot/utils/cached_property.py
@@ -1,34 +1,34 @@
# verbatim from werkzeug.utils.cached_property
#
-#Copyright (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
+# Copyright (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
#
-#Redistribution and use in source and binary forms, with or without
-#modification, are permitted provided that the following conditions are
-#met:
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
#
-# * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
#
-# * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following
-# disclaimer in the documentation and/or other materials provided
-# with the distribution.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
#
-# * The names of the contributors may not be used to endorse or
-# promote products derived from this software without specific
-# prior written permission.
+# * The names of the contributors may not be used to endorse or
+# promote products derived from this software without specific
+# prior written permission.
#
-#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-#"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-#LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-#A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-#OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-#SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-#LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-#DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-#THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-#(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-#OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
_missing = object()
diff --git a/alot/widgets/globals.py b/alot/widgets/globals.py
index 50ef80c6..3aea7622 100644
--- a/alot/widgets/globals.py
+++ b/alot/widgets/globals.py
@@ -86,8 +86,8 @@ class CompleteEdit(urwid.Edit):
The interpretation of some keypresses is hard-wired:
:enter: calls 'on_exit' callback with current value
- :esc/ctrl g: calls 'on_exit' with value `None`, which can be interpreted
- as cancellation
+ :esc/ctrl g: calls 'on_exit' with value `None`, which can be
+ interpreted as cancellation
:tab: calls the completer and tabs forward in the result list
:shift tab: tabs backward in the result list
:up/down: move in the local input history
diff --git a/alot/widgets/search.py b/alot/widgets/search.py
index 905887cc..446a8964 100644
--- a/alot/widgets/search.py
+++ b/alot/widgets/search.py
@@ -115,8 +115,9 @@ class ThreadlineWidget(urwid.AttrMap):
if self.thread:
fallback_normal = struct[name]['normal']
fallback_focus = struct[name]['focus']
- tag_widgets = sorted(TagWidget(t, fallback_normal, fallback_focus)
- for t in self.thread.get_tags())
+ tag_widgets = sorted(
+ TagWidget(t, fallback_normal, fallback_focus)
+ for t in self.thread.get_tags())
else:
tag_widgets = []
cols = []