summaryrefslogtreecommitdiff
path: root/alot/account.py
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/account.py
parent3a3898f2ce976fbb800f55764675c4962e7adf28 (diff)
parentc09196eb92af61ce0efa2f2ea47f42856ef87ac9 (diff)
Merge branch 'master' into fix/spelling
Diffstat (limited to 'alot/account.py')
-rw-r--r--alot/account.py149
1 files changed, 146 insertions, 3 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