"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Tuesday, 16th Jul, 2024

    OBJECTIVE:

        To be able to send out mails from code.

    REFERENCES:

        N/A

    DOWNLOADS:

        N/A

"""


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                   IMPORT                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")

# For working with mails:
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders

# For random strings:
import string
import random

# For system-level activities:
import os

# For working with files in RAM:
import io

# To work with Base64 encoding:
import base64

# To work with datatypes:
from typing import List


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                           MACROS / ONE-TIME INIT                                              ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                VARIABLES                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                  FUNCTIONS                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                   CLASSES                                                     ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


class GMailMessage:

    def __init__(
            self,
            from_email: str,
            to_email: str,
            subject: str,
            cc_emails: List[str] = None,
            bcc_emails: List[str] = None
    ):

        """
        Create an instance of the message that you would like to send.
        :param from_email: The EMail ID of the sender.
        :param to_email: The EMail ID of the recipient.
        :param subject: The subject of the mail.
        :param cc_emails: A list of recipients to add to the CC section.
        :param bcc_emails: A list of recipients to add to the BCC section.
        """

        # Create the instance of the message:
        self.message = MIMEMultipart()
        self.message["From"] = from_email
        self.message["To"] = to_email
        self.message["Subject"] = subject
        if cc_emails: self.message["CC"] = ",".join(cc_emails)
        if bcc_emails: self.message["BCC"] = ",".join(bcc_emails)

        # Note down the values for accessing later:
        self.__from = from_email
        self.__to = to_email
        self.__cc = cc_emails,
        self.__bcc = bcc_emails
        self.__subject = subject

    # ┏┓           •
    # ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏
    # ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛
    #       ┛

    @property
    def from_mail(self):
        return self.__from

    @property
    def to_mail(self):
        return self.__to

    @property
    def cc_mails(self):
        return self.__cc

    @property
    def bcc_mails(self):
        return self.__bcc

    @property
    def subject(self):
        return self.__subject

    # ┏┓ ┓ ┓  ┏┓
    # ┣┫┏┫┏┫  ┃ ┏┓┏┓╋┏┓┏┓╋
    # ┛┗┗┻┗┻  ┗┛┗┛┛┗┗┗ ┛┗┗

    def add_text(self, text):

        """
        Add plain-text to the mail body.
        :param text: The text to add to the mail body.
        :return: None.
        """

        self.message.attach(MIMEText(text, "plain"))

    def add_html(self, html_text):

        """
        Add HTML text to the mail body.
        :param html_text: The HTML text to add to the mail body.
        :return: None.
        """

        self.message.attach(MIMEText(html_text, "html"))

    def add_inline_image(
            self,
            image_file: str | io.BytesIO,
            file_name: str = None,
            content_id: str = None
    ):

        """
        Add an inline image to the body of the mail.
        NOTE: This is NOT the same as sending an image as an attachment.
        :param image_file: The image data to attach to the mail body.
        :param file_name: The name of the file. This is the same name by which it will be downloaded. You need not
            specify this if the input file is specified as a path. Needed when you give the input file as a buffer.
        :param content_id: Inline images are inserted via HTML bocks. This field identifies the image resource. If not
            specified, I will generate a random string. You may write a custom value here if you know what you are
            doing. For most use cases, please ignore this field.
        :return: None.
        """

        # Read the image as bytes:
        image_bytes = None
        if type(image_file) is str:
            file_name = file_name or os.path.split(image_file)[-1]
            with open(image_file, "rb") as opened_image_file:
                image_bytes = opened_image_file.read()
        if type(image_file) is io.BytesIO:
            image_file.seek(0)
            image_bytes = image_file.getvalue()

        # Declare the part to be attached to the multipart message:
        if image_bytes is not None:

            # Create the HTML block if the image pointer is blank:
            if content_id is None:
                content_id = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(8))
                self.add_html(f"""
                <html>
                    <body>
                        <p><img src="cid:{content_id}"></p>
                    </body>
                </html>
                """)

            # Then add the image:
            image_part = MIMEImage(image_bytes)
            image_part.add_header(
                "Content-ID", f"<{content_id}>"
            )
            image_part.add_header(
                "Content-Disposition",
                f"inline; filename=\"{file_name}\"",
            )
            self.message.attach(image_part)

    def add_attachment(
            self,
            attachment_file: str | io.BytesIO,
            file_name: str = None
    ):

        """
        Add a file as an attachment to the mail. This file, even if possible, will not be rendered on the screen in-line
        with the body. It will be made available as a download.
        :param attachment_file: The file that you would like to attach.
        :param file_name: The name of the file. This is the same name by which it will be downloaded. You need not
            specify this if the input file is specified as a path. Needed when you give the input file as a buffer.
        :return:
        """

        # Declare the part to be attached to the multipart message:
        part = MIMEBase("application", "octet-stream")

        # If the attachment is a file stored in the local disk:
        if isinstance(attachment_file, str):
            file_name = file_name or os.path.split(attachment_file)[-1]
            with open(attachment_file, "rb") as attachment:
                part.set_payload(attachment.read())

        # If the file is held in RAM:
        elif isinstance(attachment_file, io.BytesIO):
            attachment_file.seek(0)
            part.set_payload(attachment_file.read())

        # Encode and attach the file:
        encoders.encode_base64(part)
        part.add_header(
            "Content-Disposition",
            f"attachment; filename=\"{file_name}\"",
        )
        self.message.attach(part)

    # ┏┓
    # ┣ ┏┓┏┓┏┳┓┏┓╋
    # ┻ ┗┛┛ ┛┗┗┗┻┗

    def get_raw_message(
            self,
            as_base64: bool = True
    ):

        """
        Get the raw string dump from the current contents of the message. This text will be compliant with RFC 5322 and
        RFC 2045 (among others).
        :param as_base64: If set to True, the response will be a URL-safe B64 output, else it'll be a raw string.
        :return: The standardized raw text dump. either as a raw string or as a Base64 (url-safe) string.
        """

        if not as_base64: return self.message
        else: return base64.urlsafe_b64encode(self.message.as_bytes()).decode()


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                               MAIN PROGRAM                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


if __name__ == "__main__":

    my_mail = GMailMessage(
        from_email = "sender@gmail.com",
        to_email = "recipient@gmail.com",
        subject = "Bhopli is the best!",
        cc_emails = None,
        bcc_emails = None
    )
    my_mail.add_html(
        """
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8">
            <title>Sample HTML String</title>
            <style>
                .heading {
                    color: #ff9025;
                }
                .sub-heading {
                    color: #000000;
                }
            </style>
        </head>
        <body>
            <h1 class="heading">Hello, Bhopli!</h1>
            <h2 class="sub-heading">Bhopli is the best, most well-behaved cat in the known universe.</h2>
        </body>
        </html>
        """
    )
    my_mail.add_text("This is how you should pet her 👇")
    my_mail.add_inline_image(r"../../../data/images/cat_petting.png")
    my_mail.add_attachment(r"../../../data/pdf/sample_label.pdf")
    print(my_mail.get_raw_message(as_base64 = True))
