dhisana 0.0.1.dev278__py3-none-any.whl → 0.0.1.dev280__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,64 @@ from dhisana.schemas.sales import MessageItem
15
15
  from dhisana.utils.email_body_utils import body_variants
16
16
 
17
17
 
18
+ def _normalize_recipient(recipient: Any) -> Optional[Dict[str, str]]:
19
+ if recipient is None:
20
+ return None
21
+ if isinstance(recipient, dict):
22
+ email = recipient.get("email") or recipient.get("address")
23
+ name = recipient.get("name")
24
+ else:
25
+ email = getattr(recipient, "email", None) or getattr(recipient, "address", None)
26
+ name = getattr(recipient, "name", None)
27
+ if isinstance(recipient, str):
28
+ email = recipient
29
+ name = None
30
+ if not email:
31
+ return None
32
+ return {"email": email, "name": name}
33
+
34
+
35
+ def _normalize_recipients(recipients: Optional[List[Any]]) -> List[Dict[str, str]]:
36
+ normalized: List[Dict[str, str]] = []
37
+ for recipient in recipients or []:
38
+ item = _normalize_recipient(recipient)
39
+ if item:
40
+ normalized.append(item)
41
+ return normalized
42
+
43
+
44
+ def _dedupe_recipients(
45
+ recipients: List[Dict[str, str]],
46
+ exclude_emails: Optional[List[str]] = None,
47
+ ) -> List[Dict[str, str]]:
48
+ excluded = {email.lower() for email in (exclude_emails or []) if email}
49
+ seen: set[str] = set()
50
+ result: List[Dict[str, str]] = []
51
+ for recipient in recipients:
52
+ email = (recipient.get("email") or "").strip()
53
+ if not email:
54
+ continue
55
+ email_lc = email.lower()
56
+ if email_lc in seen or email_lc in excluded:
57
+ continue
58
+ seen.add(email_lc)
59
+ result.append({"email": email, "name": recipient.get("name")})
60
+ return result
61
+
62
+
63
+ def _recipients_to_m365(recipients: List[Dict[str, str]]) -> List[Dict[str, Any]]:
64
+ result: List[Dict[str, Any]] = []
65
+ for recipient in recipients:
66
+ email = recipient.get("email")
67
+ if not email:
68
+ continue
69
+ payload = {"emailAddress": {"address": email}}
70
+ name = recipient.get("name")
71
+ if name:
72
+ payload["emailAddress"]["name"] = name
73
+ result.append(payload)
74
+ return result
75
+
18
76
  def get_microsoft365_access_token(tool_config: Optional[List[Dict]] = None) -> str:
19
77
  """
20
78
  Retrieve a Microsoft Graph OAuth2 access token from tool_config or env.
@@ -349,6 +407,14 @@ async def reply_to_email_m365_async(
349
407
  base_res = _base_resource(reply_email_context.sender_email, tool_config, auth_mode)
350
408
  headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
351
409
 
410
+ reply_type = (reply_email_context.reply_type or "reply").lower()
411
+ if reply_type not in {"reply", "reply_all", "forward"}:
412
+ reply_type = "reply"
413
+
414
+ explicit_to = _normalize_recipients(reply_email_context.to_recipients)
415
+ explicit_cc = _normalize_recipients(reply_email_context.cc_recipients)
416
+ explicit_bcc = _normalize_recipients(reply_email_context.bcc_recipients)
417
+
352
418
  # 1) Fetch original message for context (subject, recipients, thread)
353
419
  async with httpx.AsyncClient(timeout=30) as client:
354
420
  get_url = f"{base_url}{base_res}/messages/{reply_email_context.message_id}"
@@ -357,68 +423,148 @@ async def reply_to_email_m365_async(
357
423
  orig = get_resp.json()
358
424
 
359
425
  orig_subject = orig.get("subject", "")
360
- subject = orig_subject if orig_subject.startswith("Re:") else f"Re: {orig_subject}"
361
426
  thread_id = orig.get("conversationId", "")
362
427
  cc_list = orig.get("ccRecipients", [])
363
428
  to_list = orig.get("toRecipients", [])
429
+ from_addr = (orig.get("from", {}) or {}).get("emailAddress", {}) or {}
430
+ from_email = from_addr.get("address", "")
431
+ from_name = from_addr.get("name")
432
+
364
433
  sender_email_lc = (reply_email_context.sender_email or "").lower()
365
434
 
366
435
  def _is_self(addr: str) -> bool:
367
436
  return bool(sender_email_lc) and sender_email_lc in addr.lower()
368
437
 
369
- def _addresses(recipients: List[Dict[str, Any]]) -> List[str]:
370
- return [
371
- (recipient.get("emailAddress", {}) or {}).get("address", "")
372
- for recipient in recipients
373
- if recipient
374
- ]
438
+ def _address_dicts(recipients: List[Dict[str, Any]]) -> List[Dict[str, str]]:
439
+ result: List[Dict[str, str]] = []
440
+ for recipient in recipients or []:
441
+ email_address = recipient.get("emailAddress", {}) if recipient else {}
442
+ address = email_address.get("address")
443
+ if not address:
444
+ continue
445
+ result.append({"email": address, "name": email_address.get("name")})
446
+ return result
447
+
448
+ to_recipients: List[Dict[str, str]] = []
449
+ cc_recipients: List[Dict[str, str]] = []
450
+ bcc_recipients: List[Dict[str, str]] = []
451
+
452
+ if reply_type == "forward":
453
+ if not explicit_to:
454
+ raise ValueError("Forward requires explicit to_recipients.")
455
+ to_recipients = _dedupe_recipients(explicit_to)
456
+ cc_recipients = _dedupe_recipients(explicit_cc)
457
+ bcc_recipients = _dedupe_recipients(explicit_bcc)
458
+
459
+ if reply_email_context.subject:
460
+ subject = reply_email_context.subject
461
+ elif orig_subject and not orig_subject.lower().startswith("fwd:"):
462
+ subject = f"Fwd: {orig_subject}"
463
+ else:
464
+ subject = orig_subject or "Fwd:"
465
+ else:
466
+ subject = orig_subject if orig_subject.startswith("Re:") else f"Re: {orig_subject}"
375
467
 
376
- to_addresses = ", ".join(
377
- [addr for addr in _addresses(to_list) if addr and not _is_self(addr)]
378
- )
379
- cc_addresses = ", ".join(
380
- [addr for addr in _addresses(cc_list) if addr and not _is_self(addr)]
381
- )
468
+ if reply_type == "reply_all":
469
+ base_to = _dedupe_recipients(
470
+ [{"email": from_email, "name": from_name}] if from_email and not _is_self(from_email) else [],
471
+ exclude_emails=[reply_email_context.sender_email],
472
+ )
473
+ if not base_to:
474
+ base_to = _dedupe_recipients(
475
+ _address_dicts(to_list),
476
+ exclude_emails=[reply_email_context.sender_email],
477
+ )
478
+ if not base_to and reply_email_context.fallback_recipient and not _is_self(reply_email_context.fallback_recipient):
479
+ base_to = _dedupe_recipients([{"email": reply_email_context.fallback_recipient}])
480
+ if not base_to:
481
+ raise ValueError(
482
+ "No valid recipient found in the original message; refusing to reply to sender."
483
+ )
484
+ base_to_emails = [recipient.get("email") for recipient in base_to if recipient.get("email")]
485
+ base_cc_candidates = _address_dicts(to_list) + _address_dicts(cc_list)
486
+ base_cc = _dedupe_recipients(
487
+ base_cc_candidates,
488
+ exclude_emails=[reply_email_context.sender_email] + base_to_emails,
489
+ )
490
+ else:
491
+ base_to = _dedupe_recipients(
492
+ [{"email": from_email, "name": from_name}] if from_email and not _is_self(from_email) else [],
493
+ exclude_emails=[reply_email_context.sender_email],
494
+ )
495
+ if not base_to:
496
+ base_to = _dedupe_recipients(
497
+ _address_dicts(to_list),
498
+ exclude_emails=[reply_email_context.sender_email],
499
+ )
500
+ base_cc = _dedupe_recipients(
501
+ _address_dicts(cc_list),
502
+ exclude_emails=[reply_email_context.sender_email],
503
+ )
504
+ if not base_to and reply_email_context.fallback_recipient and not _is_self(reply_email_context.fallback_recipient):
505
+ base_to = _dedupe_recipients([{"email": reply_email_context.fallback_recipient}])
506
+ base_cc = []
507
+ if not base_to:
508
+ raise ValueError(
509
+ "No valid recipient found in the original message; refusing to reply to sender."
510
+ )
511
+
512
+ to_recipients = base_to
513
+ if explicit_to:
514
+ to_recipients = _dedupe_recipients(explicit_to)
515
+
516
+ cc_recipients = base_cc
517
+ if explicit_cc:
518
+ cc_recipients = _dedupe_recipients(
519
+ base_cc + explicit_cc,
520
+ exclude_emails=[recipient.get("email") for recipient in to_recipients if recipient.get("email")],
521
+ )
522
+
523
+ bcc_recipients = _dedupe_recipients(explicit_bcc)
524
+
525
+ to_addresses = ", ".join([recipient.get("email") for recipient in to_recipients if recipient.get("email")])
526
+ cc_addresses = ", ".join([recipient.get("email") for recipient in cc_recipients if recipient.get("email")])
382
527
 
383
- all_recipients = [addr for addr in _addresses(to_list + cc_list) if addr]
384
- if not any(all_recipients):
385
- from_addr = orig.get("from", {}).get("emailAddress", {})
386
- from_address = from_addr.get("address", "")
387
- if from_address:
388
- all_recipients.append(from_address)
389
-
390
- non_self_recipients = [addr for addr in all_recipients if not _is_self(addr)]
391
- if not non_self_recipients and reply_email_context.fallback_recipient:
392
- fr = reply_email_context.fallback_recipient
393
- if fr and not _is_self(fr):
394
- non_self_recipients.append(fr)
395
-
396
- if not to_addresses and non_self_recipients:
397
- to_addresses = ", ".join(non_self_recipients)
398
- cc_addresses = ""
399
-
400
- if not non_self_recipients:
401
- raise httpx.HTTPStatusError(
402
- "No valid recipient found in the original message; refusing to reply to sender.",
403
- request=get_resp.request,
404
- response=get_resp,
528
+ if not to_addresses:
529
+ raise ValueError(
530
+ "No valid recipient found in the original message; refusing to reply to sender."
405
531
  )
406
532
 
407
- # 2) Create reply-all draft with comment
408
- create_reply_url = (
409
- f"{base_url}{base_res}/messages/{reply_email_context.message_id}/createReplyAll"
410
- )
533
+ # 2) Create draft with comment
534
+ if reply_type == "forward":
535
+ create_reply_url = (
536
+ f"{base_url}{base_res}/messages/{reply_email_context.message_id}/createForward"
537
+ )
538
+ elif reply_type == "reply":
539
+ create_reply_url = (
540
+ f"{base_url}{base_res}/messages/{reply_email_context.message_id}/createReply"
541
+ )
542
+ else:
543
+ create_reply_url = (
544
+ f"{base_url}{base_res}/messages/{reply_email_context.message_id}/createReplyAll"
545
+ )
411
546
  create_payload = {"comment": reply_email_context.reply_body}
412
547
  create_resp = await client.post(create_reply_url, headers=headers, json=create_payload)
413
548
  create_resp.raise_for_status()
414
549
  reply_msg = create_resp.json()
415
550
  reply_id = reply_msg.get("id")
416
551
 
417
- # 3) Optionally add categories (labels) to the reply draft
552
+ # 3) Optionally update recipients/subject and add categories (labels) to the draft
553
+ patch_payload: Dict[str, Any] = {}
554
+ if to_recipients:
555
+ patch_payload["toRecipients"] = _recipients_to_m365(to_recipients)
556
+ if cc_recipients:
557
+ patch_payload["ccRecipients"] = _recipients_to_m365(cc_recipients)
558
+ if bcc_recipients:
559
+ patch_payload["bccRecipients"] = _recipients_to_m365(bcc_recipients)
560
+ if reply_type == "forward":
561
+ patch_payload["subject"] = subject
418
562
  if reply_email_context.add_labels:
419
- patch_url = f"{base_url}{base_res}/messages/{reply_id}"
420
563
  categories = list(set((reply_msg.get("categories") or []) + reply_email_context.add_labels))
421
- await client.patch(patch_url, headers=headers, json={"categories": categories})
564
+ patch_payload["categories"] = categories
565
+ if patch_payload:
566
+ patch_url = f"{base_url}{base_res}/messages/{reply_id}"
567
+ await client.patch(patch_url, headers=headers, json=patch_payload)
422
568
 
423
569
  # 4) Send the reply
424
570
  send_url = f"{base_url}{base_res}/messages/{reply_id}/send"
@@ -448,6 +594,8 @@ async def reply_to_email_m365_async(
448
594
  "email_subject": subject,
449
595
  "email_sender": reply_email_context.sender_email,
450
596
  "email_recipients": [to_addresses] + ([cc_addresses] if cc_addresses else []),
597
+ "to_recipients": to_recipients,
598
+ "cc_recipients": cc_recipients,
451
599
  "read_email_status": "READ" if str(reply_email_context.mark_as_read).lower() == "true" else "UNREAD",
452
600
  "email_labels": email_labels,
453
601
  }
@@ -39,6 +39,69 @@ from dhisana.utils.email_body_utils import body_variants
39
39
  # Helper / Utility
40
40
  # --------------------------------------------------------------------------- #
41
41
 
42
+ def _normalize_recipient(recipient: Any) -> Optional[Dict[str, str]]:
43
+ if recipient is None:
44
+ return None
45
+ if isinstance(recipient, dict):
46
+ email_value = recipient.get("email") or recipient.get("address")
47
+ name = recipient.get("name")
48
+ else:
49
+ email_value = getattr(recipient, "email", None) or getattr(recipient, "address", None)
50
+ name = getattr(recipient, "name", None)
51
+ if isinstance(recipient, str):
52
+ email_value = recipient
53
+ name = None
54
+ if not email_value:
55
+ return None
56
+ return {"email": email_value, "name": name}
57
+
58
+
59
+ def _normalize_recipients(recipients: Optional[List[Any]]) -> List[Dict[str, str]]:
60
+ normalized: List[Dict[str, str]] = []
61
+ for recipient in recipients or []:
62
+ item = _normalize_recipient(recipient)
63
+ if item:
64
+ normalized.append(item)
65
+ return normalized
66
+
67
+
68
+ def _parse_header_recipients(header_value: str) -> List[Dict[str, str]]:
69
+ parsed = email.utils.getaddresses([header_value]) if header_value else []
70
+ return [{"email": address, "name": name} for name, address in parsed if address]
71
+
72
+
73
+ def _dedupe_recipients(
74
+ recipients: List[Dict[str, str]],
75
+ exclude_emails: Optional[List[str]] = None,
76
+ ) -> List[Dict[str, str]]:
77
+ excluded = {email_value.lower() for email_value in (exclude_emails or []) if email_value}
78
+ seen: set[str] = set()
79
+ result: List[Dict[str, str]] = []
80
+ for recipient in recipients:
81
+ email_value = (recipient.get("email") or "").strip()
82
+ if not email_value:
83
+ continue
84
+ email_lc = email_value.lower()
85
+ if email_lc in seen or email_lc in excluded:
86
+ continue
87
+ seen.add(email_lc)
88
+ result.append({"email": email_value, "name": recipient.get("name")})
89
+ return result
90
+
91
+
92
+ def _recipients_to_header(recipients: List[Dict[str, str]]) -> str:
93
+ parts: List[str] = []
94
+ for recipient in recipients:
95
+ name = recipient.get("name")
96
+ email_value = recipient.get("email")
97
+ if not email_value:
98
+ continue
99
+ if name:
100
+ parts.append(f"{name} <{email_value}>")
101
+ else:
102
+ parts.append(email_value)
103
+ return ", ".join(parts)
104
+
42
105
 
43
106
  def _decode_header_value(value: Any) -> str:
44
107
  """Return a unicode string for an e-mail header field."""
@@ -475,7 +538,7 @@ async def reply_to_email_via_smtp_async(
475
538
  if status == "OK" and nums and nums[0]:
476
539
  num = nums[0].split()[0]
477
540
  _, data = conn.fetch(num, "(RFC822)")
478
- if ctx.mark_as_read.lower() == "true":
541
+ if str(ctx.mark_as_read).lower() == "true":
479
542
  conn.store(num, "+FLAGS", "\\Seen")
480
543
  msg_data = data[0][1] if data and data[0] else None
481
544
  break
@@ -496,28 +559,129 @@ async def reply_to_email_via_smtp_async(
496
559
 
497
560
  raw_original = await asyncio.to_thread(_fetch_original)
498
561
  if raw_original is None:
499
- raise RuntimeError(f"Could not locate original message with ID {ctx.message_id!r}")
562
+ raise ValueError(f"Could not locate original message with ID {ctx.message_id!r}")
500
563
 
501
564
  original = email.message_from_bytes(raw_original)
502
565
  hdr = lambda h: original.get(h, "")
503
566
 
504
- # 2. Derive reply headers
505
- to_addrs = hdr("Reply-To") or hdr("From")
506
- cc_addrs = hdr("Cc")
507
- # If the derived recipient points back to the sender or is missing, fall back to provided recipient.
567
+ reply_type = (ctx.reply_type or "reply").lower()
568
+ if reply_type not in {"reply", "reply_all", "forward"}:
569
+ reply_type = "reply"
570
+
508
571
  sender_email_lc = (ctx.sender_email or "").lower()
572
+
509
573
  def _is_self(addr: str) -> bool:
510
574
  return bool(sender_email_lc) and sender_email_lc in addr.lower()
511
- if (not to_addrs or _is_self(to_addrs)) and getattr(ctx, "fallback_recipient", None):
512
- fr = ctx.fallback_recipient
513
- if fr and not _is_self(fr):
514
- to_addrs = fr
515
- cc_addrs = ""
516
- if not to_addrs or _is_self(to_addrs):
517
- raise RuntimeError("No valid recipient found in original message; refusing to reply to sender.")
518
- subject = hdr("Subject")
519
- if not subject.lower().startswith("re:"):
520
- subject = f"Re: {subject}"
575
+
576
+ explicit_to = _normalize_recipients(getattr(ctx, "to_recipients", None))
577
+ explicit_cc = _normalize_recipients(getattr(ctx, "cc_recipients", None))
578
+ explicit_bcc = _normalize_recipients(getattr(ctx, "bcc_recipients", None))
579
+
580
+ to_recipients: List[Dict[str, str]] = []
581
+ cc_recipients: List[Dict[str, str]] = []
582
+ bcc_recipients: List[Dict[str, str]] = []
583
+
584
+ from_header = hdr("From")
585
+ reply_to_header = hdr("Reply-To")
586
+ to_header = hdr("To")
587
+ cc_header = hdr("Cc")
588
+
589
+ if reply_type == "forward":
590
+ if not explicit_to:
591
+ raise ValueError("Forward requires explicit to_recipients.")
592
+ to_recipients = _dedupe_recipients(explicit_to)
593
+ cc_recipients = _dedupe_recipients(explicit_cc)
594
+ bcc_recipients = _dedupe_recipients(explicit_bcc)
595
+ # Respect custom subject if explicitly provided; otherwise derive from original
596
+ if ctx.subject:
597
+ subject = ctx.subject
598
+ else:
599
+ subject = hdr("Subject")
600
+ if subject and not subject.lower().startswith("fwd:"):
601
+ subject = f"Fwd: {subject}"
602
+ elif not subject:
603
+ subject = "Fwd:"
604
+ else:
605
+ if reply_type == "reply_all":
606
+ base_to = _dedupe_recipients(
607
+ _parse_header_recipients(from_header),
608
+ exclude_emails=[ctx.sender_email],
609
+ )
610
+ if not base_to:
611
+ base_to = _dedupe_recipients(
612
+ _parse_header_recipients(reply_to_header),
613
+ exclude_emails=[ctx.sender_email],
614
+ )
615
+ if not base_to:
616
+ base_to = _dedupe_recipients(
617
+ _parse_header_recipients(to_header),
618
+ exclude_emails=[ctx.sender_email],
619
+ )
620
+ if not base_to and getattr(ctx, "fallback_recipient", None):
621
+ fr = ctx.fallback_recipient
622
+ if fr and not _is_self(fr):
623
+ base_to = _dedupe_recipients([{"email": fr}])
624
+ if not base_to:
625
+ raise ValueError("No valid recipient found in original message; refusing to reply to sender.")
626
+ base_to_emails = [recipient.get("email") for recipient in base_to if recipient.get("email")]
627
+ base_cc_candidates = _parse_header_recipients(to_header) + _parse_header_recipients(cc_header)
628
+ base_cc = _dedupe_recipients(
629
+ base_cc_candidates,
630
+ exclude_emails=[ctx.sender_email] + base_to_emails,
631
+ )
632
+ else:
633
+ cc_header_value = cc_header or ""
634
+ if reply_to_header and not _is_self(reply_to_header):
635
+ to_header_value = reply_to_header
636
+ elif from_header and not _is_self(from_header):
637
+ to_header_value = from_header
638
+ elif to_header and not _is_self(to_header):
639
+ to_header_value = to_header
640
+ else:
641
+ to_header_value = ", ".join([v for v in (to_header, cc_header, from_header) if v])
642
+ cc_header_value = ""
643
+
644
+ base_to = _dedupe_recipients(
645
+ _parse_header_recipients(to_header_value),
646
+ exclude_emails=[ctx.sender_email],
647
+ )
648
+ base_cc = _dedupe_recipients(
649
+ _parse_header_recipients(cc_header_value),
650
+ exclude_emails=[ctx.sender_email],
651
+ )
652
+
653
+ if not base_to and getattr(ctx, "fallback_recipient", None):
654
+ fr = ctx.fallback_recipient
655
+ if fr and not _is_self(fr):
656
+ base_to = _dedupe_recipients([{"email": fr}])
657
+ base_cc = []
658
+
659
+ if not base_to:
660
+ raise ValueError("No valid recipient found in original message; refusing to reply to sender.")
661
+
662
+ to_recipients = base_to
663
+ if explicit_to:
664
+ to_recipients = _dedupe_recipients(explicit_to)
665
+
666
+ cc_recipients = base_cc
667
+ if explicit_cc:
668
+ cc_recipients = _dedupe_recipients(
669
+ base_cc + explicit_cc,
670
+ exclude_emails=[recipient.get("email") for recipient in to_recipients if recipient.get("email")],
671
+ )
672
+
673
+ bcc_recipients = _dedupe_recipients(explicit_bcc)
674
+
675
+ subject = hdr("Subject")
676
+ if not subject.lower().startswith("re:"):
677
+ subject = f"Re: {subject}"
678
+
679
+ to_addrs = _recipients_to_header(to_recipients)
680
+ cc_addrs = _recipients_to_header(cc_recipients)
681
+ bcc_addrs = _recipients_to_header(bcc_recipients)
682
+ if not to_addrs:
683
+ raise ValueError("No valid recipient found in original message; refusing to reply to sender.")
684
+
521
685
  orig_msg_id = hdr("Message-ID") # parent's ID
522
686
 
523
687
  # Build the References header by appending the parent's ID
@@ -529,13 +693,17 @@ async def reply_to_email_via_smtp_async(
529
693
 
530
694
  # 3. Build the MIMEText reply
531
695
  msg = MIMEText(ctx.reply_body, _charset="utf-8")
532
- msg["From"] = f"{ctx.sender_name} <{ctx.sender_email}>"
696
+ sender_display = ctx.sender_name or ctx.sender_email
697
+ msg["From"] = f"{sender_display} <{ctx.sender_email}>"
533
698
  msg["To"] = to_addrs
534
699
  if cc_addrs:
535
700
  msg["Cc"] = cc_addrs
701
+ if bcc_addrs:
702
+ msg["Bcc"] = bcc_addrs
536
703
  msg["Subject"] = subject
537
- msg["In-Reply-To"] = orig_msg_id
538
- msg["References"] = references
704
+ if reply_type != "forward":
705
+ msg["In-Reply-To"] = orig_msg_id
706
+ msg["References"] = references
539
707
 
540
708
  # Generate a new Message-ID for this reply
541
709
  domain_part = ctx.sender_email.split("@", 1)[-1] or "local"
@@ -571,6 +739,8 @@ async def reply_to_email_via_smtp_async(
571
739
  "email_subject": subject,
572
740
  "email_sender": ctx.sender_email,
573
741
  "email_recipients": recipients,
574
- "read_email_status": "READ" if ctx.mark_as_read.lower() == "true" else "UNREAD",
742
+ "to_recipients": to_recipients,
743
+ "cc_recipients": cc_recipients,
744
+ "read_email_status": "READ" if str(ctx.mark_as_read).lower() == "true" else "UNREAD",
575
745
  "email_labels": [], # Not applicable for IMAP
576
746
  }