summaryrefslogtreecommitdiff
path: root/alot
diff options
context:
space:
mode:
authorPatrick Totzke <patricktotzke@gmail.com>2017-08-30 19:20:15 +0100
committerGitHub <noreply@github.com>2017-08-30 19:20:15 +0100
commit16e6f984d7b02a9c9f2390a62cbb7fb813b641e7 (patch)
treecadd97072148ff06863bf09d1651957abf0907c0 /alot
parent0a7bf658fdcf906f75bf29c44ef05da65fd1c053 (diff)
parent362268d9a506c1d87f197bccef6fed0dcb83d629 (diff)
Merge pull request #1108 from dcbaker/wip/fix-1107
Handle servers that treat the user segment of an address as case insensitive
Diffstat (limited to 'alot')
-rw-r--r--alot/account.py149
-rw-r--r--alot/commands/thread.py6
-rw-r--r--alot/defaults/alot.rc.spec8
3 files changed, 158 insertions, 5 deletions
diff --git a/alot/account.py b/alot/account.py
index d8f114c5..e561f0c9 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 not bytes'
+ assert isinstance(domain, unicode), 'Domain name must be unicode not bytes'
+ 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 not bytes'
+ 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('Cannot compare Address to any but Address or basestring')
+ return self.__cmp(other, operator.eq)
+
+ def __ne__(self, other):
+ if not isinstance(other, (Address, basestring)):
+ raise TypeError('Cannot compare Address to any but 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/commands/thread.py b/alot/commands/thread.py
index 02017ae9..437a9bd0 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/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