|
63 | 63 | """
|
64 | 64 |
|
65 | 65 | import logging
|
| 66 | +import mimetypes |
66 | 67 | import os
|
67 | 68 | import smtplib
|
68 | 69 | import threading
|
@@ -531,68 +532,105 @@ def send(self) -> None:
|
531 | 532 | if self._config.scheduler
|
532 | 533 | else self._config.max_retry_sleep
|
533 | 534 | ),
|
534 |
| - subject=self._config.subject, |
535 |
| - html_body=self._config.message, |
536 |
| - to_emails=self._config.recipients, |
537 |
| - from_email=self._config.from_email, |
538 |
| - smtp_server=self._config.smtp_server, |
539 |
| - smtp_port=self._config.smtp_port, |
540 |
| - smtp_user=self._config.smtp_user, |
541 |
| - smtp_password=self._config.smtp_password, |
542 |
| - attachments=self._config.attachments, |
| 535 | + config=self._config, |
543 | 536 | )
|
544 | 537 |
|
545 |
| - @staticmethod |
546 |
| - def _send_email( # pylint: disable=too-many-arguments |
547 |
| - *, |
548 |
| - subject: str, |
549 |
| - html_body: str, |
550 |
| - to_emails: list[str], |
551 |
| - from_email: str, |
552 |
| - smtp_server: str, |
553 |
| - smtp_port: int, |
554 |
| - smtp_user: str, |
555 |
| - smtp_password: str, |
556 |
| - attachments: list[str] | None = None, |
| 538 | + def _send_email( |
| 539 | + self, |
| 540 | + config: EmailConfig, |
557 | 541 | ) -> None:
|
558 | 542 | """Send an HTML email alert with optional attachments.
|
559 | 543 |
|
560 | 544 | Args:
|
561 |
| - subject: Email subject. |
562 |
| - html_body: HTML body content for the email. |
| 545 | + config: Email configuration object. |
| 546 | + """ |
| 547 | + msg = EmailMessage() |
| 548 | + msg["From"] = config.from_email |
| 549 | + msg["To"] = ", ".join(config.recipients) |
| 550 | + msg["Subject"] = config.subject |
| 551 | + msg.add_alternative(config.message, subtype="html") |
| 552 | + |
| 553 | + if config.attachments: |
| 554 | + self._attach_files(msg, config.attachments) |
| 555 | + |
| 556 | + smtp_settings: dict[str, str | int] = { |
| 557 | + "server": config.smtp_server, |
| 558 | + "port": config.smtp_port, |
| 559 | + "user": config.smtp_user, |
| 560 | + "password": config.smtp_password, |
| 561 | + } |
| 562 | + self._connect_and_send(msg, smtp_settings, config.recipients) |
| 563 | + |
| 564 | + def _attach_files(self, msg: EmailMessage, attachments: list[str]) -> None: |
| 565 | + """Attach files to the email. |
| 566 | +
|
| 567 | + Args: |
| 568 | + msg: EmailMessage object. |
| 569 | + attachments: List of file paths to attach. |
| 570 | + """ |
| 571 | + failed_attachments = [] |
| 572 | + for file in attachments: |
| 573 | + try: |
| 574 | + with open(file, "rb") as f: |
| 575 | + maintype, subtype = self._get_mime_type(file) |
| 576 | + msg.add_attachment( |
| 577 | + f.read(), |
| 578 | + maintype=maintype, |
| 579 | + subtype=subtype, |
| 580 | + filename=os.path.basename(file), |
| 581 | + ) |
| 582 | + except OSError as e: |
| 583 | + failed_attachments.append(file) |
| 584 | + _log.error("Failed to attach file %s: %s", file, e) |
| 585 | + if failed_attachments: |
| 586 | + _log.warning( |
| 587 | + "The following attachments could not be added: %s", failed_attachments |
| 588 | + ) |
| 589 | + |
| 590 | + @staticmethod |
| 591 | + def _get_mime_type(file: str) -> tuple[str, str]: |
| 592 | + """Determine the MIME type of a file with fallback. |
| 593 | +
|
| 594 | + Args: |
| 595 | + file: Path to the file. |
| 596 | +
|
| 597 | + Returns: |
| 598 | + A tuple containing the MIME type (maintype, subtype). |
| 599 | + """ |
| 600 | + mime_type, _ = mimetypes.guess_type(file) |
| 601 | + if mime_type: |
| 602 | + maintype, subtype = mime_type.split("/") |
| 603 | + else: |
| 604 | + # generic fallback logic |
| 605 | + if file.endswith((".csv", ".txt", ".log")): |
| 606 | + maintype, subtype = "text", "plain" |
| 607 | + elif file.endswith((".jpg", ".jpeg", ".png", ".gif", ".bmp")): |
| 608 | + maintype, subtype = "image", "png" |
| 609 | + else: |
| 610 | + # default: binary file fallback |
| 611 | + maintype, subtype = "application", "octet-stream" |
| 612 | + return maintype, subtype |
| 613 | + |
| 614 | + @staticmethod |
| 615 | + def _connect_and_send( |
| 616 | + msg: EmailMessage, smtp_settings: dict[str, str | int], to_emails: list[str] |
| 617 | + ) -> None: |
| 618 | + """Send the email via SMTP. |
| 619 | +
|
| 620 | + Args: |
| 621 | + msg: EmailMessage object containing the email content. |
| 622 | + smtp_settings: SMTP server configuration. |
563 | 623 | to_emails: List of recipient email addresses.
|
564 |
| - from_email: Sender email address. |
565 |
| - smtp_server: SMTP server address. |
566 |
| - smtp_port: SMTP server port. |
567 |
| - smtp_user: SMTP login username. |
568 |
| - smtp_password: SMTP login password. |
569 |
| - attachments: List of files to attach. |
570 | 624 |
|
571 | 625 | Raises:
|
572 | 626 | SMTPException: If the email fails to send.
|
573 | 627 | """
|
574 |
| - msg = EmailMessage() |
575 |
| - msg["From"] = from_email |
576 |
| - msg["To"] = ", ".join(to_emails) |
577 |
| - msg["Subject"] = subject |
578 |
| - msg.add_alternative(html_body, subtype="html") |
579 |
| - |
580 |
| - if attachments: |
581 |
| - for file in attachments: |
582 |
| - try: |
583 |
| - with open(file, "rb") as f: |
584 |
| - msg.add_attachment( |
585 |
| - f.read(), |
586 |
| - subtype="octet-stream", |
587 |
| - filename=os.path.basename(file), |
588 |
| - ) |
589 |
| - except OSError as e: |
590 |
| - _log.error("Failed to attach file %s: %s", file, e) |
591 |
| - |
592 | 628 | try:
|
593 |
| - with smtplib.SMTP(smtp_server, smtp_port) as server: |
| 629 | + with smtplib.SMTP( |
| 630 | + str(smtp_settings["server"]), int(smtp_settings["port"]) |
| 631 | + ) as server: |
594 | 632 | server.starttls()
|
595 |
| - server.login(smtp_user, smtp_password) |
| 633 | + server.login(str(smtp_settings["user"]), str(smtp_settings["password"])) |
596 | 634 | server.send_message(msg)
|
597 | 635 | _log.info("Email sent successfully to %s", to_emails)
|
598 | 636 | except SMTPException as e:
|
|
0 commit comments