Skip to content

Commit ef6131d

Browse files
committed
branch_worker: implement "always_reply_to_author" flag
When forwarding PR comments via email it may be useful to include the patch submitter to the recipient list even if they are filtered out by provided allow/denylist configuration. Implement this behavior with a separate config flag: "email" { "pr_comments_forwarding": { "enabled": true, ... "always_reply_to_author": true } Signed-off-by: Ihor Solodrai <[email protected]>
1 parent b49cb95 commit ef6131d

File tree

4 files changed

+55
-22
lines changed

4 files changed

+55
-22
lines changed

kernel_patches_daemon/branch_worker.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import email
1212
import email.parser
1313
import email.policy
14+
import email.utils
1415
import hashlib
1516
import logging
1617
import os
@@ -378,6 +379,7 @@ def reply_email_recipients(
378379
msg: EmailMessage,
379380
allowlist: Optional[Sequence[re.Pattern]] = None,
380381
denylist: Optional[Sequence[re.Pattern]] = None,
382+
always_reply_to_author: bool = False,
381383
) -> Tuple[List[str], List[str]]:
382384
"""
383385
Extracts response recipients from the `msg`, applying allowlist/denylist.
@@ -386,18 +388,27 @@ def reply_email_recipients(
386388
msg: the EmailMessage we will be replying to
387389
allowlist: list of email address regexes to allow, ignored if empty
388390
denylist: list of email address regexes to deny, ignored if empty
391+
always_reply_to_author: if True, the patch author (message sender)
392+
will be added to the to_list even if filtered out by allowlist/denylist
389393
390394
Returns:
391395
(to_list, cc_list) - recipients of the reply email
392396
"""
397+
398+
sender_address = None
399+
to_list = []
400+
401+
sender = msg.get("From")
402+
if sender is not None:
403+
(_, sender_address) = email.utils.parseaddr(sender)
404+
if sender_address: # parseaddr might return an emptystring
405+
to_list.append(sender_address)
406+
393407
tos = msg.get_all("To", [])
394-
ccs = msg.get_all("Cc", [])
395-
# pyrefly: ignore # implicit-import
396-
cc_list = [a for (_, a) in email.utils.getaddresses(tos + ccs)]
408+
to_list += [a for (_, a) in email.utils.getaddresses(tos)]
397409

398-
# pyrefly: ignore # implicit-import, bad-argument-type
399-
(_, sender_address) = email.utils.parseaddr(msg.get("From"))
400-
to_list = [sender_address]
410+
ccs = msg.get_all("Cc", [])
411+
cc_list = [a for (_, a) in email.utils.getaddresses(ccs)]
401412

402413
if allowlist:
403414
cc_list = [a for a in cc_list if email_matches_any(a, allowlist)]
@@ -407,6 +418,9 @@ def reply_email_recipients(
407418
cc_list = [a for a in cc_list if not email_matches_any(a, denylist)]
408419
to_list = [a for a in to_list if not email_matches_any(a, denylist)]
409420

421+
if always_reply_to_author and sender_address and sender_address not in to_list:
422+
to_list.append(sender_address)
423+
410424
return (to_list, cc_list)
411425

412426

@@ -433,7 +447,10 @@ async def send_pr_comment_email(
433447
return
434448

435449
(to_list, cc_list) = reply_email_recipients(
436-
msg, allowlist=cfg.recipient_allowlist, denylist=cfg.recipient_denylist
450+
msg,
451+
allowlist=cfg.recipient_allowlist,
452+
denylist=cfg.recipient_denylist,
453+
always_reply_to_author=cfg.always_reply_to_author,
437454
)
438455
cc_list += cfg.always_cc
439456

kernel_patches_daemon/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def from_json(cls, json: Dict) -> "BranchConfig":
108108
class PRCommentsForwardingConfig:
109109
enabled: bool
110110
always_cc: List[str]
111+
always_reply_to_author: bool
111112
commenter_allowlist: List[str]
112113
recipient_allowlist: List[re.Pattern]
113114
recipient_denylist: List[re.Pattern]
@@ -117,6 +118,7 @@ def from_json(cls, json: Dict) -> "PRCommentsForwardingConfig":
117118
return cls(
118119
enabled=json.get("enabled", False),
119120
always_cc=json.get("always_cc", []),
121+
always_reply_to_author=json.get("always_reply_to_author", False),
120122
recipient_allowlist=[
121123
re.compile(pattern) for pattern in json.get("recipient_allowlist", [])
122124
],

tests/test_branch_worker.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
# pyre-unsafe
88

99
import email
10+
import email.parser
11+
import email.policy
1012
import json
1113
import os
1214
import random
@@ -42,7 +44,6 @@
4244
prs_for_the_same_series,
4345
reply_email_recipients,
4446
same_series_different_target,
45-
send_pr_comment_email,
4647
temporary_patch_file,
4748
UPSTREAM_REMOTE_NAME,
4849
)
@@ -1480,26 +1481,21 @@ def test_email_submitter_not_in_allowlist_and_allowlist_disabled(self):
14801481
self.assertEqual(expected_email, email)
14811482

14821483
def test_reply_email_recipients(self):
1483-
kpd_config_json = json.loads(read_fixture("kpd_config.json"))
1484-
kpd_config = KPDConfig.from_json(kpd_config_json)
1485-
self.assertIsNotNone(kpd_config)
1486-
14871484
mbox = read_test_data_file(
14881485
"test_sync_patches_pr_summary_success/series-970926.mbox"
14891486
)
1490-
# pyrefly: ignore # implicit-import
14911487
parser = email.parser.BytesParser(policy=email.policy.default)
14921488
msg = parser.parsebytes(mbox.encode("utf-8"), headersonly=True)
14931489
self.assertIsNotNone(mbox)
1494-
# pyrefly: ignore # missing-attribute
1495-
denylist = kpd_config.email.pr_comments_forwarding.recipient_denylist
1490+
denylist = [re.compile(".*@vger.kernel.org")]
14961491
(to_list, cc_list) = reply_email_recipients(msg, denylist=denylist)
14971492

1498-
self.assertEqual(to_list, ["[email protected]"])
1499-
self.assertEqual(len(cc_list), 17)
1493+
self.assertIn("[email protected]", to_list)
1494+
self.assertEqual(len(to_list), 17)
1495+
self.assertEqual(len(cc_list), 1)
15001496

1501-
# test allowlist by using the same denylist
1502-
(to_list, cc_list) = reply_email_recipients(msg, allowlist=denylist)
1497+
allowlist = [re.compile(".*@vger.kernel.org")]
1498+
(to_list, cc_list) = reply_email_recipients(msg, allowlist=allowlist)
15031499
self.assertEqual(to_list, [])
15041500
self.assertEqual(len(cc_list), 3)
15051501

@@ -1509,8 +1505,23 @@ def test_reply_email_recipients(self):
15091505
(to_list, cc_list) = reply_email_recipients(
15101506
msg, allowlist=allowlist, denylist=denylist
15111507
)
1508+
self.assertIn("[email protected]", to_list)
1509+
self.assertEqual(len(to_list), 3)
1510+
self.assertEqual(len(cc_list), 1)
1511+
1512+
# test denylist all
1513+
denylist = [re.compile(".*")]
1514+
(to_list, cc_list) = reply_email_recipients(msg, denylist=denylist)
1515+
self.assertEqual(to_list, [])
1516+
self.assertEqual(cc_list, [])
1517+
1518+
# test denylist all, but the sender
1519+
denylist = [re.compile(".*")]
1520+
(to_list, cc_list) = reply_email_recipients(
1521+
msg, denylist=denylist, always_reply_to_author=True
1522+
)
15121523
self.assertEqual(to_list, ["[email protected]"])
1513-
self.assertEqual(len(cc_list), 3)
1524+
self.assertEqual(cc_list, [])
15141525

15151526

15161527
class TestParsePrRef(unittest.TestCase):
@@ -1655,6 +1666,7 @@ async def test_forward_pr_comments(self, m: aioresponses) -> None:
16551666
self._bw.email_config = MagicMock(
16561667
pr_comments_forwarding=PRCommentsForwardingConfig(
16571668
enabled=True,
1669+
always_reply_to_author=False,
16581670
always_cc=["[email protected]"],
16591671
commenter_allowlist=["test_user"],
16601672
recipient_denylist=[re.compile(".*@vger.kernel.org")],
@@ -1702,9 +1714,10 @@ async def test_forward_pr_comments(self, m: aioresponses) -> None:
17021714
mock_send_email.call_args[0]
17031715
)
17041716

1705-
self.assertEqual(to_list, ["[email protected]"])
1706-
self.assertEqual(len(cc_list), 18)
1717+
self.assertIn("[email protected]", to_list)
1718+
self.assertEqual(len(to_list), 17)
17071719
self.assertIn("[email protected]", cc_list)
1720+
self.assertEqual(len(cc_list), 2)
17081721
self.assertEqual(
17091722
"Re: [PATCH bpf-next] bpf: clear user buf when bpf_d_path failed",
17101723
subject,

tests/test_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def test_valid(self) -> None:
175175
pr_comments_forwarding=PRCommentsForwardingConfig(
176176
enabled=True,
177177
always_cc=["[email protected]"],
178+
always_reply_to_author=False,
178179
commenter_allowlist=["kpd-bot[bot]"],
179180
recipient_denylist=[re.compile(".*@vger.kernel.org")],
180181
recipient_allowlist=[],

0 commit comments

Comments
 (0)