Repository: Significant-Gravitas/Auto-GPT-Plugins (language: python)

Issue:
feat: add the scaffold for twitter



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 3532ee46..c898e2d1 100644
--- a/README.md
+++ b/README.md
@@ -60,16 +60,18 @@ For interactionless use, set `ALLOWLISTED_PLUGINS=example-plugin1,example-plugin
 |---------|-------------|----------|
 | Twitter | AutoGPT is capable of retrieving Twitter posts and other related content by accessing the Twitter platform via the v1.1 API using Tweepy.| [autogpt_plugins/twitter](https://github.com/Significant-Gravitas/Auto-GPT-Plugins/tree/master/src/autogpt_plugins/twitter)
 
-## Third party plugins
+| Plugin       | Description     | Location |
+|--------------|-----------|--------|
+| Twitter      | AutoGPT is capable of retrieving Twitter posts and other related content by accessing the Twitter platform via the v1.1 API using Tweepy.| [autogpt_plugins/twitter](https://github.com/Significant-Gravitas/Auto-GPT-Plugins/tree/master/src/autogpt_plugins/twitter)
+| Email | Revolutionize email management with the Auto-GPT Email Plugin, leveraging AI to automate drafting and intelligent replies. | [autogpt_plugins/email](https://github.com/Significant-Gravitas/Auto-GPT-Plugins/tree/master/src/autogpt_plugins/email)
 
 Some third-party plugins have been created by contributors that are not included in this repository. For more information about these plugins, please visit their respective GitHub pages.
 
-| Plugin           | Description     | Repository |
-|------------------|-----------------|------------|
-| System Information | This plugin adds an extra line to the prompt, serving as a hint for the AI to use shell commands likely supported by the current system. By incorporating this plugin, you can ensure that the AI model provides more accurate and system-specific shell commands, improving its overall performance and usefulness. | [hdkiller/Auto-GPT-SystemInfo](https://github.com/hdkiller/Auto-GPT-SystemInfo) |
-| Notion           | Notion plugin for Auto-GPT.  | [doutv/Auto-GPT-Notion](https://github.com/doutv/Auto-GPT-Notion) |
-| Email Plugin     | Revolutionize Your Email Management with Auto-GPT | [riensen/Auto-GPT-Email-Plugin](https://github.com/riensen/Auto-GPT-Email-Plugin) |
-| Telegram         | A smoothly working Telegram bot that gives you all the messages you would normally get through the Terminal. | [Wladastic/Auto-GPT-Telegram-Plugin](https://github.com/Wladastic/Auto-GPT-Telegram-Plugin) |
+| Plugin       | Description     | Repository |
+|--------------|-----------------|-------------|
+| System Information      | This plugin adds an extra line to the prompt, serving as a hint for the AI to use shell commands likely supported by the current system. By incorporating this plugin, you can ensure that the AI model provides more accurate and system-specific shell commands, improving its overall performance and usefulness. | [hdkiller/Auto-GPT-SystemInfo](https://github.com/hdkiller/Auto-GPT-SystemInfo) |
+| Notion      | Notion plugin for Auto-GPT.  | [doutv/Auto-GPT-Notion](https://github.com/doutv/Auto-GPT-Notion) |
+| Telegram | A smoothly working Telegram bot that gives you all the messages you would normally get through the Terminal. | [Wladastic/Auto-GPT-Telegram-Plugin](https://github.com/Wladastic/Auto-GPT-Telegram-Plugin)
 
 ## Configuration
 
diff --git a/pyproject.toml b/pyproject.toml
index e6ff9bee..ca6dcb48 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
 name = "agpt_plugins"
 version = "0.0.2"
 authors = [
-  { name="Torantulino", email="support@agpt.co" },
+  { name="Torantulino", email="support@agpt.co" }, { name="Riensen", email="3340218+riensen@users.noreply.github.com" }
 ]
 description = "The plugins for Auto-GPT."
 readme = "README.md"
diff --git a/src/autogpt_plugins/email/README.md b/src/autogpt_plugins/email/README.md
new file mode 100644
index 00000000..467b8db2
--- /dev/null
+++ b/src/autogpt_plugins/email/README.md
@@ -0,0 +1,151 @@
+# Auto-GPT Email Plugin: Revolutionize Your Email Management with Auto-GPT 🚀
+
+The Auto-GPT Email Plugin is an innovative and powerful plugin for the groundbreaking base software, Auto-GPT. Harnessing the capabilities of the latest Auto-GPT architecture, Auto-GPT aims to autonomously achieve any goal you set, pushing the boundaries of what is possible with artificial intelligence. This email plugin takes Auto-GPT to the next level by enabling it to send and read emails, opening up a world of exciting use cases.
+
+[![Twitter Follow](https://img.shields.io/twitter/follow/riensen?style=social)](https://twitter.com/riensen)
+[![GitHub Repo stars](https://img.shields.io/github/stars/Significant-Gravitas/auto-gpt-plugins?style=social)](https://github.com/Significant-Gravitas/Auto-GPT-Plugins/stargazers)
+
+<img width="1063" alt="auto-gpt-email-plugin" src="https://user-images.githubusercontent.com/3340218/233331404-fd663c98-5065-4aa5-8cfb-12ce3ed261d0.png">
+
+<img width="1011" alt="gmail-view-auto-gpt-email-plugin" src="https://user-images.githubusercontent.com/3340218/233331422-c5afe433-d4ad-48e0-a0e4-2783cc5f842b.png">
+
+## 🌟 Key Features
+
+- 📬 **Read Emails:** Effortlessly manage your inbox with Auto-GPT's email reading capabilities, ensuring you never miss important information.
+- 📤 **Auto-Compose and Send Emails**: Auto-GPT crafts personalized, context-aware emails using its advanced language model capabilities, saving you time and effort.
+- 📝 **Save Emails to Drafts Folder:** Gain more control by letting Auto-GPT create email drafts that you can review and edit before sending, ensuring your messages are fine-tuned to your preferences.
+- 📎 **Send Emails with Attachments:** Effortlessly send emails with attachments, making your communication richer and more comprehensive.
+- 🛡️ **Custom Email Signature:** Personalize your emails with a custom Auto-GPT signature, adding a touch of automation to every message sent by Auto-GPT.
+- 🎯 **Auto-Reply and Answer Questions:** Streamline your email responses by letting Auto-GPT intelligently read, analyze, and reply to incoming messages with accurate answers.
+- 🔌 **Seamless Integration with Auto-GPT:** Enjoy easy setup and integration with the base Auto-GPT software, opening up a world of powerful automation possibilities.
+
+Unlock the full potential of your email management with the Auto-GPT Email Plugin and revolutionize your email experience today! 🚀
+
+## 🔧 Installation
+
+Follow these steps to configure the Auto-GPT Email Plugin:
+
+### 1. Follow Auto-GPT-Plugins Installation Instructions
+Follow the instructions as per the [Auto-GPT-Plugins/README.md](https://github.com/Significant-Gravitas/Auto-GPT-Plugins/blob/master/README.md)
+
+### 2. Locate the `.env.template` file
+Find the file named `.env.template` in the main `/Auto-GPT` folder.
+
+### 3. Create and rename a copy of the file
+Duplicate the `.env.template` file and rename the copy to `.env` inside the `/Auto-GPT` folder.
+
+### 4. Edit the `.env` file
+Open the `.env` file in a text editor. Note: Files starting with a dot might be hidden by your operating system.
+
+### 5. Add email configuration settings
+Append the following configuration settings to the end of the file:
+
+```ini
+################################################################################
+### EMAIL (SMTP / IMAP)
+################################################################################
+
+EMAIL_ADDRESS=
+EMAIL_PASSWORD=
+EMAIL_SMTP_HOST=smtp.gmail.com
+EMAIL_SMTP_PORT=587
+EMAIL_IMAP_SERVER=imap.gmail.com
+
+#Optional Settings
+EMAIL_MARK_AS_SEEN=False
+EMAIL_SIGNATURE="This was sent by Auto-GPT"
+EMAIL_DRAFT_MODE_WITH_FOLDER=[Gmail]/Drafts
+```
+
+1. **Email address and password:**
+    - Set `EMAIL_ADDRESS` to your sender email address.
+    - Set `EMAIL_PASSWORD` to your password. For Gmail, use an [App Password](https://myaccount.google.com/apppasswords).
+
+2. **Provider-specific settings:**
+    - If not using Gmail, adjust `EMAIL_SMTP_HOST`, `EMAIL_IMAP_SERVER`, and `EMAIL_SMTP_PORT` according to your email provider's settings.
+
+3. **Optional settings:**
+    - `EMAIL_MARK_AS_SEEN`: By default, processed emails are not marked as `SEEN`. Set to `True` to change this.
+    - `EMAIL_SIGNATURE`: By default, no email signature is included. Configure this parameter to add a custom signature to each message sent by Auto-GPT.
+    - `EMAIL_DRAFT_MODE_WITH_FOLDER`: Prevents emails from being sent and instead stores them as drafts in the specified IMAP folder. `[Gmail]/Drafts` is the default drafts folder for Gmail.
+
+
+### 6. Allowlist Plugin
+In your `.env` search for `ALLOWLISTED_PLUGINS` and add this Plugin:
+
+```ini
+################################################################################
+### ALLOWLISTED PLUGINS
+################################################################################
+
+#ALLOWLISTED_PLUGINS - Sets the listed plugins that are allowed (Example: plugin1,plugin2,plugin3)
+ALLOWLISTED_PLUGINS=AutoGPTEmailPlugin
+```
+
+## 🧪 Test the Auto-GPT Email Plugin
+
+Experience the plugin's capabilities by testing it for sending and receiving emails.
+
+### 📤 Test Sending Emails
+
+1. **Configure Auto-GPT:**
+   Set up Auto-GPT with the following parameters:
+   - Name: `CommunicatorGPT`
+   - Role: `Communicate`
+   - Goals:
+     1. Goal 1: `Send an email to my-email-plugin-test@trash-mail.com to introduce yourself`
+     2. Goal 2: `Terminate`
+
+2. **Run Auto-GPT:**
+   Launch Auto-GPT, which should use the email plugin to send an email to my-email-plugin-test@trash-mail.com.
+
+3. **Verify the email:**
+   Check your outbox to confirm that the email was sent. Visit [trash-mail.com](https://www.trash-mail.com/) and enter your chosen email to ensure the email was received.
+
+4. **Sample email content:**
+   Auto-GPT might send the following email:
+   ```
+   Hello,
+
+   My name is CommunicatorGPT, and I am an LLM. I am writing to introduce myself and to let you know that I will be terminating shortly. Thank you for your time.
+
+   Best regards,
+   CommunicatorGPT
+   ```
+
+### 📬 Test Receiving Emails and Replying Back
+
+1. **Send a test email:**
+   Compose an email with a simple question from a [trash-mail.com](https://www.trash-mail.com/) email address to your configured `EMAIL_ADDRESS` in your `.env` file.
+
+2. **Configure Auto-GPT:**
+   Set up Auto-GPT with the following parameters:
+   - Name: `CommunicatorGPT`
+   - Role: `Communicate`
+   - Goals:
+     1. Goal 1: `Read my latest emails`
+     2. Goal 2: `Send back an email with an answer`
+     3. Goal 3: `Terminate`
+
+3. **Run Auto-GPT:**
+   Launch Auto-GPT, which should automatically reply to the email with an answer.
+
+### 🎁 Test Sending Emails with Attachment
+
+1. **Send a test email:**
+   Compose an email with a simple question from a [trash-mail.com](https://www.trash-mail.com/) email address to your configured `EMAIL_ADDRESS` in your `.env` file.
+
+2. **Place attachment in Auto-GPT workspace folder**
+   Insert the attachment intended for sending into the Auto-GPT workspace folder, typically named auto_gpt_workspace, which is located within the cloned [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) Github repository.
+
+3. **Configure Auto-GPT:**
+   Set up Auto-GPT with the following parameters:
+   - Name: `CommunicatorGPT`
+   - Role: `Communicate`
+   - Goals:
+     1. Goal 1: `Read my latest emails`
+     2. Goal 2: `Send back an email with an answer and always attach happy.png`
+     3. Goal 3: `Terminate`
+
+4. **Run Auto-GPT:**
+   Launch Auto-GPT, which should automatically reply to the email with an answer and the attached file.
diff --git a/src/autogpt_plugins/email/__init__.py b/src/autogpt_plugins/email/__init__.py
new file mode 100644
index 00000000..1f385a66
--- /dev/null
+++ b/src/autogpt_plugins/email/__init__.py
@@ -0,0 +1,250 @@
+"""This is the email plugin for Auto-GPT."""
+from typing import Any, Dict, List, Optional, Tuple, TypeVar, TypedDict
+from auto_gpt_plugin_template import AutoGPTPluginTemplate
+import os
+
+PromptGenerator = TypeVar("PromptGenerator")
+
+
+class Message(TypedDict):
+    role: str
+    content: str
+
+
+class AutoGPTEmailPlugin(AutoGPTPluginTemplate):
+    """
+    This is the Auto-GPT email plugin.
+    """
+
+    def __init__(self):
+        super().__init__()
+        self._name = "Auto-GPT-Email-Plugin"
+        self._version = "0.1.3"
+        self._description = "Auto-GPT Email Plugin: Supercharge email management."
+        self.load_commands = os.getenv("EMAIL_ADDRESS") and os.getenv("EMAIL_PASSWORD")
+
+    def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator:
+        if self.load_commands:
+            from .email_plugin.email_plugin import (
+                read_emails,
+                send_email,
+                send_email_with_attachment,
+            )
+
+            prompt.add_command(
+                "Read Emails",
+                "read_emails",
+                {
+                    "imap_folder": "<imap_folder>",
+                    "imap_search_command": "<imap_search_criteria_command>",
+                },
+                read_emails,
+            )
+            prompt.add_command(
+                "Send Email",
+                "send_email",
+                {"to": "<to>", "subject": "<subject>", "body": "<body>"},
+                send_email,
+            )
+            prompt.add_command(
+                "Send Email",
+                "send_email_with_attachment",
+                {
+                    "to": "<to>",
+                    "subject": "<subject>",
+                    "body": "<body>",
+                    "attachment": "<path_to_file>",
+                },
+                send_email_with_attachment,
+            )
+
+        return prompt
+
+    def can_handle_post_prompt(self) -> bool:
+        """This method is called to check that the plugin can
+        handle the post_prompt method.
+
+        Returns:
+            bool: True if the plugin can handle the post_prompt method."""
+        return True
+
+    def can_handle_on_response(self) -> bool:
+        """This method is called to check that the plugin can
+        handle the on_response method.
+
+        Returns:
+            bool: True if the plugin can handle the on_response method."""
+        return False
+
+    def on_response(self, response: str, *args, **kwargs) -> str:
+        """This method is called when a response is received from the model."""
+        pass
+
+    def can_handle_on_planning(self) -> bool:
+        """This method is called to check that the plugin can
+        handle the on_planning method.
+
+        Returns:
+            bool: True if the plugin can handle the on_planning method."""
+        return False
+
+    def on_planning(
+        self, prompt: PromptGenerator, messages: List[Message]
+    ) -> Optional[str]:
+        """This method is called before the planning chat completion is done.
+
+        Args:
+            prompt (PromptGenerator): The prompt generator.
+            messages (List[str]): The list of messages.
+        """
+        pass
+
+    def can_handle_post_planning(self) -> bool:
+        """This method is called to check that the plugin can
+        handle the post_planning method.
+
+        Returns:
+            bool: True if the plugin can handle the post_planning method."""
+        return False
+
+    def post_planning(self, response: str) -> str:
+        """This method is called after the planning chat completion is done.
+
+        Args:
+            response (str): The response.
+
+        Returns:
+            str: The resulting response.
+        """
+        pass
+
+    def can_handle_pre_instruction(self) -> bool:
+        """This method is called to check that the plugin can
+        handle the pre_instruction method.
+
+        Returns:
+            bool: True if the plugin can handle the pre_instruction method."""
+        return False
+
+    def pre_instruction(self, messages: List[Message]) -> List[Message]:
+        """This method is called before the instruction chat is done.
+
+        Args:
+            messages (List[Message]): The list of context messages.
+
+        Returns:
+            List[Message]: The resulting list of messages.
+        """
+        pass
+
+    def can_handle_on_instruction(self) -> bool:
+        """This method is called to check that the plugin can
+        handle the on_instruction method.
+
+        Returns:
+            bool: True if the plugin can handle the on_instruction method."""
+        return False
+
+    def on_instruction(self, messages: List[Message]) -> Optional[str]:
+        """This method is called when the instruction chat is done.
+
+        Args:
+            messages (List[Message]): The list of context messages.
+
+        Returns:
+            Optional[str]: The resulting message.
+        """
+        pass
+
+    def can_handle_post_instruction(self) -> bool:
+        """This method is called to check that the plugin can
+        handle the post_instruction method.
+
+        Returns:
+            bool: True if the plugin can handle the post_instruction method."""
+        return False
+
+    def post_instruction(self, response: str) -> str:
+        """This method is called after the instruction chat is done.
+
+        Args:
+            response (str): The response.
+
+        Returns:
+            str: The resulting response.
+        """
+        pass
+
+    def can_handle_pre_command(self) -> bool:
+        """This method is called to check that the plugin can
+        handle the pre_command method.
+
+        Returns:
+            bool: True if the plugin can handle the pre_command method."""
+        return False
+
+    def pre_command(
+        self, command_name: str, arguments: Dict[str, Any]
+    ) -> Tuple[str, Dict[str, Any]]:
+        """This method is called before the command is executed.
+
+        Args:
+            command_name (str): The command name.
+            arguments (Dict[str, Any]): The arguments.
+
+        Returns:
+            Tuple[str, Dict[str, Any]]: The command name and the arguments.
+        """
+        pass
+
+    def can_handle_post_command(self) -> bool:
+        """This method is called to check that the plugin can
+        handle the post_command method.
+
+        Returns:
+            bool: True if the plugin can handle the post_command method."""
+        return False
+
+    def post_command(self, command_name: str, response: str) -> str:
+        """This method is called after the command is executed.
+
+        Args:
+            command_name (str): The command name.
+            response (str): The response.
+
+        Returns:
+            str: The resulting response.
+        """
+        pass
+
+    def can_handle_chat_completion(
+        self, messages: Dict[Any, Any], model: str, temperature: float, max_tokens: int
+    ) -> bool:
+        """This method is called to check that the plugin can
+          handle the chat_completion method.
+
+        Args:
+            messages (List[Message]): The messages.
+            model (str): The model name.
+            temperature (float): The temperature.
+            max_tokens (int): The max tokens.
+
+          Returns:
+              bool: True if the plugin can handle the chat_completion method."""
+        return False
+
+    def handle_chat_completion(
+        self, messages: List[Message], model: str, temperature: float, max_tokens: int
+    ) -> str:
+        """This method is called when the chat completion is done.
+
+        Args:
+            messages (List[Message]): The messages.
+            model (str): The model name.
+            temperature (float): The temperature.
+            max_tokens (int): The max tokens.
+
+        Returns:
+            str: The resulting response.
+        """
+        pass
diff --git a/src/autogpt_plugins/email/email_plugin/email_plugin.py b/src/autogpt_plugins/email/email_plugin/email_plugin.py
new file mode 100644
index 00000000..1fff33e4
--- /dev/null
+++ b/src/autogpt_plugins/email/email_plugin/email_plugin.py
@@ -0,0 +1,183 @@
+import os
+import json
+import smtplib
+import email
+import imaplib
+import mimetypes
+import time
+from email.header import decode_header
+from email.message import EmailMessage
+
+
+def getSender():
+    email_sender = os.getenv("EMAIL_ADDRESS")
+    if not email_sender:
+        return "Error: email not sent. EMAIL_ADDRESS not set in environment."
+    return email_sender
+
+
+def getPwd():
+    email_password = os.getenv("EMAIL_PASSWORD")
+    if not email_password:
+        return "Error: email not sent. EMAIL_PASSWORD not set in environment."
+    return email_password
+
+
+def send_email(recipient: str, subject: str, message: str) -> str:
+    return send_email_with_attachment_internal(recipient, subject, message, None, None)
+
+
+def send_email_with_attachment(
+    recipient: str, subject: str, message: str, attachment: str
+) -> str:
+    from autogpt.workspace import path_in_workspace
+
+    attachment_path = path_in_workspace(attachment)
+    return send_email_with_attachment_internal(
+        recipient, subject, message, attachment_path, attachment
+    )
+
+
+def send_email_with_attachment_internal(
+    recipient: str, subject: str, message: str, attachment_path: str, attachment: str
+) -> str:
+    """Send an email
+
+    Args:
+        recipient (str): The email of the recipients
+        subject (str): The subject of the email
+        message (str): The message content of the email
+
+    Returns:
+        str: Any error messages
+    """
+    email_sender = getSender()
+    email_password = getPwd()
+
+    msg = EmailMessage()
+    msg["Subject"] = subject
+    msg["From"] = email_sender
+    msg["To"] = recipient
+
+    signature = os.getenv("EMAIL_SIGNATURE")
+    if signature:
+        message += f"\n{signature}"
+
+    msg.set_content(message)
+
+    if attachment_path:
+        ctype, encoding = mimetypes.guess_type(attachment_path)
+        if ctype is None or encoding is not None:
+            # No guess could be made, or the file is encoded (compressed)
+            ctype = "application/octet-stream"
+        maintype, subtype = ctype.split("/", 1)
+        with open(attachment_path, "rb") as fp:
+            msg.add_attachment(
+                fp.read(), maintype=maintype, subtype=subtype, filename=attachment
+            )
+
+    draft_folder = os.getenv("EMAIL_DRAFT_MODE_WITH_FOLDER")
+
+    if not draft_folder:
+        smtp_host = os.getenv("EMAIL_SMTP_HOST")
+        smtp_port = os.getenv("EMAIL_SMTP_PORT")
+        # send email
+        with smtplib.SMTP(smtp_host, smtp_port) as smtp:
+            smtp.ehlo()
+            smtp.starttls()
+            smtp.login(email_sender, email_password)
+            smtp.send_message(msg)
+            smtp.quit()
+        return f"Email was sent to {recipient}!"
+    else:
+        conn = imap_open(draft_folder, email_sender, email_password)
+        conn.append(
+            draft_folder,
+            "",
+            imaplib.Time2Internaldate(time.time()),
+            str(msg).encode("UTF-8"),
+        )
+        return f"Email went to {draft_folder}!"
+
+
+def read_emails(imap_folder: str = "inbox", imap_search_command: str = "UNSEEN") -> str:
+    """Read emails
+
+    Args:
+        recipient (str): The email of the recipients
+        subject (str): The subject of the email
+        message (str): The message content of the email
+
+    Returns:
+        str: Any error messages
+    """
+    email_sender = getSender()
+    email_password = getPwd()
+
+    mark_as_seen = os.getenv("EMAIL_MARK_AS_SEEN")
+    if isinstance(mark_as_seen, str):
+        mark_as_seen = json.loads(mark_as_seen.lower())
+
+    conn = imap_open(imap_folder, email_sender, email_password)
+    _, search_data = conn.search(None, imap_search_command)
+
+    messages = []
+    for num in search_data[0].split():
+        if mark_as_seen:
+            message_parts = "(RFC822)"
+        else:
+            message_parts = "(BODY.PEEK[])"
+        _, msg_data = conn.fetch(num, message_parts)
+        for response_part in msg_data:
+            if isinstance(response_part, tuple):
+                msg = email.message_from_bytes(response_part[1])
+
+                subject, encoding = decode_header(msg["Subject"])[0]
+                if isinstance(subject, bytes):
+                    subject = subject.decode(encoding)
+
+                body = get_email_body(msg)
+                from_address = msg["From"]
+                to_address = msg["To"]
+                date = msg["Date"]
+                cc = msg["CC"] if msg["CC"] else ""
+
+                messages.append(
+                    {
+                        "From": from_address,
+                        "To": to_address,
+                        "Date": date,
+                        "CC": cc,
+                        "Subject": subject,
+                        "Message Body": body,
+                    }
+                )
+
+    conn.logout()
+    if not messages:
+        return (
+            f"There are no Emails in your folder `{imap_folder}` "
+            f"when searching with imap command `{imap_search_command}`"
+        )
+    return messages
+
+
+def imap_open(
+    imap_folder: str, email_sender: str, email_password: str
+) -> imaplib.IMAP4_SSL:
+    imap_server = os.getenv("EMAIL_IMAP_SERVER")
+    conn = imaplib.IMAP4_SSL(imap_server)
+    conn.login(email_sender, email_password)
+    conn.select(imap_folder)
+    return conn
+
+
+def get_email_body(msg: email.message.Message) -> str:
+    if msg.is_multipart():
+        for part in msg.walk():
+            content_type = part.get_content_type()
+            content_disposition = str(part.get("Content-Disposition"))
+            if content_type == "text/plain" and "attachment" not in content_disposition:
+                return part.get_payload(decode=True).decode()
+    else:
+        return msg.get_payload(decode=True).decode()


Test patch:
diff --git a/src/autogpt_plugins/email/email_plugin/test_email_plugin.py b/src/autogpt_plugins/email/email_plugin/test_email_plugin.py
new file mode 100644
index 00000000..b99c5121
--- /dev/null
+++ b/src/autogpt_plugins/email/email_plugin/test_email_plugin.py
@@ -0,0 +1,294 @@
+import os
+from unittest.mock import patch
+from email.message import EmailMessage
+from email_plugin import (
+    send_email,
+    read_emails,
+    imap_open,
+    send_email_with_attachment_internal,
+)
+from unittest.mock import mock_open
+import unittest
+from functools import partial
+
+MOCK_FROM = "sender@example.com"
+MOCK_PWD = "secret"
+MOCK_TO = "test@example.com"
+MOCK_DATE = "Fri, 21 Apr 2023 10:00:00 -0000"
+MOCK_CONTENT = "Test message\n"
+MOCK_SUBJECT = "Test Subject"
+MOCK_IMAP_SERVER = "imap.example.com"
+MOCK_SMTP_SERVER = "smtp.example.com"
+MOCK_SMTP_PORT = "587"
+
+MOCK_DRAFT_FOLDER = "Example/Drafts"
+MOCK_ATTACHMENT_PATH = "example/file.txt"
+MOCK_ATTACHMENT_NAME = "file.txt"
+
+
+class TestEmailPlugin(unittest.TestCase):
+    @patch("imaplib.IMAP4_SSL")
+    @patch.dict(
+        os.environ,
+        {
+            "EMAIL_ADDRESS": MOCK_FROM,
+            "EMAIL_PASSWORD": MOCK_PWD,
+            "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
+        },
+    )
+    def test_imap_open(self, mock_imap):
+        # Test imapOpen function
+        imap_folder = "inbox"
+        imap_open(imap_folder, MOCK_FROM, MOCK_PWD)
+
+        # Check if the IMAP object was created and used correctly
+        mock_imap.assert_called_once_with(MOCK_IMAP_SERVER)
+        mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
+        mock_imap.return_value.select.assert_called_once_with(imap_folder)
+
+    # Test for successful email sending without attachment
+    @patch("smtplib.SMTP", autospec=True)
+    @patch.dict(
+        os.environ,
+        {
+            "EMAIL_ADDRESS": MOCK_FROM,
+            "EMAIL_PASSWORD": MOCK_PWD,
+            "EMAIL_SMTP_HOST": MOCK_SMTP_SERVER,
+            "EMAIL_SMTP_PORT": MOCK_SMTP_PORT,
+        },
+    )
+    def test_send_email_no_attachment(self, mock_smtp):
+        result = send_email(MOCK_TO, MOCK_SUBJECT, MOCK_CONTENT)
+        assert result == f"Email was sent to {MOCK_TO}!"
+
+        mock_smtp.assert_called_once_with(MOCK_SMTP_SERVER, MOCK_SMTP_PORT)
+
+        # Check if the SMTP object was created and used correctly
+        context = mock_smtp.return_value.__enter__.return_value
+        context.ehlo.assert_called()
+        context.starttls.assert_called_once()
+        context.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
+        context.send_message.assert_called_once()
+        context.quit.assert_called_once()
+
+    # Test for reading emails in a specific folder with a specific search command
+    @patch("imaplib.IMAP4_SSL")
+    @patch.dict(
+        os.environ,
+        {
+            "EMAIL_ADDRESS": MOCK_FROM,
+            "EMAIL_PASSWORD": MOCK_PWD,
+            "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
+        },
+    )
+    def test_read_emails(self, mock_imap):
+        assert os.getenv("EMAIL_ADDRESS") == MOCK_FROM
+
+        # Create a mock email message
+        message = EmailMessage()
+        message["From"] = MOCK_FROM
+        message["To"] = MOCK_TO
+        message["Date"] = MOCK_DATE
+        message["Subject"] = MOCK_SUBJECT
+        message.set_content(MOCK_CONTENT)
+
+        # Set up mock IMAP server behavior
+        mock_imap.return_value.search.return_value = (None, [b"1"])
+        mock_imap.return_value.fetch.return_value = (None, [(b"1", message.as_bytes())])
+
+        # Test read_emails function
+        result = read_emails("inbox", "UNSEEN")
+        expected_result = [
+            {
+                "From": MOCK_FROM,
+                "To": MOCK_TO,
+                "Date": MOCK_DATE,
+                "CC": "",
+                "Subject": MOCK_SUBJECT,
+                "Message Body": MOCK_CONTENT,
+            }
+        ]
+        assert result == expected_result
+
+        # Check if the IMAP object was created and used correctly
+        mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
+        mock_imap.return_value.select.assert_called_once_with("inbox")
+        mock_imap.return_value.search.assert_called_once_with(None, "UNSEEN")
+        mock_imap.return_value.fetch.assert_called_once_with(b"1", "(BODY.PEEK[])")
+
+    # Test for reading empty emails
+    @patch("imaplib.IMAP4_SSL")
+    @patch.dict(
+        os.environ,
+        {
+            "EMAIL_ADDRESS": MOCK_FROM,
+            "EMAIL_PASSWORD": MOCK_PWD,
+            "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
+        },
+    )
+    def test_read_empty_emails(self, mock_imap):
+        assert os.getenv("EMAIL_ADDRESS") == MOCK_FROM
+
+        # Set up mock IMAP server behavior
+        mock_imap.return_value.search.return_value = (None, [b"0"])
+        mock_imap.return_value.fetch.return_value = (None, [])
+
+        # Test read_emails function
+        result = read_emails("inbox", "UNSEEN")
+        expected = "There are no Emails in your folder `inbox` "
+        expected += "when searching with imap command `UNSEEN`"
+        assert result == expected
+
+        # Check if the IMAP object was created and used correctly
+        mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
+        mock_imap.return_value.select.assert_called_once_with("inbox")
+        mock_imap.return_value.search.assert_called_once_with(None, "UNSEEN")
+        mock_imap.return_value.fetch.assert_called_once_with(b"0", "(BODY.PEEK[])")
+
+    # Test for reading emails in a specific folder
+    # with a specific search command with EMAIL_MARK_AS_SEEN=True
+    @patch("imaplib.IMAP4_SSL")
+    @patch.dict(
+        os.environ,
+        {
+            "EMAIL_ADDRESS": MOCK_FROM,
+            "EMAIL_PASSWORD": MOCK_PWD,
+            "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
+            "EMAIL_MARK_AS_SEEN": "True",
+        },
+    )
+    def test_read_emails_mark_as_read_true(self, mock_imap):
+        assert os.getenv("EMAIL_ADDRESS") == MOCK_FROM
+
+        # Create a mock email message
+        message = EmailMessage()
+        message["From"] = MOCK_FROM
+        message["To"] = MOCK_TO
+        message["Date"] = MOCK_DATE
+        message["Subject"] = MOCK_SUBJECT
+        message.set_content(MOCK_CONTENT)
+
+        # Set up mock IMAP server behavior
+        mock_imap.return_value.search.return_value = (None, [b"1"])
+        mock_imap.return_value.fetch.return_value = (None, [(b"1", message.as_bytes())])
+
+        # Test read_emails function
+        result = read_emails("inbox", "UNSEEN")
+        expected_result = [
+            {
+                "From": MOCK_FROM,
+                "To": MOCK_TO,
+                "Date": MOCK_DATE,
+                "CC": "",
+                "Subject": MOCK_SUBJECT,
+                "Message Body": MOCK_CONTENT,
+            }
+        ]
+        assert result == expected_result
+
+        # Check if the IMAP object was created and used correctly
+        mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
+        mock_imap.return_value.select.assert_called_once_with("inbox")
+        mock_imap.return_value.search.assert_called_once_with(None, "UNSEEN")
+        mock_imap.return_value.fetch.assert_called_once_with(b"1", "(RFC822)")
+
+    # Test for reading emails in a specific folder
+    # with a specific search command with EMAIL_MARK_AS_SEEN=False
+    @patch("imaplib.IMAP4_SSL")
+    @patch.dict(
+        os.environ,
+        {
+            "EMAIL_ADDRESS": MOCK_FROM,
+            "EMAIL_PASSWORD": MOCK_PWD,
+            "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
+            "EMAIL_MARK_AS_SEEN": "False",
+        },
+    )
+    def test_read_emails_mark_as_seen_false(self, mock_imap):
+        assert os.getenv("EMAIL_ADDRESS") == MOCK_FROM
+
+        # Create a mock email message
+        message = EmailMessage()
+        message["From"] = MOCK_FROM
+        message["To"] = MOCK_TO
+        message["Date"] = MOCK_DATE
+        message["Subject"] = MOCK_SUBJECT
+        message.set_content(MOCK_CONTENT)
+
+        # Set up mock IMAP server behavior
+        mock_imap.return_value.search.return_value = (None, [b"1"])
+        mock_imap.return_value.fetch.return_value = (None, [(b"1", message.as_bytes())])
+
+        # Test read_emails function
+        result = read_emails("inbox", "UNSEEN")
+        expected_result = [
+            {
+                "From": MOCK_FROM,
+                "To": MOCK_TO,
+                "Date": MOCK_DATE,
+                "CC": "",
+                "Subject": MOCK_SUBJECT,
+                "Message Body": MOCK_CONTENT,
+            }
+        ]
+        assert result == expected_result
+
+        # Check if the IMAP object was created and used correctly
+        mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
+        mock_imap.return_value.select.assert_called_once_with("inbox")
+        mock_imap.return_value.search.assert_called_once_with(None, "UNSEEN")
+        mock_imap.return_value.fetch.assert_called_once_with(b"1", "(BODY.PEEK[])")
+
+    def side_effect_for_open(original_open, file_path, *args, **kwargs):
+        if file_path == MOCK_ATTACHMENT_PATH:
+            return mock_open(read_data=b"file_content").return_value
+        return original_open(file_path, *args, **kwargs)
+
+    original_open = open
+    side_effect_with_original_open = partial(side_effect_for_open, original_open)
+
+    # Test for sending emails with EMAIL_DRAFT_MODE_WITH_FOLDER
+    @patch("imaplib.IMAP4_SSL")
+    @patch.dict(
+        os.environ,
+        {
+            "EMAIL_ADDRESS": MOCK_FROM,
+            "EMAIL_PASSWORD": MOCK_PWD,
+            "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
+            "EMAIL_DRAFT_MODE_WITH_FOLDER": MOCK_DRAFT_FOLDER,
+        },
+    )
+    @patch(f"{__name__}.imap_open")
+    @patch("builtins.open", side_effect=side_effect_with_original_open)
+    def test_send_emails_with_draft_mode(self, mock_file, mock_imap_open, mock_imap):
+        mock_imap_conn = mock_imap_open.return_value
+        mock_imap_conn.select.return_value = ("OK", [b"0"])
+        mock_imap_conn.append.return_value = ("OK", [b"1"])
+
+        result = send_email_with_attachment_internal(
+            MOCK_TO,
+            MOCK_SUBJECT,
+            MOCK_CONTENT,
+            MOCK_ATTACHMENT_PATH,
+            MOCK_ATTACHMENT_NAME,
+        )
+        assert result == f"Email went to {MOCK_DRAFT_FOLDER}!"
+        mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
+        mock_imap.return_value.select.assert_called_once_with(MOCK_DRAFT_FOLDER)
+
+        # Get the actual MIME message appended
+        mock_imap.return_value.append.assert_called_once()
+
+        append_args, _ = mock_imap.return_value.append.call_args
+        actual_mime_msg = append_args[3].decode("utf-8")
+
+        # Check for the presence of relevant information in the MIME message
+        assert MOCK_FROM in actual_mime_msg
+        assert MOCK_TO in actual_mime_msg
+        assert MOCK_SUBJECT in actual_mime_msg
+        assert MOCK_CONTENT in actual_mime_msg
+        assert MOCK_ATTACHMENT_NAME in actual_mime_msg
+
+
+if __name__ == "__main__":
+    unittest.main()