From f7c5b841568886be64695a14f341c4c7c58b3fba Mon Sep 17 00:00:00 2001 From: vrs Date: Sat, 8 Dec 2018 23:11:24 +0100 Subject: match addresses against accounts, not address lists fixes #1230, fixes an unfiled bug in clear_my_address() --- alot/account.py | 25 ++++++++++++++++++++++- alot/commands/thread.py | 46 +++++++++++++++---------------------------- alot/db/thread.py | 16 ++++++++------- alot/settings/manager.py | 8 ++++---- tests/account_test.py | 22 +++++++++++++++------ tests/commands/thread_test.py | 43 ++++++++++++++++++++++------------------ 6 files changed, 93 insertions(+), 67 deletions(-) diff --git a/alot/account.py b/alot/account.py index c8f3c09a..116c3417 100644 --- a/alot/account.py +++ b/alot/account.py @@ -9,6 +9,7 @@ import logging import mailbox import operator import os +import re from .helper import call_cmd_async from .helper import split_commandstring @@ -176,7 +177,7 @@ class Account(object): """this accounts main email address""" aliases = [] """list of alternative addresses""" - alias_regexp = [] + alias_regexp = "" """regex matching alternative addresses""" realname = None """real name used to format from-headers""" @@ -243,12 +244,34 @@ class Account(object): encrypt_by_default = u"none" logging.info(msg) self.encrypt_by_default = encrypt_by_default + # cache alias_regexp regexes + if self.alias_regexp != "": + self._alias_regexp = re.compile( + u'^' + str(self.alias_regexp) + u'$', + flags=0 if case_sensitive_username else re.IGNORECASE) + def get_addresses(self): """return all email addresses connected to this account, in order of their importance""" return [self.address] + self.aliases + def matches_address(self, address): + """returns whether this account knows about an email address + + :param str address: address to look up + :rtype: bool + """ + if self.address == address: + return True + for alias in self.aliases: + if alias == address: + return True + if self._alias_regexp is not None: + if self._alias_regexp.match(address): + return True + return False + @staticmethod def store_mail(mbx, mail): """ diff --git a/alot/commands/thread.py b/alot/commands/thread.py index f68e5405..3fbe8148 100644 --- a/alot/commands/thread.py +++ b/alot/commands/thread.py @@ -6,7 +6,6 @@ import argparse import logging import mailcap import os -import re import subprocess import tempfile import email @@ -69,35 +68,23 @@ def determine_sender(mail, action='reply'): logging.debug('candidate addresses: %s', candidate_addresses) # pick the most important account that has an address in candidates - # and use that accounts realname and the address found here + # and use that account's realname and the address found here for account in my_accounts: - 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'^' + str(alias) + u'$', - flags=( - re.IGNORECASE if not account.address.case_sensitive - else 0)) - for seen_name, seen_address in candidate_addresses: - if not regex.match(seen_address): - continue - logging.debug("match!: '%s' '%s'", seen_address, alias) + for seen_name, seen_address in candidate_addresses: + if account.matches_address(seen_address): if settings.get(action + '_force_realname'): realname = account.realname else: realname = seen_name if settings.get(action + '_force_address'): - address = account.address + address = str(account.address) else: address = seen_address logging.debug('using realname: "%s"', realname) logging.debug('using address: %s', address) - from_value = formataddr((realname, str(address))) + from_value = formataddr((realname, address)) return from_value, account # revert to default account if nothing found @@ -199,24 +186,23 @@ class ReplyCommand(Command): # set To sender = mail['Reply-To'] or mail['From'] - my_addresses = settings.get_addresses() sender_address = parseaddr(sender)[1] cc = [] # check if reply is to self sent message - if sender_address in my_addresses: + if account.matches_address(sender_address): recipients = mail.get_all('To', []) emsg = 'Replying to own message, set recipients to: %s' \ % recipients logging.debug(emsg) else: - recipients = self.clear_my_address([], [sender]) + recipients = [sender] if self.groupreply: # make sure that our own address is not included # if the message was self-sent, then our address is not included MFT = mail.get_all('Mail-Followup-To', []) - followupto = self.clear_my_address(my_addresses, MFT) + followupto = self.clear_my_address(account, MFT) if followupto and settings.get('honor_followup_to'): logging.debug('honor followup to: %s', ', '.join(followupto)) recipients = followupto @@ -226,15 +212,15 @@ class ReplyCommand(Command): recipients.append(mail['From']) # append To addresses if not replying to self sent message - if sender_address not in my_addresses: + if not account.matches_address(sender_address): cleared = self.clear_my_address( - my_addresses, mail.get_all('To', [])) + account, mail.get_all('To', [])) recipients.extend(cleared) # copy cc for group-replies if 'Cc' in mail: cc = self.clear_my_address( - my_addresses, mail.get_all('Cc', [])) + account, mail.get_all('Cc', [])) envelope.add('Cc', decode_header(', '.join(cc))) to = ', '.join(self.ensure_unique_address(recipients)) @@ -293,11 +279,11 @@ class ReplyCommand(Command): encrypt=encrypt)) @staticmethod - def clear_my_address(my_addresses, value): - """return recipient header without the addresses in my_addresses + def clear_my_address(my_account, value): + """return recipient header without the addresses in my_account - :param my_addresses: a list of my email addresses (no real name part) - :type my_addresses: list(str) + :param my_account: my account + :type my_account: :class:`Account` :param value: a list of recipient or sender strings (with or without real names as taken from email headers) :type value: list(str) @@ -306,7 +292,7 @@ class ReplyCommand(Command): """ new_value = [] for name, address in getaddresses(value): - if address not in my_addresses: + if not my_account.matches_address(address): new_value.append(formataddr((name, str(address)))) return new_value diff --git a/alot/db/thread.py b/alot/db/thread.py index fe7ea10f..0943d815 100644 --- a/alot/db/thread.py +++ b/alot/db/thread.py @@ -174,14 +174,14 @@ class Thread(object): return self._authors - def get_authors_string(self, own_addrs=None, replace_own=None): + def get_authors_string(self, own_accts=None, replace_own=None): """ returns a string of comma-separated authors Depending on settings, it will substitute "me" for author name if address is user's own. - :param own_addrs: list of own email addresses to replace - :type own_addrs: list of str + :param own_accts: list of own accounts to replace + :type own_accts: list of :class:`Account` :param replace_own: whether or not to actually do replacement :type replace_own: bool :rtype: str @@ -189,12 +189,14 @@ class Thread(object): if replace_own is None: replace_own = settings.get('thread_authors_replace_me') if replace_own: - if own_addrs is None: - own_addrs = settings.get_addresses() + if own_accts is None: + own_accts = settings.get_accounts() authorslist = [] for aname, aaddress in self.get_authors(): - if aaddress in own_addrs: - aname = settings.get('thread_authors_me') + for account in own_accts: + if account.matches_address(aaddress): + aname = settings.get('thread_authors_me') + break if not aname: aname = aaddress if aname not in authorslist: diff --git a/alot/settings/manager.py b/alot/settings/manager.py index d1f57083..87ae14c7 100644 --- a/alot/settings/manager.py +++ b/alot/settings/manager.py @@ -469,16 +469,16 @@ class SettingsManager(object): :param str address: address to look up. A realname part will be ignored. :param bool return_default: If True and no address can be found, then - the default account wil be returned + the default account wil be returned. :rtype: :class:`Account` :raises ~alot.settings.errors.NoMatchingAccount: If no account can be found. This includes if return_default is True and there are no accounts defined. """ _, address = email.utils.parseaddr(address) - for myad in self.get_addresses(): - if myad == address: - return self._accountmap[myad] + for account in self.get_accounts(): + if account.matches_address(address): + return account if return_default: try: return self.get_accounts()[0] diff --git a/tests/account_test.py b/tests/account_test.py index 9f6287be..9d0ac125 100644 --- a/tests/account_test.py +++ b/tests/account_test.py @@ -32,20 +32,30 @@ class _AccountTestClass(account.Account): class TestAccount(unittest.TestCase): """Tests for the Account class.""" - def test_get_address(self): + def test_matches_address(self): """Tests address without aliases.""" acct = _AccountTestClass(address="foo@example.com") - self.assertListEqual(acct.get_addresses(), ['foo@example.com']) + self.assertTrue(acct.matches_address(u"foo@example.com")) + self.assertFalse(acct.matches_address(u"bar@example.com")) - def test_get_address_with_aliases(self): + def test_matches_address_with_aliases(self): """Tests address with aliases.""" acct = _AccountTestClass(address="foo@example.com", aliases=['bar@example.com']) - self.assertListEqual(acct.get_addresses(), - ['foo@example.com', 'bar@example.com']) + self.assertTrue(acct.matches_address(u"foo@example.com")) + self.assertTrue(acct.matches_address(u"bar@example.com")) + self.assertFalse(acct.matches_address(u"baz@example.com")) + + def test_matches_address_with_regex_aliases(self): + """Tests address with regex aliases.""" + acct = _AccountTestClass(address=u"foo@example.com", + alias_regexp=r'to\+.*@example.com') + self.assertTrue(acct.matches_address(u"to+foo@example.com")) + self.assertFalse(acct.matches_address(u"to@example.com")) + def test_deprecated_encrypt_by_default(self): - """Tests that depreacted values are still accepted.""" + """Tests that deprecated values are still accepted.""" for each in ['true', 'yes', '1']: acct = _AccountTestClass(address='foo@example.com', encrypt_by_default=each) diff --git a/tests/commands/thread_test.py b/tests/commands/thread_test.py index 315273c5..634c35e8 100644 --- a/tests/commands/thread_test.py +++ b/tests/commands/thread_test.py @@ -45,15 +45,27 @@ class Test_ensure_unique_address(unittest.TestCase): self.assertListEqual(actual, expected) +class _AccountTestClass(Account): + """Implements stubs for ABC methods.""" + + def send_mail(self, mail): + pass + + class TestClearMyAddress(unittest.TestCase): - me1 = 'me@example.com' - me2 = 'ME@example.com' - me_named = 'alot team ' - you = 'you@example.com' - named = 'somebody you know ' - imposter = 'alot team ' - mine = [me1, me2] + me1 = u'me@example.com' + me2 = u'ME@example.com' + me3 = u'me+label@example.com' + me4 = u'ME+label@example.com' + me_regex = r'me\+.*@example.com' + me_named = u'alot team ' + you = u'you@example.com' + named = u'somebody you know ' + imposter = u'alot team ' + mine = _AccountTestClass( + address=me1, aliases=[], alias_regexp=me_regex, case_sensitive_username=True) + def test_empty_input_returns_empty_list(self): self.assertListEqual( @@ -62,7 +74,7 @@ class TestClearMyAddress(unittest.TestCase): def test_only_my_emails_result_in_empty_list(self): expected = [] actual = thread.ReplyCommand.clear_my_address( - self.mine, self.mine+[self.me_named]) + self.mine, [self.me1, self.me3, self.me_named]) self.assertListEqual(actual, expected) def test_other_emails_are_untouched(self): @@ -72,22 +84,15 @@ class TestClearMyAddress(unittest.TestCase): self.assertListEqual(actual, expected) def test_case_matters(self): - expected = [self.me1] - mine = [self.me2] - actual = thread.ReplyCommand.clear_my_address(mine, expected) + input_ = [self.me1, self.me2, self.me3, self.me4] + expected = [self.me2, self.me4] + actual = thread.ReplyCommand.clear_my_address(self.mine, input_) self.assertListEqual(actual, expected) def test_same_address_with_different_real_name_is_removed(self): input_ = [self.me_named, self.you] - mine = [self.me1] expected = [self.you] - actual = thread.ReplyCommand.clear_my_address(mine, input_) - self.assertListEqual(actual, expected) - - def test_real_name_is_never_considered(self): - expected = [self.imposter] - mine = 'alot team' - actual = thread.ReplyCommand.clear_my_address(mine, expected) + actual = thread.ReplyCommand.clear_my_address(self.mine, input_) self.assertListEqual(actual, expected) -- cgit v1.2.3