Skip to content

Commit

Permalink
Merge pull request #76 from The-Compiler/fix-reply-quoted-comma
Browse files Browse the repository at this point in the history
Fix reply with quoted comma
  • Loading branch information
akissinger authored Sep 17, 2024
2 parents c23cd27 + 780dfff commit 194fb49
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 35 deletions.
24 changes: 8 additions & 16 deletions dodo/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,8 @@ def __init__(self, a: app.Dodo, mode: str='', msg: Optional[dict]=None, parent:
self.message_string = ''

if msg:
senders: List[str] = []
recipients: List[str] = []

email_sep = re.compile(r'\s*,\s*')
if 'Reply-To' in msg['headers']:
senders.append(msg['headers']['Reply-To'])
elif 'From' in msg['headers']:
senders.append(msg["headers"]["From"])
if 'To' in msg['headers']:
recipients += email_sep.split(msg['headers']['To'])
if 'Cc' in msg['headers']:
recipients += email_sep.split(msg['headers']['Cc'])
senders = util.get_header_addresses(msg['headers'], ['From', 'Reply-To'])
recipients = util.get_header_addresses(msg['headers'], ['To', 'Cc'])

# Select current_account by checking which smtp_account's address
# is found first in the headers. Start with the recipient headers
Expand All @@ -98,7 +88,7 @@ def __init__(self, a: app.Dodo, mode: str='', msg: Optional[dict]=None, parent:
if isinstance(settings.email_address, dict):
self.current_account = next(
(
util.email_smtp_account_index(m) for m in
util.email_smtp_account_index(m) for _, m in
recipients + senders if
util.email_smtp_account_index(m) is not None
), 0)
Expand All @@ -121,15 +111,17 @@ def __init__(self, a: app.Dodo, mode: str='', msg: Optional[dict]=None, parent:
self.raw_message_string += '\n\n\n'

elif msg and (mode == 'reply' or mode == 'replyall'):
send_to = [e for e in senders + recipients if not util.email_is_me(e)]
send_to = [(name, e) for name, e in senders + recipients if not util.email_is_me(e)]

# put the first non-me email in To
if len(send_to) != 0:
self.raw_message_string += f'To: {send_to.pop(0)}\n'
to_value = email.utils.formataddr(send_to.pop(0))
self.raw_message_string += f'To: {to_value}\n'

# for replyall, put the rest of the emails in Cc
if len(send_to) != 0 and mode == 'replyall':
self.raw_message_string += f'Cc: {", ".join(send_to)}\n'
cc_values = [email.utils.formataddr(pair) for pair in send_to]
self.raw_message_string += f'Cc: {", ".join(cc_values)}\n'

if 'Subject' in msg['headers']:
subject = msg['headers']['Subject']
Expand Down
4 changes: 2 additions & 2 deletions dodo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
"""

from . import themes
from typing import Literal
from typing import Literal, Dict, Union

# functional
email_address = ''
email_address: Union[str, Dict[str, str]] = ''
"""Your email address (REQUIRED)
This is used both to populate the 'From' field of emails and to (mostly)
Expand Down
41 changes: 24 additions & 17 deletions dodo/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# along with Dodo. If not, see <https://www.gnu.org/licenses/>.

from __future__ import annotations
from typing import Iterator, List, Tuple, Dict, Union
from typing import Iterator, List, Tuple, Dict, Optional

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QKeyEvent
Expand Down Expand Up @@ -89,6 +89,19 @@ def linkify(s: str) -> str:
# get preference over email addresses
return lnk_email.linkify(lnk.linkify(s))


def get_header_addresses(
headers: Dict[str, str], header_keys: List[str]
) -> List[Tuple[str, str]]:
"""Extract realnames and email addresses from message headers.
The given header_keys are considered, e.g. ['From', 'Reply-To'] to get senders,
or ['To', 'Cc'] to get recipients.
"""
header_values = [headers[key] for key in header_keys if key in headers]
return email.utils.getaddresses(header_values)


html2html = lambda s : s
"""Function used to process HTML messages
Expand Down Expand Up @@ -271,11 +284,7 @@ def strip_email_address(e: str) -> str:
E.g. "First Last <me@domain.com>" -> "me@domain.com"
"""

# TODO proper handling of quoted strings
head = re.compile(r'^.*<')
tail = re.compile(r'>.*$')
return tail.sub('', head.sub('', e))
return email.utils.parseaddr(e)[1]

def email_is_me(e: str) -> bool:
"""Check whether the provided email is me
Expand All @@ -285,27 +294,25 @@ def email_is_me(e: str) -> bool:
:class:`dodo.compose.Compose` to filter out the user's own email when forming
a "reply-to-all" message.
"""

e_strip = strip_email_address(e)

if isinstance(settings.email_address, dict):
for v in settings.email_address.values():
if strip_email_address(v) == e_strip:
return True
addresses = [
strip_email_address(v) for v in settings.email_address.values()
]
else:
if strip_email_address(settings.email_address) == e_strip:
return True
addresses = [email.utils.parseaddr(settings.email_address)[1]]

return False
# nb: strip_email_address(e) is unnecessary with how this is used in compose.py,
# but doing it avoids a future footgun, and it is idempotent.
return strip_email_address(e) in addresses

def email_smtp_account_index(e: str) -> Union[int, None]:
def email_smtp_account_index(e: str) -> Optional[int]:
"""Index in settings.smtp_accounts of account having the provided email address
This method is used e.g. by :class:`dodo.compose.Compose` to autmatically
select the account to be used when replying to a mail. It returns the index
of first matching account or None if provided email does not match
any smtp account. """

assert isinstance(settings.email_address, dict)
return next(
(i for i, acc in enumerate(settings.smtp_accounts) if
strip_email_address(e) ==
Expand Down

0 comments on commit 194fb49

Please sign in to comment.