Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix reply with quoted comma #76

Merged
merged 4 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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