2121from collections import namedtuple
2222from contextlib import contextmanager
2323from datetime import datetime , timedelta , timezone
24- from email .mime . application import MIMEApplication
24+ from email .message import EmailMessage
2525from email .mime .multipart import MIMEMultipart
2626from email .mime .text import MIMEText
2727from enum import Enum
@@ -248,6 +248,11 @@ def bump_email_status_counters(status: Status):
248248
249249def generate_msg_id (host : str ) -> str :
250250 """Generate an email message ID based on the provided host."""
251+ # RFC 822 style message ID to allow for easier referencing of this
252+ # message. Note that it's not entirely correct for us to refer to a host
253+ # that is not entirely under our control, but we don't want to expose our
254+ # actual host name either. Collisions of a sha256 hash are assumed to be
255+ # unlikely in many contexts, so we do the same.
251256 checksum = hashlib .sha256 (str (time .time ()).encode ("utf-8" )).hexdigest ()
252257 return f"{ checksum } @{ host } "
253258
@@ -265,11 +270,13 @@ def email_in_submitter_allowlist(email: str, allowlist: Sequence[re.Pattern]) ->
265270
266271def build_email (
267272 config : EmailConfig ,
268- series : Series ,
273+ to_list : List [str ],
274+ cc_list : List [str ],
269275 subject : str ,
270276 msg_id : str ,
271277 body : str ,
272- boundary : str = "" ,
278+ boundary : Optional [str ] = None ,
279+ in_reply_to : Optional [str ] = None ,
273280) -> Tuple [List [str ], str ]:
274281 """
275282 Builds complete email (including headers) to be sent along with curl command
@@ -297,36 +304,24 @@ def build_email(
297304 "-" ,
298305 ]
299306
300- to_list = copy .copy (config .smtp_to )
301- cc_list = copy .copy (config .smtp_cc )
302-
303- if config .ignore_allowlist or email_in_submitter_allowlist (
304- series .submitter_email , config .submitter_allowlist
305- ):
306- to_list += [series .submitter_email ]
307-
308307 for to in to_list + cc_list :
309308 args += ["--mail-rcpt" , to ]
310309
311310 if config .smtp_http_proxy is not None :
312311 args += ["--proxy" , config .smtp_http_proxy ]
313312
314313 msg = MIMEMultipart ()
315- # Add some RFC 822 style message ID to allow for easier referencing of this
316- # message. Note that it's not entirely correct for us to refer to a host
317- # that is not entirely under our control, but we don't want to expose our
318- # actual host name either. Collisions of a sha256 hash are assumed to be
319- # unlikely in many contexts, so we do the same.
320314 msg ["Message-Id" ] = f"<{ msg_id } >"
321- msg ["In-Reply-To" ] = get_ci_base (series )["msgid" ]
315+ if in_reply_to is not None :
316+ msg ["In-Reply-To" ] = in_reply_to
322317 msg ["References" ] = msg ["In-Reply-To" ]
323318 msg ["Subject" ] = subject
324319 msg ["From" ] = config .smtp_from
325320 if to_list :
326321 msg ["To" ] = "," .join (to_list )
327322 if cc_list :
328323 msg ["Cc" ] = "," .join (cc_list )
329- if boundary :
324+ if boundary is not None :
330325 msg .set_boundary (boundary )
331326 msg .attach (MIMEText (body , "plain" ))
332327
@@ -335,14 +330,17 @@ def build_email(
335330
336331async def send_email (
337332 config : EmailConfig ,
338- series : Series ,
333+ to_list : List [str ],
334+ cc_list : List [str ],
339335 subject : str ,
340336 body : str ,
337+ in_reply_to : Optional [str ] = None ,
341338):
342339 """Send an email."""
343340 msg_id = generate_msg_id (config .smtp_host )
344- curl_args , msg = build_email (config , series , subject , msg_id , body )
345-
341+ curl_args , msg = build_email (
342+ config , to_list , cc_list , subject , msg_id , body , in_reply_to = in_reply_to
343+ )
346344 proc = await asyncio .create_subprocess_exec (
347345 * curl_args , stdin = PIPE , stdout = PIPE , stderr = PIPE
348346 )
@@ -353,6 +351,30 @@ async def send_email(
353351 email_send_fail_counter .add (1 )
354352
355353
354+ def ci_results_email_recipients (
355+ config : EmailConfig , series : Series
356+ ) -> Tuple [List [str ], List [str ]]:
357+ to_list = copy .copy (config .smtp_to )
358+ cc_list = copy .copy (config .smtp_cc )
359+ if config .ignore_allowlist or email_in_submitter_allowlist (
360+ series .submitter_email , config .submitter_allowlist
361+ ):
362+ to_list += [series .submitter_email ]
363+
364+ return (to_list , cc_list )
365+
366+
367+ async def send_ci_results_email (
368+ config : EmailConfig ,
369+ series : Series ,
370+ subject : str ,
371+ body : str ,
372+ ):
373+ (to_list , cc_list ) = ci_results_email_recipients (config , series )
374+ in_reply_to = get_ci_base (series )["msgid" ]
375+ await send_email (config , to_list , cc_list , subject , body , in_reply_to )
376+
377+
356378def pr_has_label (pr : PullRequest , label : str ) -> bool :
357379 for pr_label in pr .get_labels ():
358380 if pr_label .name == label :
@@ -559,7 +581,7 @@ def __init__(
559581 )
560582
561583 self .patchwork = patchwork
562- self .email = email
584+ self .email_config = email
563585
564586 self .log_extractor = log_extractor
565587 self .ci_repo_url = ci_repo_url
@@ -1204,8 +1226,7 @@ async def evaluate_ci_result(
12041226 self , status : Status , series : Series , pr : PullRequest , jobs : List [WorkflowJob ]
12051227 ) -> None :
12061228 """Evaluate the result of a CI run and send an email as necessary."""
1207- email = self .email
1208- if email is None :
1229+ if self .email_config is None :
12091230 logger .info ("No email configuration present; skipping sending..." )
12101231 return
12111232
@@ -1245,7 +1266,7 @@ async def evaluate_ci_result(
12451266 subject = await get_ci_email_subject (series )
12461267 ctx = build_email_body_context (self .repo , pr , status , series , inline_logs )
12471268 body = furnish_ci_email_body (ctx )
1248- await send_email ( email , series , subject , body )
1269+ await send_ci_results_email ( self . email_config , series , subject , body )
12491270 bump_email_status_counters (status )
12501271
12511272 def expire_branches (self ) -> None :
0 commit comments