├── .editorconfig
├── .flake8
├── .gitignore
├── .pre-commit-config.yaml
├── LICENSE
├── Makefile
├── README.md
├── helpers.sh
├── pylintrc
├── pyproject.toml
├── requirements.txt
├── run_pylint.py
├── sourcery.yaml
└── src
└── autogpt_plugins
├── __init__.py
└── email
├── __init__.py
└── email_plugin
├── email_plugin.py
└── test_email_plugin.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Top-most EditorConfig file
2 | root = true
3 |
4 | # Set default charset
5 | [*]
6 | charset = utf-8
7 |
8 | # Use black formatter for python files
9 | [*.py]
10 | profile = black
11 |
12 | # Set defaults for windows and batch filess
13 | [*.bat]
14 | end_of_line = crlf
15 | indent_style = space
16 | indent_size = 2
17 |
18 | # Set defaults for shell scripts
19 | [*.sh]
20 | end_of_line = lf
21 | trim_trailing_whitespace = true
22 | insert_final_newline = false
23 |
24 | # Set defaults for Makefiles
25 | [Makefile]
26 | end_of_line = lf
27 | indent_style = tab
28 | indent_size = 4
29 | trim_trailing_whitespace = true
30 | insert_final_newline = true
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 88
3 | extend-ignore = E203
4 | exclude =
5 | .tox,
6 | __pycache__,
7 | *.pyc,
8 | .env
9 | venv/*
10 | .venv/*
11 | reports/*
12 | dist/*
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/sourcery-ai/sourcery
3 | rev: v1.1.0 # Get the latest tag from https://github.com/sourcery-ai/sourcery/tags
4 | hooks:
5 | - id: sourcery
6 |
7 | - repo: git://github.com/pre-commit/pre-commit-hooks
8 | rev: v0.9.2
9 | hooks:
10 | - id: check-added-large-files
11 | args: ["--maxkb=500"]
12 | - id: check-byte-order-marker
13 | - id: check-case-conflict
14 | - id: check-merge-conflict
15 | - id: check-symlinks
16 | - id: debug-statements
17 | - repo: local
18 | hooks:
19 | - id: isort
20 | name: isort-local
21 | entry: isort
22 | language: python
23 | types: [python]
24 | exclude: .+/(dist|.venv|venv|build)/.+
25 | pass_filenames: true
26 | - id: black
27 | name: black-local
28 | entry: black
29 | language: python
30 | types: [python]
31 | exclude: .+/(dist|.venv|venv|build)/.+
32 | pass_filenames: true
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 github.com/riensen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ifeq ($(OS),Windows_NT)
2 | os := win
3 | SCRIPT_EXT := .bat
4 | SHELL_CMD := cmd /C
5 | else
6 | os := nix
7 | SCRIPT_EXT := .sh
8 | SHELL_CMD := bash
9 | endif
10 |
11 | helpers = @$(SHELL_CMD) helpers$(SCRIPT_EXT) $1
12 |
13 | clean: helpers$(SCRIPT_EXT)
14 | $(call helpers,clean)
15 |
16 | qa: helpers$(SCRIPT_EXT)
17 | $(call helpers,qa)
18 |
19 | style: helpers$(SCRIPT_EXT)
20 | $(call helpers,style)
21 |
22 | .PHONY: clean qa style
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Auto-GPT Email Plugin: Revolutionize Your Email Management with Auto-GPT 🚀
2 |
3 | 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.
4 |
5 | [](https://github.com/riensen/Auto-GPT-Email-Plugin/stargazers)
6 | [](https://twitter.com/riensen)
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## 🌟 Key Features
14 |
15 | - 📬 **Read Emails:** Effortlessly manage your inbox with Auto-GPT's email reading capabilities, ensuring you never miss important information.
16 | - 📤 **Auto-Compose and Send Emails**: Auto-GPT crafts personalized, context-aware emails using its advanced language model capabilities, saving you time and effort.
17 | - 📝 **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.
18 | - 📎 **Send Emails with Attachments:** Effortlessly send emails with attachments, making your communication richer and more comprehensive.
19 | - 🛡️ **Custom Email Signature:** Personalize your emails with a custom Auto-GPT signature, adding a touch of automation to every message sent by Auto-GPT.
20 | - 🎯 **Auto-Reply and Answer Questions:** Streamline your email responses by letting Auto-GPT intelligently read, analyze, and reply to incoming messages with accurate answers.
21 | - 🔌 **Seamless Integration with Auto-GPT:** Enjoy easy setup and integration with the base Auto-GPT software, opening up a world of powerful automation possibilities.
22 |
23 | Unlock the full potential of your email management with the Auto-GPT Email Plugin and revolutionize your email experience today! 🚀
24 |
25 | ## 🔧 Installation
26 |
27 | Follow these steps to configure the Auto-GPT Email Plugin:
28 |
29 | ### 1. Clone the Auto-GPT-Email-Plugin repository
30 | Clone this repository and navigate to the `Auto-GPT-Email-Plugin` folder in your terminal:
31 |
32 | ```bash
33 | git clone https://github.com/riensen/Auto-GPT-Email-Plugin.git
34 | ```
35 |
36 | ### 2. Install required dependencies
37 | Execute the following command to install the necessary dependencies:
38 |
39 | ```bash
40 | pip install -r requirements.txt
41 | ```
42 |
43 | ### 3. Package the plugin as a Zip file
44 | Compress the `Auto-GPT-Email-Plugin` folder or [download the repository as a zip file](https://github.com/riensen/Auto-GPT-Email-Plugin/archive/refs/heads/master.zip).
45 |
46 | ### 4. Install Auto-GPT
47 | If you haven't already, clone the [Auto-GPT](https://github.com/Significant-Gravitas/Auto-GPT) repository, follow its installation instructions, and navigate to the `Auto-GPT` folder.
48 |
49 | ### 5. Copy the Zip file into the Auto-GPT Plugin folder
50 | Transfer the zip file from step 3 into the `plugins` subfolder within the `Auto-GPT` repo.
51 |
52 | ### 6. Locate the `.env.template` file
53 | Find the file named `.env.template` in the main `/Auto-GPT` folder.
54 |
55 | ### 7. Create and rename a copy of the file
56 | Duplicate the `.env.template` file and rename the copy to `.env` inside the `/Auto-GPT` folder.
57 |
58 | ### 8. Edit the `.env` file
59 | Open the `.env` file in a text editor. Note: Files starting with a dot might be hidden by your operating system.
60 |
61 | ### 9. Add email configuration settings
62 | Append the following configuration settings to the end of the file:
63 |
64 | ```ini
65 | ################################################################################
66 | ### EMAIL (SMTP / IMAP)
67 | ################################################################################
68 |
69 | EMAIL_ADDRESS=
70 | EMAIL_PASSWORD=
71 | EMAIL_SMTP_HOST=smtp.gmail.com
72 | EMAIL_SMTP_PORT=587
73 | EMAIL_IMAP_SERVER=imap.gmail.com
74 |
75 | #Optional Settings
76 | EMAIL_MARK_AS_SEEN=False
77 | EMAIL_SIGNATURE="This was sent by Auto-GPT"
78 | EMAIL_DRAFT_MODE_WITH_FOLDER=[Gmail]/Drafts
79 | ```
80 |
81 | 1. **Email address and password:**
82 | - Set `EMAIL_ADDRESS` to your sender email address.
83 | - Set `EMAIL_PASSWORD` to your password. For Gmail, use an [App Password](https://myaccount.google.com/apppasswords).
84 |
85 | 2. **Provider-specific settings:**
86 | - If not using Gmail, adjust `EMAIL_SMTP_HOST`, `EMAIL_IMAP_SERVER`, and `EMAIL_SMTP_PORT` according to your email provider's settings.
87 |
88 | 3. **Optional settings:**
89 | - `EMAIL_MARK_AS_SEEN`: By default, processed emails are not marked as `SEEN`. Set to `True` to change this.
90 | - `EMAIL_SIGNATURE`: By default, no email signature is included. Configure this parameter to add a custom signature to each message sent by Auto-GPT.
91 | - `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.
92 |
93 |
94 | ### 10. Allowlist Plugin
95 | In your `.env` search for `ALLOWLISTED_PLUGINS` and add this Plugin:
96 |
97 | ```ini
98 | ################################################################################
99 | ### ALLOWLISTED PLUGINS
100 | ################################################################################
101 |
102 | #ALLOWLISTED_PLUGINS - Sets the listed plugins that are allowed (Example: plugin1,plugin2,plugin3)
103 | ALLOWLISTED_PLUGINS=AutoGPTEmailPlugin
104 | ```
105 |
106 | ## 🧪 Test the Auto-GPT Email Plugin
107 |
108 | Experience the plugin's capabilities by testing it for sending and receiving emails.
109 |
110 | ### 📤 Test Sending Emails
111 |
112 | 1. **Configure Auto-GPT:**
113 | Set up Auto-GPT with the following parameters:
114 | - Name: `CommunicatorGPT`
115 | - Role: `Communicate`
116 | - Goals:
117 | 1. Goal 1: `Send an email to my-email-plugin-test@trash-mail.com to introduce yourself`
118 | 2. Goal 2: `Terminate`
119 |
120 | 2. **Run Auto-GPT:**
121 | Launch Auto-GPT, which should use the email plugin to send an email to my-email-plugin-test@trash-mail.com.
122 |
123 | 3. **Verify the email:**
124 | 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.
125 |
126 | 4. **Sample email content:**
127 | Auto-GPT might send the following email:
128 | ```
129 | Hello,
130 |
131 | 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.
132 |
133 | Best regards,
134 | CommunicatorGPT
135 | ```
136 |
137 | ### 📬 Test Receiving Emails and Replying Back
138 |
139 | 1. **Send a test email:**
140 | 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.
141 |
142 | 2. **Configure Auto-GPT:**
143 | Set up Auto-GPT with the following parameters:
144 | - Name: `CommunicatorGPT`
145 | - Role: `Communicate`
146 | - Goals:
147 | 1. Goal 1: `Read my latest emails`
148 | 2. Goal 2: `Send back an email with an answer`
149 | 3. Goal 3: `Terminate`
150 |
151 | 3. **Run Auto-GPT:**
152 | Launch Auto-GPT, which should automatically reply to the email with an answer.
153 |
154 | ### 🎁 Test Sending Emails with Attachment
155 |
156 | 1. **Send a test email:**
157 | 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.
158 |
159 | 2. **Place attachment in Auto-GPT workspace folder**
160 | 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.
161 |
162 | 3. **Configure Auto-GPT:**
163 | Set up Auto-GPT with the following parameters:
164 | - Name: `CommunicatorGPT`
165 | - Role: `Communicate`
166 | - Goals:
167 | 1. Goal 1: `Read my latest emails`
168 | 2. Goal 2: `Send back an email with an answer and always attach happy.png`
169 | 3. Goal 3: `Terminate`
170 |
171 | 4. **Run Auto-GPT:**
172 | Launch Auto-GPT, which should automatically reply to the email with an answer and the attached file.
173 |
--------------------------------------------------------------------------------
/helpers.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | clean() {
4 | # Remove build artifacts and temporary files
5 | rm -rf build 2>/dev/null || true
6 | rm -rf dist 2>/dev/null || true
7 | rm -rf __pycache__ 2>/dev/null || true
8 | rm -rf *.egg-info 2>/dev/null || true
9 | rm -rf **/*.egg-info 2>/dev/null || true
10 | rm -rf *.pyc 2>/dev/null || true
11 | rm -rf **/*.pyc 2>/dev/null || true
12 | rm -rf reports 2>/dev/null || true
13 | }
14 |
15 | qa() {
16 | # Run static analysis tools
17 | flake8 .
18 | python run_pylint.py
19 | }
20 |
21 | style() {
22 | # Format code
23 | isort .
24 | black --exclude=".*\/*(dist|venv|.venv|test-results)\/*.*" .
25 | }
26 |
27 | if [ "$1" = "clean" ]; then
28 | echo Removing build artifacts and temporary files...
29 | clean
30 | elif [ "$1" = "qa" ]; then
31 | echo Running static analysis tools...
32 | qa
33 | elif [ "$1" = "style" ]; then
34 | echo Running code formatters...
35 | style
36 | else
37 | echo "Usage: $0 [clean|qa|style]"
38 | exit 1
39 | fi
40 |
41 | echo Done!
42 | echo
43 | exit 0
--------------------------------------------------------------------------------
/pylintrc:
--------------------------------------------------------------------------------
1 | # This Pylint rcfile contains a best-effort configuration to uphold the
2 | # best-practices and style described in the Google Python style guide:
3 | # https://google.github.io/styleguide/pyguide.html
4 | #
5 | # Its canonical open-source location is:
6 | # https://google.github.io/styleguide/pylintrc
7 |
8 | [MASTER]
9 |
10 | # Files or directories to be skipped. They should be base names, not paths.
11 | ignore=
12 |
13 | # Files or directories matching the regex patterns are skipped. The regex
14 | # matches against base names, not paths.
15 | ignore-patterns=
16 |
17 | # Pickle collected data for later comparisons.
18 | persistent=no
19 |
20 | # List of plugins (as comma separated values of python modules names) to load,
21 | # usually to register additional checkers.
22 | load-plugins=
23 |
24 | # Use multiple processes to speed up Pylint.
25 | jobs=4
26 |
27 | # Allow loading of arbitrary C extensions. Extensions are imported into the
28 | # active Python interpreter and may run arbitrary code.
29 | unsafe-load-any-extension=no
30 |
31 |
32 | [MESSAGES CONTROL]
33 |
34 | ignore=*.pyc
35 | # Only show warnings with the listed confidence levels. Leave empty to show
36 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
37 | confidence=
38 |
39 | # Enable the message, report, category or checker with the given id(s). You can
40 | # either give multiple identifier separated by comma (,) or put this option
41 | # multiple time (only on the command line, not in the configuration file where
42 | # it should appear only once). See also the "--disable" option for examples.
43 | #enable=
44 |
45 | # Disable the message, report, category or checker with the given id(s). You
46 | # can either give multiple identifiers separated by comma (,) or put this
47 | # option multiple times (only on the command line, not in the configuration
48 | # file where it should appear only once).You can also use "--disable=all" to
49 | # disable everything first and then reenable specific checks. For example, if
50 | # you want to run only the similarities checker, you can use "--disable=all
51 | # --enable=similarities". If you want to run only the classes checker, but have
52 | # no Warning level messages displayed, use"--disable=all --enable=classes
53 | # --disable=W"
54 | disable=abstract-method,
55 | parse-error,
56 | apply-builtin,
57 | arguments-differ,
58 | attribute-defined-outside-init,
59 | backtick,
60 | bad-option-value,
61 | basestring-builtin,
62 | buffer-builtin,
63 | c-extension-no-member,
64 | consider-using-enumerate,
65 | cmp-builtin,
66 | cmp-method,
67 | coerce-builtin,
68 | coerce-method,
69 | delslice-method,
70 | div-method,
71 | duplicate-code,
72 | eq-without-hash,
73 | execfile-builtin,
74 | file-builtin,
75 | filter-builtin-not-iterating,
76 | fixme,
77 | getslice-method,
78 | global-statement,
79 | hex-method,
80 | idiv-method,
81 | implicit-str-concat,
82 | import-error,
83 | import-self,
84 | import-star-module-level,
85 | inconsistent-return-statements,
86 | input-builtin,
87 | intern-builtin,
88 | invalid-str-codec,
89 | locally-disabled,
90 | long-builtin,
91 | long-suffix,
92 | map-builtin-not-iterating,
93 | misplaced-comparison-constant,
94 | missing-function-docstring,
95 | metaclass-assignment,
96 | next-method-called,
97 | next-method-defined,
98 | no-absolute-import,
99 | no-else-break,
100 | no-else-continue,
101 | no-else-raise,
102 | no-else-return,
103 | no-init, # added
104 | no-member,
105 | no-name-in-module,
106 | no-self-use,
107 | nonzero-method,
108 | oct-method,
109 | old-division,
110 | old-ne-operator,
111 | old-octal-literal,
112 | old-raise-syntax,
113 | parameter-unpacking,
114 | print-statement,
115 | raising-string,
116 | range-builtin-not-iterating,
117 | raw_input-builtin,
118 | rdiv-method,
119 | reduce-builtin,
120 | relative-import,
121 | reload-builtin,
122 | round-builtin,
123 | setslice-method,
124 | signature-differs,
125 | standarderror-builtin,
126 | suppressed-message,
127 | sys-max-int,
128 | too-few-public-methods,
129 | too-many-ancestors,
130 | too-many-arguments,
131 | too-many-boolean-expressions,
132 | too-many-branches,
133 | too-many-instance-attributes,
134 | too-many-locals,
135 | too-many-nested-blocks,
136 | too-many-public-methods,
137 | too-many-return-statements,
138 | too-many-statements,
139 | trailing-newlines,
140 | unichr-builtin,
141 | unicode-builtin,
142 | unnecessary-pass,
143 | unpacking-in-except,
144 | useless-else-on-loop,
145 | useless-object-inheritance,
146 | useless-suppression,
147 | using-cmp-argument,
148 | wrong-import-order,
149 | xrange-builtin,
150 | zip-builtin-not-iterating,
151 |
152 |
153 | [REPORTS]
154 |
155 | # Set the output format. Available formats are text, parseable, colorized, msvs
156 | # (visual studio) and html. You can also give a reporter class, eg
157 | # mypackage.mymodule.MyReporterClass.
158 | output-format=text
159 |
160 | # Tells whether to display a full report or only the messages
161 | reports=no
162 |
163 | # Python expression which should return a note less than 10 (10 is the highest
164 | # note). You have access to the variables errors warning, statement which
165 | # respectively contain the number of errors / warnings messages and the total
166 | # number of statements analyzed. This is used by the global evaluation report
167 | # (RP0004).
168 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
169 |
170 | # Template used to display messages. This is a python new-style format string
171 | # used to format the message information. See doc for all details
172 | #msg-template=
173 |
174 |
175 | [BASIC]
176 |
177 | # Good variable names which should always be accepted, separated by a comma
178 | good-names=main,_
179 |
180 | # Bad variable names which should always be refused, separated by a comma
181 | bad-names=
182 |
183 | # Colon-delimited sets of names that determine each other's naming style when
184 | # the name regexes allow several styles.
185 | name-group=
186 |
187 | # Include a hint for the correct naming format with invalid-name
188 | include-naming-hint=no
189 |
190 | # List of decorators that produce properties, such as abc.abstractproperty. Add
191 | # to this list to register other decorators that produce valid properties.
192 | property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
193 |
194 | # Regular expression matching correct function names
195 | function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$
196 |
197 | # Regular expression matching correct variable names
198 | variable-rgx=^[a-z][a-z0-9_]*$
199 |
200 | # Regular expression matching correct constant names
201 | const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
202 |
203 | # Regular expression matching correct attribute names
204 | attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
205 |
206 | # Regular expression matching correct argument names
207 | argument-rgx=^[a-z][a-z0-9_]*$
208 |
209 | # Regular expression matching correct class attribute names
210 | class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
211 |
212 | # Regular expression matching correct inline iteration names
213 | inlinevar-rgx=^[a-z][a-z0-9_]*$
214 |
215 | # Regular expression matching correct class names
216 | class-rgx=^_?[A-Z][a-zA-Z0-9]*$
217 |
218 | # Regular expression matching correct module names
219 | module-rgx=^(_?[a-z][a-z0-9_]*|__init__|__main__)$
220 |
221 | # Regular expression matching correct method names
222 | method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$
223 |
224 | # Regular expression which should only match function or class names that do
225 | # not require a docstring.
226 | no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
227 |
228 | # Minimum line length for functions/classes that require docstrings, shorter
229 | # ones are exempt.
230 | docstring-min-length=10
231 |
232 |
233 | [TYPECHECK]
234 |
235 | # List of decorators that produce context managers, such as
236 | # contextlib.contextmanager. Add to this list to register other decorators that
237 | # produce valid context managers.
238 | contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
239 |
240 | # Tells whether missing members accessed in mixin class should be ignored. A
241 | # mixin class is detected if its name ends with "mixin" (case insensitive).
242 | ignore-mixin-members=yes
243 |
244 | # List of module names for which member attributes should not be checked
245 | # (useful for modules/projects where namespaces are manipulated during runtime
246 | # and thus existing member attributes cannot be deduced by static analysis. It
247 | # supports qualified module names, as well as Unix pattern matching.
248 | ignored-modules=
249 |
250 | # List of class names for which member attributes should not be checked (useful
251 | # for classes with dynamically set attributes). This supports the use of
252 | # qualified names.
253 | ignored-classes=optparse.Values,thread._local,_thread._local
254 |
255 | # List of members which are set dynamically and missed by pylint inference
256 | # system, and so shouldn't trigger E1101 when accessed. Python regular
257 | # expressions are accepted.
258 | generated-members=
259 |
260 |
261 | [FORMAT]
262 |
263 | # Maximum number of characters on a single line.
264 | max-line-length=88
265 |
266 | # TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt
267 | # lines made too long by directives to pytype.
268 |
269 | # Regexp for a line that is allowed to be longer than the limit.
270 | ignore-long-lines=(?x)(
271 | ^\s*(\#\ )??$|
272 | ^\s*(from\s+\S+\s+)?import\s+.+$)
273 |
274 | # Allow the body of an if to be on the same line as the test if there is no
275 | # else.
276 | single-line-if-stmt=yes
277 |
278 | # Maximum number of lines in a module
279 | max-module-lines=99999
280 |
281 | # String used as indentation unit. The internal Google style guide mandates 2
282 | # spaces. Google's externaly-published style guide says 4, consistent with
283 | # PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google
284 | # projects (like TensorFlow).
285 | indent-string=' '
286 |
287 | # Number of spaces of indent required inside a hanging or continued line.
288 | indent-after-paren=4
289 |
290 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
291 | expected-line-ending-format=
292 |
293 |
294 | [MISCELLANEOUS]
295 |
296 | # List of note tags to take in consideration, separated by a comma.
297 | notes=TODO
298 |
299 |
300 | [STRING]
301 |
302 | # This flag controls whether inconsistent-quotes generates a warning when the
303 | # character used as a quote delimiter is used inconsistently within a module.
304 | check-quote-consistency=yes
305 |
306 |
307 | [VARIABLES]
308 |
309 | # Tells whether we should check for unused import in __init__ files.
310 | init-import=no
311 |
312 | # A regular expression matching the name of dummy variables (i.e. expectedly
313 | # not used).
314 | dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
315 |
316 | # List of additional names supposed to be defined in builtins. Remember that
317 | # you should avoid to define new builtins when possible.
318 | additional-builtins=
319 |
320 | # List of strings which can identify a callback function by name. A callback
321 | # name must start or end with one of those strings.
322 | callbacks=cb_,_cb
323 |
324 | # List of qualified module names which can have objects that can redefine
325 | # builtins.
326 | redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
327 |
328 |
329 | [LOGGING]
330 |
331 | # Logging modules to check that the string format arguments are in logging
332 | # function parameter format
333 | logging-modules=logging,absl.logging,tensorflow.io.logging
334 |
335 |
336 | [SIMILARITIES]
337 |
338 | # Minimum lines number of a similarity.
339 | min-similarity-lines=4
340 |
341 | # Ignore comments when computing similarities.
342 | ignore-comments=yes
343 |
344 | # Ignore docstrings when computing similarities.
345 | ignore-docstrings=yes
346 |
347 | # Ignore imports when computing similarities.
348 | ignore-imports=no
349 |
350 |
351 | [SPELLING]
352 |
353 | # Spelling dictionary name. Available dictionaries: none. To make it working
354 | # install python-enchant package.
355 | spelling-dict=
356 |
357 | # List of comma separated words that should not be checked.
358 | spelling-ignore-words=
359 |
360 | # A path to a file that contains private dictionary; one word per line.
361 | spelling-private-dict-file=
362 |
363 | # Tells whether to store unknown words to indicated private dictionary in
364 | # --spelling-private-dict-file option instead of raising a message.
365 | spelling-store-unknown-words=no
366 |
367 |
368 | [IMPORTS]
369 |
370 | # Deprecated modules which should not be used, separated by a comma
371 | deprecated-modules=regsub,
372 | TERMIOS,
373 | Bastion,
374 | rexec,
375 | sets
376 |
377 | # Create a graph of every (i.e. internal and external) dependencies in the
378 | # given file (report RP0402 must not be disabled)
379 | import-graph=
380 |
381 | # Create a graph of external dependencies in the given file (report RP0402 must
382 | # not be disabled)
383 | ext-import-graph=
384 |
385 | # Create a graph of internal dependencies in the given file (report RP0402 must
386 | # not be disabled)
387 | int-import-graph=
388 |
389 | # Force import order to recognize a module as part of the standard
390 | # compatibility libraries.
391 | known-standard-library=
392 |
393 | # Force import order to recognize a module as part of a third party library.
394 | known-third-party=enchant, absl
395 |
396 | # Analyse import fallback blocks. This can be used to support both Python 2 and
397 | # 3 compatible code, which means that the block might have code that exists
398 | # only in one or another interpreter, leading to false positives when analysed.
399 | analyse-fallback-blocks=no
400 |
401 |
402 | [CLASSES]
403 |
404 | # List of method names used to declare (i.e. assign) instance attributes.
405 | defining-attr-methods=__init__,
406 | __new__,
407 | setUp
408 |
409 | # List of member names, which should be excluded from the protected access
410 | # warning.
411 | exclude-protected=_asdict,
412 | _fields,
413 | _replace,
414 | _source,
415 | _make
416 |
417 | # List of valid names for the first argument in a class method.
418 | valid-classmethod-first-arg=cls,
419 | class_
420 |
421 | # List of valid names for the first argument in a metaclass class method.
422 | valid-metaclass-classmethod-first-arg=mcs
423 |
424 |
425 | [EXCEPTIONS]
426 |
427 | # Exceptions that will emit a warning when being caught. Defaults to
428 | # "Exception"
429 | overgeneral-exceptions=builtins.StandardError,
430 | builtins.Exception,
431 | builtins.BaseException
432 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "auto_gpt_email_plugin"
7 | version = "0.0.1"
8 | authors = [
9 | { name="Riensen", email="3340218+riensen@users.noreply.github.com" },
10 | ]
11 | description = "The Auto-GPT Email Plugin"
12 | readme = "README.md"
13 | requires-python = ">=3.9"
14 | classifiers = [
15 | "Programming Language :: Python :: 3",
16 | "License :: OSI Approved :: MIT License",
17 | "Operating System :: OS Independent",
18 | ]
19 | dependencies = ["abstract-singleton"]
20 |
21 | [project.urls]
22 | "Homepage" = "https://github.com/riensen/Auto-GPT-Email-Plugin"
23 | "Bug Tracker" = "https://github.com/riensen/Auto-GPT-Email-Plugin/issues"
24 |
25 | [tool.black]
26 | line-length = 88
27 | target-version = ['py310']
28 | include = '\.pyi?$'
29 | extend-exclude = ""
30 |
31 | [tool.isort]
32 | profile = "black"
33 |
34 | [tool.pylint.messages_control]
35 | disable = "C0330, C0326"
36 |
37 | [tool.pylint.format]
38 | max-line-length = "88"
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | black
2 | isort
3 | flake8
4 | pylint
5 | abstract-singleton
6 | wheel
7 | setuptools
8 | build
9 | twine
10 | auto_gpt_plugin_template
11 | colorama
12 |
--------------------------------------------------------------------------------
/run_pylint.py:
--------------------------------------------------------------------------------
1 | """
2 | https://stackoverflow.com/questions/49100806/
3 | pylint-and-subprocess-run-returning-exit-status-28
4 | """
5 | import subprocess
6 |
7 | cmd = " pylint src\\**\\*"
8 | try:
9 | subprocComplete = subprocess.run(
10 | cmd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
11 | )
12 | print(subprocComplete.stdout.decode("utf-8"))
13 | except subprocess.CalledProcessError as err:
14 | print(err.output.decode("utf-8"))
15 |
--------------------------------------------------------------------------------
/sourcery.yaml:
--------------------------------------------------------------------------------
1 | # 🪄 This is your project's Sourcery configuration file.
2 |
3 | # You can use it to get Sourcery working in the way you want, such as
4 | # ignoring specific refactorings, skipping directories in your project,
5 | # or writing custom rules.
6 |
7 | # 📚 For a complete reference to this file, see the documentation at
8 | # https://docs.sourcery.ai/Configuration/Project-Settings/
9 |
10 | # This file was auto-generated by Sourcery on 2023-02-25 at 21:07.
11 |
12 | version: "1" # The schema version of this config file
13 |
14 | ignore: # A list of paths or files which Sourcery will ignore.
15 | - .git
16 | - venv
17 | - .venv
18 | - build
19 | - dist
20 | - env
21 | - .env
22 | - .tox
23 |
24 | rule_settings:
25 | enable:
26 | - default
27 | - gpsg
28 | disable: [] # A list of rule IDs Sourcery will never suggest.
29 | rule_types:
30 | - refactoring
31 | - suggestion
32 | - comment
33 | python_version: "3.9" # A string specifying the lowest Python version your project supports. Sourcery will not suggest refactorings requiring a higher Python version.
34 |
35 | # rules: # A list of custom rules Sourcery will include in its analysis.
36 | # - id: no-print-statements
37 | # description: Do not use print statements in the test directory.
38 | # pattern: print(...)
39 | # language: python
40 | # replacement:
41 | # condition:
42 | # explanation:
43 | # paths:
44 | # include:
45 | # - test
46 | # exclude:
47 | # - conftest.py
48 | # tests: []
49 | # tags: []
50 |
51 | # rule_tags: {} # Additional rule tags.
52 |
53 | # metrics:
54 | # quality_threshold: 25.0
55 |
56 | # github:
57 | # labels: []
58 | # ignore_labels:
59 | # - sourcery-ignore
60 | # request_review: author
61 | # sourcery_branch: sourcery/{base_branch}
62 |
63 | # clone_detection:
64 | # min_lines: 3
65 | # min_duplicates: 2
66 | # identical_clones_only: false
67 |
68 | # proxy:
69 | # url:
70 | # ssl_certs_file:
71 | # no_ssl_verify: false
72 |
--------------------------------------------------------------------------------
/src/autogpt_plugins/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riensen/Auto-GPT-Email-Plugin/22f6e50a410ab3947fd44a59dc9cc904bbfb22b9/src/autogpt_plugins/__init__.py
--------------------------------------------------------------------------------
/src/autogpt_plugins/email/__init__.py:
--------------------------------------------------------------------------------
1 | """This is the email plugin for Auto-GPT."""
2 | from typing import Any, Dict, List, Optional, Tuple, TypeVar, TypedDict
3 | from auto_gpt_plugin_template import AutoGPTPluginTemplate
4 | from colorama import Fore
5 |
6 | PromptGenerator = TypeVar("PromptGenerator")
7 |
8 |
9 | class Message(TypedDict):
10 | role: str
11 | content: str
12 |
13 |
14 | class AutoGPTEmailPlugin(AutoGPTPluginTemplate):
15 | """
16 | This is the Auto-GPT email plugin.
17 | """
18 |
19 | def __init__(self):
20 | super().__init__()
21 | self._name = "Auto-GPT-Email-Plugin"
22 | self._version = "0.1.3"
23 | self._description = "Auto-GPT Email Plugin: Supercharge email management."
24 |
25 | def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator:
26 | from .email_plugin.email_plugin import (
27 | read_emails,
28 | send_email,
29 | send_email_with_attachment,
30 | bothEmailAndPwdSet,
31 | )
32 |
33 | if bothEmailAndPwdSet():
34 | prompt.add_command(
35 | "Read Emails",
36 | "read_emails",
37 | {
38 | "imap_folder": "",
39 | "imap_search_command": "",
40 | },
41 | read_emails,
42 | )
43 | prompt.add_command(
44 | "Send Email",
45 | "send_email",
46 | {"to": "", "subject": "", "body": ""},
47 | send_email,
48 | )
49 | prompt.add_command(
50 | "Send Email",
51 | "send_email_with_attachment",
52 | {
53 | "to": "",
54 | "subject": "",
55 | "body": "",
56 | "filename": "",
57 | },
58 | send_email_with_attachment,
59 | )
60 | else:
61 | print(
62 | Fore.RED
63 | + f"{self._name} - {self._version} - Email plugin not loaded, because EMAIL_PASSWORD or EMAIL_ADDRESS were not set in env."
64 | )
65 |
66 | return prompt
67 |
68 | def can_handle_post_prompt(self) -> bool:
69 | """This method is called to check that the plugin can
70 | handle the post_prompt method.
71 |
72 | Returns:
73 | bool: True if the plugin can handle the post_prompt method."""
74 | return True
75 |
76 | def can_handle_on_response(self) -> bool:
77 | """This method is called to check that the plugin can
78 | handle the on_response method.
79 |
80 | Returns:
81 | bool: True if the plugin can handle the on_response method."""
82 | return False
83 |
84 | def on_response(self, response: str, *args, **kwargs) -> str:
85 | """This method is called when a response is received from the model."""
86 | pass
87 |
88 | def can_handle_on_planning(self) -> bool:
89 | """This method is called to check that the plugin can
90 | handle the on_planning method.
91 |
92 | Returns:
93 | bool: True if the plugin can handle the on_planning method."""
94 | return False
95 |
96 | def on_planning(
97 | self, prompt: PromptGenerator, messages: List[Message]
98 | ) -> Optional[str]:
99 | """This method is called before the planning chat completion is done.
100 |
101 | Args:
102 | prompt (PromptGenerator): The prompt generator.
103 | messages (List[str]): The list of messages.
104 | """
105 | pass
106 |
107 | def can_handle_post_planning(self) -> bool:
108 | """This method is called to check that the plugin can
109 | handle the post_planning method.
110 |
111 | Returns:
112 | bool: True if the plugin can handle the post_planning method."""
113 | return False
114 |
115 | def post_planning(self, response: str) -> str:
116 | """This method is called after the planning chat completion is done.
117 |
118 | Args:
119 | response (str): The response.
120 |
121 | Returns:
122 | str: The resulting response.
123 | """
124 | pass
125 |
126 | def can_handle_pre_instruction(self) -> bool:
127 | """This method is called to check that the plugin can
128 | handle the pre_instruction method.
129 |
130 | Returns:
131 | bool: True if the plugin can handle the pre_instruction method."""
132 | return False
133 |
134 | def pre_instruction(self, messages: List[Message]) -> List[Message]:
135 | """This method is called before the instruction chat is done.
136 |
137 | Args:
138 | messages (List[Message]): The list of context messages.
139 |
140 | Returns:
141 | List[Message]: The resulting list of messages.
142 | """
143 | pass
144 |
145 | def can_handle_on_instruction(self) -> bool:
146 | """This method is called to check that the plugin can
147 | handle the on_instruction method.
148 |
149 | Returns:
150 | bool: True if the plugin can handle the on_instruction method."""
151 | return False
152 |
153 | def on_instruction(self, messages: List[Message]) -> Optional[str]:
154 | """This method is called when the instruction chat is done.
155 |
156 | Args:
157 | messages (List[Message]): The list of context messages.
158 |
159 | Returns:
160 | Optional[str]: The resulting message.
161 | """
162 | pass
163 |
164 | def can_handle_post_instruction(self) -> bool:
165 | """This method is called to check that the plugin can
166 | handle the post_instruction method.
167 |
168 | Returns:
169 | bool: True if the plugin can handle the post_instruction method."""
170 | return False
171 |
172 | def post_instruction(self, response: str) -> str:
173 | """This method is called after the instruction chat is done.
174 |
175 | Args:
176 | response (str): The response.
177 |
178 | Returns:
179 | str: The resulting response.
180 | """
181 | pass
182 |
183 | def can_handle_pre_command(self) -> bool:
184 | """This method is called to check that the plugin can
185 | handle the pre_command method.
186 |
187 | Returns:
188 | bool: True if the plugin can handle the pre_command method."""
189 | return False
190 |
191 | def pre_command(
192 | self, command_name: str, arguments: Dict[str, Any]
193 | ) -> Tuple[str, Dict[str, Any]]:
194 | """This method is called before the command is executed.
195 |
196 | Args:
197 | command_name (str): The command name.
198 | arguments (Dict[str, Any]): The arguments.
199 |
200 | Returns:
201 | Tuple[str, Dict[str, Any]]: The command name and the arguments.
202 | """
203 | pass
204 |
205 | def can_handle_post_command(self) -> bool:
206 | """This method is called to check that the plugin can
207 | handle the post_command method.
208 |
209 | Returns:
210 | bool: True if the plugin can handle the post_command method."""
211 | return False
212 |
213 | def post_command(self, command_name: str, response: str) -> str:
214 | """This method is called after the command is executed.
215 |
216 | Args:
217 | command_name (str): The command name.
218 | response (str): The response.
219 |
220 | Returns:
221 | str: The resulting response.
222 | """
223 | pass
224 |
225 | def can_handle_chat_completion(
226 | self, messages: Dict[Any, Any], model: str, temperature: float, max_tokens: int
227 | ) -> bool:
228 | """This method is called to check that the plugin can
229 | handle the chat_completion method.
230 |
231 | Args:
232 | messages (List[Message]): The messages.
233 | model (str): The model name.
234 | temperature (float): The temperature.
235 | max_tokens (int): The max tokens.
236 |
237 | Returns:
238 | bool: True if the plugin can handle the chat_completion method."""
239 | return False
240 |
241 | def handle_chat_completion(
242 | self, messages: List[Message], model: str, temperature: float, max_tokens: int
243 | ) -> str:
244 | """This method is called when the chat completion is done.
245 |
246 | Args:
247 | messages (List[Message]): The messages.
248 | model (str): The model name.
249 | temperature (float): The temperature.
250 | max_tokens (int): The max tokens.
251 |
252 | Returns:
253 | str: The resulting response.
254 | """
255 | pass
256 |
--------------------------------------------------------------------------------
/src/autogpt_plugins/email/email_plugin/email_plugin.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import smtplib
4 | import email
5 | import imaplib
6 | import mimetypes
7 | import time
8 | from email.header import decode_header
9 | from email.message import EmailMessage
10 | import re
11 |
12 |
13 | def bothEmailAndPwdSet() -> bool:
14 | return True if os.getenv("EMAIL_ADDRESS") and os.getenv("EMAIL_PASSWORD") else False
15 |
16 |
17 | def getSender():
18 | email_sender = os.getenv("EMAIL_ADDRESS")
19 | if not email_sender:
20 | return "Error: email not sent. EMAIL_ADDRESS not set in environment."
21 | return email_sender
22 |
23 |
24 | def getPwd():
25 | email_password = os.getenv("EMAIL_PASSWORD")
26 | if not email_password:
27 | return "Error: email not sent. EMAIL_PASSWORD not set in environment."
28 | return email_password
29 |
30 |
31 | def send_email(to: str, subject: str, body: str) -> str:
32 | return send_email_with_attachment_internal(to, subject, body, None, None)
33 |
34 |
35 | def send_email_with_attachment(to: str, subject: str, body: str, filename: str) -> str:
36 | attachment_path = filename
37 | attachment = os.path.basename(filename)
38 | return send_email_with_attachment_internal(
39 | to, subject, body, attachment_path, attachment
40 | )
41 |
42 |
43 | def send_email_with_attachment_internal(
44 | to: str, title: str, message: str, attachment_path: str, attachment: str
45 | ) -> str:
46 | """Send an email
47 |
48 | Args:
49 | to (str): The email of the recipient
50 | title (str): The title of the email
51 | message (str): The message content of the email
52 |
53 | Returns:
54 | str: Any error messages
55 | """
56 | email_sender = getSender()
57 | email_password = getPwd()
58 |
59 | msg = EmailMessage()
60 | msg["Subject"] = title
61 | msg["From"] = email_sender
62 | msg["To"] = to
63 |
64 | signature = os.getenv("EMAIL_SIGNATURE")
65 | if signature:
66 | message += f"\n{signature}"
67 |
68 | msg.set_content(message)
69 |
70 | if attachment_path:
71 | ctype, encoding = mimetypes.guess_type(attachment_path)
72 | if ctype is None or encoding is not None:
73 | # No guess could be made, or the file is encoded (compressed)
74 | ctype = "application/octet-stream"
75 | maintype, subtype = ctype.split("/", 1)
76 | with open(attachment_path, "rb") as fp:
77 | msg.add_attachment(
78 | fp.read(), maintype=maintype, subtype=subtype, filename=attachment
79 | )
80 |
81 | draft_folder = os.getenv("EMAIL_DRAFT_MODE_WITH_FOLDER")
82 |
83 | if not draft_folder:
84 | smtp_host = os.getenv("EMAIL_SMTP_HOST")
85 | smtp_port = os.getenv("EMAIL_SMTP_PORT")
86 | # send email
87 | with smtplib.SMTP(smtp_host, smtp_port) as smtp:
88 | smtp.ehlo()
89 | smtp.starttls()
90 | smtp.login(email_sender, email_password)
91 | smtp.send_message(msg)
92 | smtp.quit()
93 | return f"Email was sent to {to}!"
94 | else:
95 | conn = imap_open(draft_folder, email_sender, email_password)
96 | conn.append(
97 | draft_folder,
98 | "",
99 | imaplib.Time2Internaldate(time.time()),
100 | str(msg).encode("UTF-8"),
101 | )
102 | return f"Email went to {draft_folder}!"
103 |
104 |
105 | def read_emails(imap_folder: str = "inbox", imap_search_command: str = "UNSEEN") -> str:
106 | """Read emails from an IMAP mailbox.
107 |
108 | This function reads emails from a specified IMAP folder, using a given IMAP search command.
109 | It returns a list of emails with their details, including the sender, recipient, date, CC, subject, and message body.
110 |
111 | Args:
112 | imap_folder (str, optional): The name of the IMAP folder to read emails from. Defaults to "inbox".
113 | imap_search_command (str, optional): The IMAP search command to filter emails. Defaults to "UNSEEN".
114 |
115 | Returns:
116 | str: A list of dictionaries containing email details if there are any matching emails. Otherwise, returns
117 | a string indicating that no matching emails were found.
118 | """
119 | email_sender = getSender()
120 | imap_folder = adjust_imap_folder_for_gmail(imap_folder, email_sender)
121 | imap_folder = enclose_with_quotes(imap_folder)
122 | imap_search_ar = split_imap_search_command(imap_search_command)
123 | email_password = getPwd()
124 |
125 | mark_as_seen = os.getenv("EMAIL_MARK_AS_SEEN")
126 | if isinstance(mark_as_seen, str):
127 | mark_as_seen = json.loads(mark_as_seen.lower())
128 |
129 | conn = imap_open(imap_folder, email_sender, email_password)
130 |
131 | imap_keyword = imap_search_ar[0]
132 | if len(imap_search_ar) == 1:
133 | _, search_data = conn.search(None, imap_keyword)
134 | else:
135 | argument = enclose_with_quotes(imap_search_ar[1])
136 | _, search_data = conn.search(None, imap_keyword, argument)
137 |
138 | messages = []
139 | for num in search_data[0].split():
140 | if mark_as_seen:
141 | message_parts = "(RFC822)"
142 | else:
143 | message_parts = "(BODY.PEEK[])"
144 | _, msg_data = conn.fetch(num, message_parts)
145 | for response_part in msg_data:
146 | if isinstance(response_part, tuple):
147 | msg = email.message_from_bytes(response_part[1])
148 |
149 | subject, encoding = decode_header(msg["Subject"])[0]
150 | if isinstance(subject, bytes):
151 | subject = subject.decode(encoding)
152 |
153 | body = get_email_body(msg)
154 | from_address = msg["From"]
155 | to_address = msg["To"]
156 | date = msg["Date"]
157 | cc = msg["CC"] if msg["CC"] else ""
158 |
159 | messages.append(
160 | {
161 | "From": from_address,
162 | "To": to_address,
163 | "Date": date,
164 | "CC": cc,
165 | "Subject": subject,
166 | "Message Body": body,
167 | }
168 | )
169 |
170 | conn.logout()
171 | if not messages:
172 | return (
173 | f"There are no Emails in your folder `{imap_folder}` "
174 | f"when searching with imap command `{imap_search_command}`"
175 | )
176 | return messages
177 |
178 |
179 | def adjust_imap_folder_for_gmail(imap_folder: str, email_sender: str) -> str:
180 | if "@gmail" in email_sender.lower() or "@googlemail" in email_sender.lower():
181 | if "sent" in imap_folder.lower():
182 | return '"[Gmail]/Sent Mail"'
183 | if "draft" in imap_folder.lower():
184 | return "[Gmail]/Drafts"
185 | return imap_folder
186 |
187 |
188 | def imap_open(
189 | imap_folder: str, email_sender: str, email_password: str
190 | ) -> imaplib.IMAP4_SSL:
191 | imap_server = os.getenv("EMAIL_IMAP_SERVER")
192 | conn = imaplib.IMAP4_SSL(imap_server)
193 | conn.login(email_sender, email_password)
194 | conn.select(imap_folder)
195 | return conn
196 |
197 |
198 | def get_email_body(msg: email.message.Message) -> str:
199 | if msg.is_multipart():
200 | for part in msg.walk():
201 | content_type = part.get_content_type()
202 | content_disposition = str(part.get("Content-Disposition"))
203 | if content_type == "text/plain" and "attachment" not in content_disposition:
204 | return part.get_payload(decode=True).decode()
205 | else:
206 | return msg.get_payload(decode=True).decode()
207 |
208 |
209 | def enclose_with_quotes(s):
210 | # Check if string contains whitespace
211 | has_whitespace = bool(re.search(r"\s", s))
212 |
213 | # Check if string is already enclosed by quotes
214 | is_enclosed = s.startswith(("'", '"')) and s.endswith(("'", '"'))
215 |
216 | # If string has whitespace and is not enclosed by quotes, enclose it with double quotes
217 | if has_whitespace and not is_enclosed:
218 | return f'"{s}"'
219 | else:
220 | return s
221 |
222 |
223 | def split_imap_search_command(input_string):
224 | input_string = input_string.strip()
225 | parts = input_string.split(maxsplit=1)
226 | parts = [part.strip() for part in parts]
227 |
228 | return parts
229 |
--------------------------------------------------------------------------------
/src/autogpt_plugins/email/email_plugin/test_email_plugin.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest.mock import patch
3 | from email.message import EmailMessage
4 | from email_plugin import (
5 | send_email,
6 | read_emails,
7 | imap_open,
8 | send_email_with_attachment_internal,
9 | bothEmailAndPwdSet,
10 | adjust_imap_folder_for_gmail,
11 | enclose_with_quotes,
12 | split_imap_search_command,
13 | )
14 | from unittest.mock import mock_open
15 | import unittest
16 | from functools import partial
17 |
18 | MOCK_FROM = "sender@example.com"
19 | MOCK_PWD = "secret"
20 | MOCK_TO = "test@example.com"
21 | MOCK_DATE = "Fri, 21 Apr 2023 10:00:00 -0000"
22 | MOCK_CONTENT = "Test message\n"
23 | MOCK_SUBJECT = "Test Subject"
24 | MOCK_IMAP_SERVER = "imap.example.com"
25 | MOCK_SMTP_SERVER = "smtp.example.com"
26 | MOCK_SMTP_PORT = "587"
27 |
28 | MOCK_DRAFT_FOLDER = "Example/Drafts"
29 | MOCK_ATTACHMENT_PATH = "example/file.txt"
30 | MOCK_ATTACHMENT_NAME = "file.txt"
31 |
32 |
33 | class TestEmailPlugin(unittest.TestCase):
34 | @patch.dict(
35 | os.environ,
36 | {
37 | "EMAIL_ADDRESS": "test@example.com",
38 | "EMAIL_PASSWORD": "test_password",
39 | },
40 | )
41 | def test_both_email_and_pwd_set(self):
42 | self.assertTrue(bothEmailAndPwdSet())
43 |
44 | @patch.dict(
45 | os.environ,
46 | {
47 | "EMAIL_PASSWORD": "test_password",
48 | },
49 | clear=True,
50 | )
51 | def test_email_not_set(self):
52 | self.assertFalse(bothEmailAndPwdSet())
53 |
54 | @patch.dict(
55 | os.environ,
56 | {
57 | "EMAIL_ADDRESS": "",
58 | "EMAIL_PASSWORD": "test_password",
59 | },
60 | clear=True,
61 | )
62 | def test_email_not_set_2(self):
63 | self.assertFalse(bothEmailAndPwdSet())
64 |
65 | @patch.dict(
66 | os.environ,
67 | {
68 | "EMAIL_ADDRESS": "test@example.com",
69 | },
70 | clear=True,
71 | )
72 | def test_pwd_not_set(self):
73 | self.assertFalse(bothEmailAndPwdSet())
74 |
75 | @patch.dict(os.environ, {}, clear=True)
76 | def test_both_email_and_pwd_not_set(self):
77 | self.assertFalse(bothEmailAndPwdSet())
78 |
79 | def test_adjust_imap_folder_for_gmail_normal_cases(self):
80 | self.assertEqual(
81 | adjust_imap_folder_for_gmail("Sent", "user@gmail.com"),
82 | '"[Gmail]/Sent Mail"',
83 | )
84 | self.assertEqual(
85 | adjust_imap_folder_for_gmail("Drafts", "user@googlemail.com"),
86 | "[Gmail]/Drafts",
87 | )
88 | self.assertEqual(
89 | adjust_imap_folder_for_gmail("Inbox", "user@gmail.com"), "Inbox"
90 | )
91 |
92 | def test_adjust_imap_folder_for_gmail_case_insensitivity(self):
93 | self.assertEqual(
94 | adjust_imap_folder_for_gmail("SeNT", "user@GMail.com"),
95 | '"[Gmail]/Sent Mail"',
96 | )
97 | self.assertEqual(
98 | adjust_imap_folder_for_gmail("DRAFTS", "user@gOogLemail.com"),
99 | "[Gmail]/Drafts",
100 | )
101 | self.assertEqual(
102 | adjust_imap_folder_for_gmail("InbOx", "user@gmail.com"), "InbOx"
103 | )
104 |
105 | def test_adjust_imap_folder_for_gmail_non_gmail_sender(self):
106 | self.assertEqual(adjust_imap_folder_for_gmail("Sent", "user@yahoo.com"), "Sent")
107 | self.assertEqual(
108 | adjust_imap_folder_for_gmail("Drafts", "user@hotmail.com"), "Drafts"
109 | )
110 | self.assertEqual(
111 | adjust_imap_folder_for_gmail("SENT", "gmail@hotmail.com"), "SENT"
112 | )
113 |
114 | def test_adjust_imap_folder_for_gmail_edge_cases(self):
115 | self.assertEqual(adjust_imap_folder_for_gmail("", "user@gmail.com"), "")
116 | self.assertEqual(adjust_imap_folder_for_gmail("Inbox", ""), "Inbox")
117 | self.assertEqual(adjust_imap_folder_for_gmail("", ""), "")
118 |
119 | def test_enclose_with_quotes(self):
120 | assert enclose_with_quotes("REVERSE DATE") == '"REVERSE DATE"'
121 | assert enclose_with_quotes('"My Search"') == '"My Search"'
122 | assert enclose_with_quotes("'test me'") == "'test me'"
123 | assert enclose_with_quotes("ALL") == "ALL"
124 | assert enclose_with_quotes("quotes needed") == '"quotes needed"'
125 | assert enclose_with_quotes(" whitespace ") == '" whitespace "'
126 | assert enclose_with_quotes("whitespace\te") == '"whitespace\te"'
127 | assert enclose_with_quotes("\"mixed quotes'") == "\"mixed quotes'"
128 | assert enclose_with_quotes("'mixed quotes\"") == "'mixed quotes\""
129 |
130 | def test_split_imap_search_command(self):
131 | self.assertEqual(split_imap_search_command("SEARCH"), ["SEARCH"])
132 | self.assertEqual(
133 | split_imap_search_command("SEARCH UNSEEN"), ["SEARCH", "UNSEEN"]
134 | )
135 | self.assertEqual(
136 | split_imap_search_command(" SEARCH UNSEEN "), ["SEARCH", "UNSEEN"]
137 | )
138 | self.assertEqual(
139 | split_imap_search_command(
140 | "FROM speixoto@caicm.ca SINCE 01-JAN-2022 BEFORE 01-FEB-2023 HAS attachment xls OR HAS attachment xlsx"
141 | ),
142 | [
143 | "FROM",
144 | "speixoto@caicm.ca SINCE 01-JAN-2022 BEFORE 01-FEB-2023 HAS attachment xls OR HAS attachment xlsx",
145 | ],
146 | )
147 | self.assertEqual(
148 | split_imap_search_command("BODY here is my long body"),
149 | ["BODY", "here is my long body"],
150 | )
151 | self.assertEqual(split_imap_search_command(""), [])
152 |
153 | @patch("imaplib.IMAP4_SSL")
154 | @patch.dict(
155 | os.environ,
156 | {
157 | "EMAIL_ADDRESS": MOCK_FROM,
158 | "EMAIL_PASSWORD": MOCK_PWD,
159 | "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
160 | },
161 | )
162 | def test_imap_open(self, mock_imap):
163 | # Test imapOpen function
164 | imap_folder = "inbox"
165 | imap_open(imap_folder, MOCK_FROM, MOCK_PWD)
166 |
167 | # Check if the IMAP object was created and used correctly
168 | mock_imap.assert_called_once_with(MOCK_IMAP_SERVER)
169 | mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
170 | mock_imap.return_value.select.assert_called_once_with(imap_folder)
171 |
172 | # Test for successful email sending without attachment
173 | @patch("smtplib.SMTP", autospec=True)
174 | @patch.dict(
175 | os.environ,
176 | {
177 | "EMAIL_ADDRESS": MOCK_FROM,
178 | "EMAIL_PASSWORD": MOCK_PWD,
179 | "EMAIL_SMTP_HOST": MOCK_SMTP_SERVER,
180 | "EMAIL_SMTP_PORT": MOCK_SMTP_PORT,
181 | },
182 | )
183 | def test_send_email_no_attachment(self, mock_smtp):
184 | result = send_email(MOCK_TO, MOCK_SUBJECT, MOCK_CONTENT)
185 | assert result == f"Email was sent to {MOCK_TO}!"
186 |
187 | mock_smtp.assert_called_once_with(MOCK_SMTP_SERVER, MOCK_SMTP_PORT)
188 |
189 | # Check if the SMTP object was created and used correctly
190 | context = mock_smtp.return_value.__enter__.return_value
191 | context.ehlo.assert_called()
192 | context.starttls.assert_called_once()
193 | context.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
194 | context.send_message.assert_called_once()
195 | context.quit.assert_called_once()
196 |
197 | # Test for reading emails in a specific folder with a specific search command
198 | @patch("imaplib.IMAP4_SSL")
199 | @patch.dict(
200 | os.environ,
201 | {
202 | "EMAIL_ADDRESS": MOCK_FROM,
203 | "EMAIL_PASSWORD": MOCK_PWD,
204 | "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
205 | },
206 | )
207 | def test_read_emails(self, mock_imap):
208 | assert os.getenv("EMAIL_ADDRESS") == MOCK_FROM
209 |
210 | # Create a mock email message
211 | message = EmailMessage()
212 | message["From"] = MOCK_FROM
213 | message["To"] = MOCK_TO
214 | message["Date"] = MOCK_DATE
215 | message["Subject"] = MOCK_SUBJECT
216 | message.set_content(MOCK_CONTENT)
217 |
218 | # Set up mock IMAP server behavior
219 | mock_imap.return_value.search.return_value = (None, [b"1"])
220 | mock_imap.return_value.fetch.return_value = (None, [(b"1", message.as_bytes())])
221 |
222 | # Test read_emails function
223 | result = read_emails("inbox", "UNSEEN")
224 | expected_result = [
225 | {
226 | "From": MOCK_FROM,
227 | "To": MOCK_TO,
228 | "Date": MOCK_DATE,
229 | "CC": "",
230 | "Subject": MOCK_SUBJECT,
231 | "Message Body": MOCK_CONTENT,
232 | }
233 | ]
234 | assert result == expected_result
235 |
236 | # Check if the IMAP object was created and used correctly
237 | mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
238 | mock_imap.return_value.select.assert_called_once_with("inbox")
239 | mock_imap.return_value.search.assert_called_once_with(None, "UNSEEN")
240 | mock_imap.return_value.fetch.assert_called_once_with(b"1", "(BODY.PEEK[])")
241 |
242 | # Test for reading empty emails
243 | @patch("imaplib.IMAP4_SSL")
244 | @patch.dict(
245 | os.environ,
246 | {
247 | "EMAIL_ADDRESS": MOCK_FROM,
248 | "EMAIL_PASSWORD": MOCK_PWD,
249 | "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
250 | },
251 | )
252 | def test_read_empty_emails(self, mock_imap):
253 | assert os.getenv("EMAIL_ADDRESS") == MOCK_FROM
254 |
255 | # Set up mock IMAP server behavior
256 | mock_imap.return_value.search.return_value = (None, [b"0"])
257 | mock_imap.return_value.fetch.return_value = (None, [])
258 |
259 | # Test read_emails function
260 | result = read_emails("inbox", "UNSEEN")
261 | expected = "There are no Emails in your folder `inbox` "
262 | expected += "when searching with imap command `UNSEEN`"
263 | assert result == expected
264 |
265 | # Check if the IMAP object was created and used correctly
266 | mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
267 | mock_imap.return_value.select.assert_called_once_with("inbox")
268 | mock_imap.return_value.search.assert_called_once_with(None, "UNSEEN")
269 | mock_imap.return_value.fetch.assert_called_once_with(b"0", "(BODY.PEEK[])")
270 |
271 | # Test for reading emails in a specific folder
272 | # with a specific search command with EMAIL_MARK_AS_SEEN=True
273 | @patch("imaplib.IMAP4_SSL")
274 | @patch.dict(
275 | os.environ,
276 | {
277 | "EMAIL_ADDRESS": MOCK_FROM,
278 | "EMAIL_PASSWORD": MOCK_PWD,
279 | "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
280 | "EMAIL_MARK_AS_SEEN": "True",
281 | },
282 | )
283 | def test_read_emails_mark_as_read_true(self, mock_imap):
284 | assert os.getenv("EMAIL_ADDRESS") == MOCK_FROM
285 |
286 | # Create a mock email message
287 | message = EmailMessage()
288 | message["From"] = MOCK_FROM
289 | message["To"] = MOCK_TO
290 | message["Date"] = MOCK_DATE
291 | message["Subject"] = MOCK_SUBJECT
292 | message.set_content(MOCK_CONTENT)
293 |
294 | # Set up mock IMAP server behavior
295 | mock_imap.return_value.search.return_value = (None, [b"1"])
296 | mock_imap.return_value.fetch.return_value = (None, [(b"1", message.as_bytes())])
297 |
298 | # Test read_emails function
299 | result = read_emails("inbox", "UNSEEN")
300 | expected_result = [
301 | {
302 | "From": MOCK_FROM,
303 | "To": MOCK_TO,
304 | "Date": MOCK_DATE,
305 | "CC": "",
306 | "Subject": MOCK_SUBJECT,
307 | "Message Body": MOCK_CONTENT,
308 | }
309 | ]
310 | assert result == expected_result
311 |
312 | # Check if the IMAP object was created and used correctly
313 | mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
314 | mock_imap.return_value.select.assert_called_once_with("inbox")
315 | mock_imap.return_value.search.assert_called_once_with(None, "UNSEEN")
316 | mock_imap.return_value.fetch.assert_called_once_with(b"1", "(RFC822)")
317 |
318 | # Test for reading emails in a specific folder
319 | # with a specific search command with EMAIL_MARK_AS_SEEN=False
320 | @patch("imaplib.IMAP4_SSL")
321 | @patch.dict(
322 | os.environ,
323 | {
324 | "EMAIL_ADDRESS": MOCK_FROM,
325 | "EMAIL_PASSWORD": MOCK_PWD,
326 | "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
327 | "EMAIL_MARK_AS_SEEN": "False",
328 | },
329 | )
330 | def test_read_emails_mark_as_seen_false(self, mock_imap):
331 | assert os.getenv("EMAIL_ADDRESS") == MOCK_FROM
332 |
333 | # Create a mock email message
334 | message = EmailMessage()
335 | message["From"] = MOCK_FROM
336 | message["To"] = MOCK_TO
337 | message["Date"] = MOCK_DATE
338 | message["Subject"] = MOCK_SUBJECT
339 | message.set_content(MOCK_CONTENT)
340 |
341 | # Set up mock IMAP server behavior
342 | mock_imap.return_value.search.return_value = (None, [b"1"])
343 | mock_imap.return_value.fetch.return_value = (None, [(b"1", message.as_bytes())])
344 |
345 | # Test read_emails function
346 | result = read_emails("inbox", "UNSEEN")
347 | expected_result = [
348 | {
349 | "From": MOCK_FROM,
350 | "To": MOCK_TO,
351 | "Date": MOCK_DATE,
352 | "CC": "",
353 | "Subject": MOCK_SUBJECT,
354 | "Message Body": MOCK_CONTENT,
355 | }
356 | ]
357 | assert result == expected_result
358 |
359 | # Check if the IMAP object was created and used correctly
360 | mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
361 | mock_imap.return_value.select.assert_called_once_with("inbox")
362 | mock_imap.return_value.search.assert_called_once_with(None, "UNSEEN")
363 | mock_imap.return_value.fetch.assert_called_once_with(b"1", "(BODY.PEEK[])")
364 |
365 | def side_effect_for_open(original_open, file_path, *args, **kwargs):
366 | if file_path == MOCK_ATTACHMENT_PATH:
367 | return mock_open(read_data=b"file_content").return_value
368 | return original_open(file_path, *args, **kwargs)
369 |
370 | original_open = open
371 | side_effect_with_original_open = partial(side_effect_for_open, original_open)
372 |
373 | # Test for sending emails with EMAIL_DRAFT_MODE_WITH_FOLDER
374 | @patch("imaplib.IMAP4_SSL")
375 | @patch.dict(
376 | os.environ,
377 | {
378 | "EMAIL_ADDRESS": MOCK_FROM,
379 | "EMAIL_PASSWORD": MOCK_PWD,
380 | "EMAIL_IMAP_SERVER": MOCK_IMAP_SERVER,
381 | "EMAIL_DRAFT_MODE_WITH_FOLDER": MOCK_DRAFT_FOLDER,
382 | },
383 | )
384 | @patch(f"{__name__}.imap_open")
385 | @patch("builtins.open", side_effect=side_effect_with_original_open)
386 | def test_send_emails_with_draft_mode(self, mock_file, mock_imap_open, mock_imap):
387 | mock_imap_conn = mock_imap_open.return_value
388 | mock_imap_conn.select.return_value = ("OK", [b"0"])
389 | mock_imap_conn.append.return_value = ("OK", [b"1"])
390 |
391 | result = send_email_with_attachment_internal(
392 | MOCK_TO,
393 | MOCK_SUBJECT,
394 | MOCK_CONTENT,
395 | MOCK_ATTACHMENT_PATH,
396 | MOCK_ATTACHMENT_NAME,
397 | )
398 | assert result == f"Email went to {MOCK_DRAFT_FOLDER}!"
399 | mock_imap.return_value.login.assert_called_once_with(MOCK_FROM, MOCK_PWD)
400 | mock_imap.return_value.select.assert_called_once_with(MOCK_DRAFT_FOLDER)
401 |
402 | # Get the actual MIME message appended
403 | mock_imap.return_value.append.assert_called_once()
404 |
405 | append_args, _ = mock_imap.return_value.append.call_args
406 | actual_mime_msg = append_args[3].decode("utf-8")
407 |
408 | # Check for the presence of relevant information in the MIME message
409 | assert MOCK_FROM in actual_mime_msg
410 | assert MOCK_TO in actual_mime_msg
411 | assert MOCK_SUBJECT in actual_mime_msg
412 | assert MOCK_CONTENT in actual_mime_msg
413 | assert MOCK_ATTACHMENT_NAME in actual_mime_msg
414 |
415 |
416 | if __name__ == "__main__":
417 | unittest.main()
418 |
--------------------------------------------------------------------------------