├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose.yml ├── emailProcessingUtils ├── fetchThread.js ├── getThreadId.js └── gmailParser.js ├── imapConfig.js ├── index.js ├── openai └── openai.js ├── package-lock.json └── package.json /.env.example: -------------------------------------------------------------------------------- 1 | EMAIL_USER='youremail@gmail.com' 2 | EMAIL_PASS='your-password' 3 | OPENAI_API_KEY='ChatGPT-API-KEY' 4 | SYSTEM_PROMPT='You are a helpful assistant.' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # next.js build output 62 | .next 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tancrede GEANT 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Email Assistant 2 | 3 | This is a simple email assistant application that reads incoming emails, processes them using the OpenAI GPT-3.5 API, and sends responses. It uses Node.js, IMAP for reading emails, Nodemailer for sending emails, and the OpenAI API for natural language processing. 4 | 5 | ## Prerequisites 6 | 7 | - Node.js v14.20.0 or later 8 | - A Gmail account with "Allow less secure apps" enabled or an App Password generated 9 | - OpenAI API Key (You will be provided with the necessary information) 10 | 11 | ## Gmail Account Setup 12 | 13 | To use this application with your Gmail account, you need to enable "Allow less secure apps" or generate an App Password. Follow the steps below: 14 | 15 | ### Option 1: Allow Less Secure Apps 16 | 17 | 1. Sign in to your Google Account. 18 | 2. Go to the [Less secure apps](https://myaccount.google.com/lesssecureapps) page. 19 | 3. Turn on "Allow less secure apps." 20 | 21 | ### Option 2: Generate an App Password (recommended) 22 | 23 | 1. Sign in to your Google Account. 24 | 2. Go to the [App passwords](https://myaccount.google.com/apppasswords) page. 25 | 3. Select "Mail" as the app and "Other (Custom name)" as the device. 26 | 4. Enter a custom name (e.g., "Email Assistant") and click "Generate." 27 | 5. Copy the generated 16-character App Password and use it as the `PASSWORD` in your `.env` file. 28 | 29 | **Important:** Do not share your App Password or use it for any other purpose. 30 | 31 | ## OpenAI API Key 32 | 33 | To obtain your OpenAI API Key, follow these steps: 34 | 35 | 1. Go to the [OpenAI API Keys](https://platform.openai.com/account/api-keys) page. 36 | 2. Sign in with your OpenAI account or create one if you don't have it already. 37 | 3. After signing in, you will see a list of your API keys. If you don't have any API keys yet, click the "Create" button to generate one. 38 | 4. Copy the secret API key (it starts with `sk-`) and paste it into the `OPENAI_API_KEY` variable in your `.env` file. 39 | 40 | **Important:** Do not share your API key with others or expose it in the browser or other client-side code. In order to protect the security of your account, OpenAI may automatically rotate any API key that they've found has leaked publicly. 41 | 42 | ## Setup 43 | 44 | 1. Clone this repository and navigate to the project directory: 45 | 46 | ```bash 47 | git clone https://github.com/tgeant/gmail-chatgpt-assistant.git 48 | cd gmail-chatgpt-assistant 49 | ``` 50 | 51 | 2. Install the required dependencies: 52 | 53 | ```bash 54 | npm install 55 | ``` 56 | 57 | 3. Create a `.env` file in the root directory of the project and add the necessary environment variables: 58 | 59 | ``` 60 | EMAIL=myemail@gmail.com 61 | PASSWORD=my_password 62 | OPENAI_API_KEY=my_api_key 63 | SYSTEM_PROMPT=You are a helpful assistant. 64 | ``` 65 | 66 | Replace `myemail@gmail.com` with your Gmail address, `my_password` with your Gmail password or App Password, and `my_api_key` with your OpenAI API key. The `SYSTEM_PROMPT` variable contains the system message used to set the context for the GPT-3.5 API. 67 | 68 | ## Running the Application 69 | 70 | You can run the application using either `npm` or `Docker`. 71 | 72 | ### Using npm 73 | 74 | To start the application using npm, run the following command: 75 | 76 | ```bash 77 | npm start 78 | ``` 79 | 80 | ### Using Docker 81 | 82 | 1. Build the Docker image: 83 | 84 | ```bash 85 | docker build -t email-assistant . 86 | ``` 87 | 88 | 2. Run the Docker container: 89 | 90 | ```bash 91 | docker run -d --name email-assistant --env-file .env -v $(pwd):/app email-assistant sh -c "npm install && npm start" 92 | ``` 93 | 94 | This will run the container in the background, use the environment variables from the `.env` file, and mount the current directory to the container's working directory. 95 | 96 | ### Using Docker Compose 97 | 98 | 1. Make sure the `docker-compose.yml` file is in the project root. 99 | 100 | 2. Run the Docker Compose: 101 | 102 | ```bash 103 | docker-compose up -d 104 | ``` 105 | 106 | This will run the container in the background using the configuration from the `docker-compose.yml` file. 107 | 108 | ## License 109 | 110 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | image: 'node:14.20.0' 6 | working_dir: /app 7 | volumes: 8 | - .:/app 9 | env_file: 10 | - .env 11 | command: sh -c "npm install && npm start" -------------------------------------------------------------------------------- /emailProcessingUtils/fetchThread.js: -------------------------------------------------------------------------------- 1 | // Libraries 2 | const simpleParser = require('mailparser').simpleParser; 3 | const Imap = require('imap'); 4 | 5 | // Config 6 | const imapConfig = require('../imapConfig'); 7 | 8 | // IMAP configuration 9 | const imap = new Imap(imapConfig); 10 | 11 | // Function to open the inbox 12 | function openInbox(cb) { 13 | imap.openBox('INBOX', true, cb); 14 | } 15 | 16 | // Function to fetch the email thread using the threadId 17 | async function fetchThread(threadId) { 18 | return new Promise((resolve, reject) => { 19 | let threadArray = []; 20 | 21 | // When the IMAP connection is ready, open the inbox 22 | imap.once('ready', function () { 23 | openInbox(async function (err, box) { 24 | if (err) reject(err); 25 | 26 | // Search for messages in the thread using the threadId 27 | imap.search([['X-GM-THRID', `${threadId}`]], async function (err, results) { 28 | if (err) reject(err); 29 | 30 | // Fetch the messages in the thread 31 | const f = imap.fetch(results, { bodies: '' }); 32 | let messageCount = results.length; 33 | let processedMessages = 0; 34 | 35 | // Process each message in the thread 36 | f.on('message', function (msg, seqno) { 37 | msg.on('body', async function (stream, info) { 38 | try { 39 | // Parse the message using the simpleParser library 40 | const parsed = await simpleParser(stream); 41 | 42 | // Create a message object with the sender's email address and content 43 | const msg = { 44 | from: parsed.from.value[0].address, 45 | content: parsed.text 46 | } 47 | // Add the message object to the thread array 48 | threadArray.push(msg); 49 | } catch (err) { 50 | reject(err); 51 | } finally { 52 | processedMessages++; 53 | // Once all messages are processed, close the IMAP connection and resolve the thread array 54 | if (processedMessages === messageCount) { 55 | imap.end(); 56 | resolve(threadArray); 57 | } 58 | } 59 | }); 60 | }); 61 | 62 | // Handle fetch errors 63 | f.once('error', function (err) { 64 | reject('Fetch error: ' + err); 65 | }); 66 | }); 67 | }); 68 | }); 69 | 70 | // Handle IMAP connection errors 71 | imap.once('error', function (err) { 72 | reject(err); 73 | }); 74 | 75 | // Connect to the IMAP server 76 | imap.connect(); 77 | }); 78 | } 79 | 80 | // Export the fetchThread function 81 | module.exports = fetchThread; 82 | -------------------------------------------------------------------------------- /emailProcessingUtils/getThreadId.js: -------------------------------------------------------------------------------- 1 | // Libraries 2 | const Imap = require('imap'); 3 | 4 | // Config 5 | const imapConfig = require('../imapConfig'); 6 | 7 | // IMAP configuration 8 | const imap = new Imap(imapConfig); 9 | 10 | // Function to get the thread ID from a message ID 11 | async function getThreadIdFromMessageId(messageId) { 12 | return new Promise((resolve, reject) => { 13 | // When the IMAP connection is ready, open the inbox 14 | imap.once('ready', function () { 15 | imap.openBox('INBOX', true, async function (err, box) { 16 | if (err) reject(err); 17 | 18 | // Search for the message with the given messageId 19 | imap.search([['HEADER', 'MESSAGE-ID', messageId]], async function (err, results) { 20 | if (err) reject(err); 21 | 22 | // Fetch the message with the matching messageId 23 | const f = imap.fetch(results, { bodies: '' }); 24 | 25 | f.on("message", async function (msg, seqno) { 26 | // Extract the threadId from the message attributes 27 | msg.on("attributes", function (attrs) { 28 | const threadId = attrs["x-gm-thrid"]; 29 | resolve(threadId); 30 | }); 31 | }); 32 | 33 | // Handle fetch errors 34 | f.once('error', function (err) { 35 | reject('Fetch error: ' + err); 36 | }); 37 | 38 | // Close the IMAP connection when the fetch is complete 39 | f.once('end', function () { 40 | imap.end(); 41 | }); 42 | }); 43 | }); 44 | }); 45 | 46 | // Handle IMAP connection errors 47 | imap.once('error', function (err) { 48 | reject(err); 49 | }); 50 | 51 | // Connect to the IMAP server 52 | imap.connect(); 53 | }); 54 | } 55 | 56 | // Export the getThreadIdFromMessageId function 57 | module.exports = { 58 | getThreadIdFromMessageId 59 | }; 60 | -------------------------------------------------------------------------------- /emailProcessingUtils/gmailParser.js: -------------------------------------------------------------------------------- 1 | // Libraries 2 | const replyParser = require('node-email-reply-parser'); 3 | 4 | // This function takes an email address and returns the role of the email sender, 5 | // 'assistant' if it's the same as the environment variable EMAIL_USER, 'user' otherwise. 6 | function emailToRole(emailAddress) { 7 | return emailAddress === process.env.EMAIL_USER ? 'assistant' : 'user'; 8 | } 9 | 10 | // This function takes a text and extracts the email address from the first line, 11 | // assuming the email address is enclosed in angle brackets. 12 | function extractEmailFromFirstLine(text) { 13 | const firstLine = text.split('\n')[0]; 14 | const regex = /<([^>]+)>/; 15 | const match = firstLine.match(regex); 16 | 17 | if (match) { 18 | return match[1]; 19 | } 20 | 21 | return null; 22 | } 23 | 24 | // This function takes a text and removes the email quoting format, 25 | // '>' character followed by a space, from the beginning of each line. 26 | function cleanReplyText(text) { 27 | const lines = text.split('\n'); 28 | lines.shift(); // Remove the first line 29 | const cleanedLines = lines.map(line => line.replace(/^>\s*/, '')); 30 | return cleanedLines.join('\n'); 31 | } 32 | 33 | // This function takes a history message object and returns an array of messages 34 | // with roles (assistant or user) based on the email addresses and content. 35 | // The resulting messages are formatted according to the GPT-3.5 Chat API. 36 | function historyMessageToMessagesWithRoles(msg) { 37 | // Extract the content from the message object and Parse the email content 38 | let emailContent = msg.content; 39 | let reply = replyParser(emailContent); 40 | 41 | // Create a message object for the visible (non-quoted) text of the email with the appropriate role 42 | let visibleResponse = { "role": emailToRole(msg.from), "content": reply.getVisibleText() }; 43 | 44 | // Initialize the result array with the visible response 45 | let result = [visibleResponse]; 46 | 47 | // Find the first quoted fragment in the parsed reply object 48 | let firstQuotedFragment = reply.getFragments().find(fragment => fragment._isQuoted); 49 | let firstQuotedContent = firstQuotedFragment ? firstQuotedFragment._content : null; 50 | 51 | // If there is quoted content, extract the email address of the sender and create a message object 52 | if (firstQuotedContent) { 53 | let mailFirstQuote = extractEmailFromFirstLine(firstQuotedContent); 54 | result.unshift({ "role": mailFirstQuote ? emailToRole(mailFirstQuote) : 'assistant', "content": cleanReplyText(firstQuotedContent) }); 55 | } 56 | 57 | // Return the resulting array of messages with roles 58 | return result; 59 | } 60 | 61 | 62 | // Export the historyMessageToMessagesWithRoles function for use in other modules 63 | module.exports = { 64 | historyMessageToMessagesWithRoles, 65 | }; 66 | -------------------------------------------------------------------------------- /imapConfig.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const config = { 4 | user: process.env.EMAIL_USER, 5 | password: process.env.EMAIL_PASS, 6 | host: 'imap.gmail.com', 7 | port: 993, 8 | tls: true, 9 | tlsOptions: { 10 | rejectUnauthorized: false, 11 | }, 12 | }; 13 | 14 | module.exports = config; 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Libraries 2 | const Imap = require('imap'); 3 | const { simpleParser } = require('mailparser'); 4 | const nodemailer = require('nodemailer'); 5 | 6 | // Config 7 | require('dotenv').config(); 8 | const imapConfig = require('./imapConfig'); 9 | 10 | // chatGPT 11 | const callAPIChatGPT = require('./openai/openai'); 12 | 13 | // Import functions for emailProcessing 14 | const fetchThread = require('./emailProcessingUtils/fetchThread'); 15 | const { getThreadIdFromMessageId } = require('./emailProcessingUtils/getThreadId'); 16 | const { historyMessageToMessagesWithRoles } = require('./emailProcessingUtils/gmailParser'); 17 | 18 | // IMAP configuration 19 | const imap = new Imap(imapConfig); 20 | 21 | // Email transporter configuration 22 | const transporter = nodemailer.createTransport({ 23 | service: 'gmail', 24 | auth: { 25 | user: process.env.EMAIL_USER, 26 | pass: process.env.EMAIL_PASS, 27 | }, 28 | }); 29 | 30 | 31 | // Function to process and respond to emails 32 | async function processEmail(email) { 33 | console.log("Processing email with subject:", email.subject); 34 | 35 | let messageContent = email.text || email.html; 36 | let messageId = email.messageId; 37 | 38 | let messages = []; 39 | 40 | // Check if the email has a messageId and fetch the thread history 41 | if (messageId) { 42 | let threadId = await getThreadIdFromMessageId(messageId); 43 | 44 | // If a threadId is found, fetch the thread history 45 | if (threadId) { 46 | let history = await fetchThread(threadId); 47 | 48 | // Process each history message and add it to the messages array 49 | for (const historyMessage of history) { 50 | const newMessages = historyMessageToMessagesWithRoles(historyMessage); 51 | messages.push(...newMessages); 52 | } 53 | } else { 54 | // If there's no threadId, create a user message with the email content 55 | messages.push({ "role": "user", "content": deleteReplyQuotation(messageContent) }); 56 | } 57 | } else { 58 | // If there's no messageId, create a user message with the email content 59 | messages.push({ "role": "user", "content": deleteReplyQuotation(messageContent) }); 60 | } 61 | 62 | 63 | // Create prompts for the GPT-3.5 Chat API 64 | const prompts = [ 65 | { "role": "system", "content": process.env.SYSTEM_PROMPT }, 66 | ...messages, 67 | ]; 68 | 69 | try { 70 | // Call the GPT-3.5 Chat API with the prompts 71 | console.log("Requesting response from GPT-3.5 Chat API"); 72 | const apiResponse = await callAPIChatGPT(prompts); 73 | 74 | // Create the response email 75 | let subj = `RE: ${email.subject}`; 76 | const mailOptions = { 77 | from: process.env.EMAIL_USER, 78 | to: email.from.value[0].address, 79 | subject: subj, 80 | text: apiResponse, 81 | }; 82 | 83 | // Send the response email 84 | console.log("Sending response email with subject:", subj); 85 | transporter.sendMail(mailOptions, (error, info) => { 86 | if (error) { 87 | console.error('Error while sending the email:', error); 88 | } else { 89 | console.log('Response email sent successfully:', info.response); 90 | } 91 | }); 92 | } catch (error) { 93 | console.error('Error encountered while processing the email:', error); 94 | } 95 | } 96 | 97 | 98 | 99 | // Function to fetch unread emails 100 | function fetchUnreadEmails() { 101 | imap.search(['UNSEEN'], (error, results) => { 102 | if (error) { 103 | console.error('Error searching for emails:', error); 104 | return; 105 | } 106 | 107 | // Check if there are no unread emails and start idle mode if true 108 | if (results.length === 0) { 109 | console.log('No unread emails found.'); 110 | startIdle(); 111 | return; 112 | } 113 | 114 | // Fetch unread emails, mark them as seen and retrieve their bodies 115 | const fetch = imap.fetch(results, { 116 | bodies: '', 117 | markSeen: true, 118 | }); 119 | 120 | // Listen for the 'message' event when fetching emails 121 | fetch.on('message', (msg, seqno) => { 122 | let emailData = ''; 123 | 124 | // Listen for the 'body' event to retrieve the email body 125 | msg.on('body', (stream, info) => { 126 | // Concatenate email data chunks as they are received 127 | stream.on('data', (chunk) => { 128 | emailData += chunk; 129 | }); 130 | 131 | // When the email body stream ends, parse the email data 132 | stream.on('end', () => { 133 | simpleParser(emailData) 134 | .then((email) => { 135 | processEmail(email); 136 | }) 137 | .catch((err) => { 138 | // Handle any errors that occurred during email parsing 139 | console.error("Error parsing email:", err); 140 | }); 141 | }); 142 | }); 143 | }); 144 | 145 | // Listen for the 'error' event during email fetching 146 | fetch.on('error', (error) => { 147 | console.error('Error fetching emails:', error); 148 | }); 149 | 150 | // Listen for the 'end' event after all emails have been fetched 151 | fetch.on('end', () => { 152 | console.log('All unread emails have been processed.'); 153 | // Start the IMAP idle mode to wait for new emails 154 | startIdle(); 155 | }); 156 | }); 157 | } 158 | 159 | // Function to start idle mode 160 | function startIdle() { 161 | // Add a delay of two seconds before entering IDLE mode 162 | // This delay ensures that any ongoing email processing is completed 163 | // before the IMAP client starts waiting for new emails 164 | setTimeout(() => { 165 | imap.once('idle', () => { 166 | console.log('Listening for new emails...'); 167 | imap.done(); 168 | }); 169 | }, 2000); 170 | } 171 | 172 | let firstMailEvent = true; 173 | 174 | // IMAP new mail event 175 | imap.on('mail', (numNewMail) => { 176 | if (firstMailEvent) { 177 | firstMailEvent = false; 178 | return; 179 | } 180 | 181 | console.log(`New email${numNewMail > 1 ? 's' : ''} received.`); 182 | fetchUnreadEmails(); 183 | }); 184 | 185 | // IMAP ready event 186 | imap.once('ready', () => { 187 | imap.openBox('INBOX', false, (error, box) => { 188 | if (error) { 189 | console.error("Error opening the inbox:", error); 190 | return; 191 | } 192 | 193 | fetchUnreadEmails(); 194 | }); 195 | }); 196 | 197 | // IMAP error event 198 | imap.once('error', (error) => { 199 | console.error('IMAP error:', error); 200 | }); 201 | 202 | // IMAP end event 203 | imap.once('end', () => { 204 | console.log('IMAP connection closed.'); 205 | }); 206 | 207 | // Connect to IMAP 208 | imap.connect(); -------------------------------------------------------------------------------- /openai/openai.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | require('dotenv').config(); 3 | 4 | // OpenAI API key 5 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY; 6 | 7 | async function callAPIChatGPT(prompts, retries = 3) { 8 | const headers = { 9 | 'Content-Type': 'application/json', 10 | 'Authorization': `Bearer ${OPENAI_API_KEY}` 11 | }; 12 | 13 | const data = { 14 | "model": "gpt-3.5-turbo", 15 | "messages": prompts, 16 | "temperature": 0.7, 17 | "n": 1, 18 | "max_tokens": 600, 19 | //"stop": "\n", // Lengthen the response time 20 | "presence_penalty": 0, 21 | "frequency_penalty": 0 22 | }; 23 | 24 | try { 25 | const response = await axios.post('https://api.openai.com/v1/chat/completions', data, { headers }); 26 | return response.data.choices[0].message.content; 27 | } catch (error) { 28 | if (error.response.status === 429 && retries > 0) { 29 | console.log('Too many requests. Retrying in 30 seconds...'); 30 | await new Promise(resolve => setTimeout(resolve, 30000)); 31 | return callAPIChatGPT(prompts, retries - 1); 32 | } else { 33 | let status = error.response.status ? error.response.status : "inconnu"; 34 | let message = error.response.message ? error.response.message : error.message; 35 | console.log("Error calling chatGPT API ("+status+") :"+message); 36 | throw error; 37 | } 38 | } 39 | } 40 | 41 | 42 | module.exports = callAPIChatGPT; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gmail-chatgpt-assistant", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@selderee/plugin-htmlparser2": { 8 | "version": "0.10.0", 9 | "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.10.0.tgz", 10 | "integrity": "sha512-gW69MEamZ4wk1OsOq1nG1jcyhXIQcnrsX5JwixVw/9xaiav8TCyjESAruu1Rz9yyInhgBXxkNwMeygKnN2uxNA==", 11 | "requires": { 12 | "domhandler": "^5.0.3", 13 | "selderee": "^0.10.0" 14 | } 15 | }, 16 | "asynckit": { 17 | "version": "0.4.0", 18 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 19 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 20 | }, 21 | "axios": { 22 | "version": "1.4.0", 23 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", 24 | "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", 25 | "requires": { 26 | "follow-redirects": "^1.15.0", 27 | "form-data": "^4.0.0", 28 | "proxy-from-env": "^1.1.0" 29 | } 30 | }, 31 | "combined-stream": { 32 | "version": "1.0.8", 33 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 34 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 35 | "requires": { 36 | "delayed-stream": "~1.0.0" 37 | } 38 | }, 39 | "core-util-is": { 40 | "version": "1.0.3", 41 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 42 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 43 | }, 44 | "deepmerge": { 45 | "version": "4.3.1", 46 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", 47 | "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" 48 | }, 49 | "delayed-stream": { 50 | "version": "1.0.0", 51 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 52 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" 53 | }, 54 | "dom-serializer": { 55 | "version": "2.0.0", 56 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 57 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 58 | "requires": { 59 | "domelementtype": "^2.3.0", 60 | "domhandler": "^5.0.2", 61 | "entities": "^4.2.0" 62 | } 63 | }, 64 | "domelementtype": { 65 | "version": "2.3.0", 66 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 67 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" 68 | }, 69 | "domhandler": { 70 | "version": "5.0.3", 71 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 72 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 73 | "requires": { 74 | "domelementtype": "^2.3.0" 75 | } 76 | }, 77 | "domutils": { 78 | "version": "3.1.0", 79 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 80 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 81 | "requires": { 82 | "dom-serializer": "^2.0.0", 83 | "domelementtype": "^2.3.0", 84 | "domhandler": "^5.0.3" 85 | } 86 | }, 87 | "dotenv": { 88 | "version": "16.0.3", 89 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", 90 | "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" 91 | }, 92 | "encoding-japanese": { 93 | "version": "2.0.0", 94 | "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.0.0.tgz", 95 | "integrity": "sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==" 96 | }, 97 | "entities": { 98 | "version": "4.5.0", 99 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 100 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" 101 | }, 102 | "esrever": { 103 | "version": "0.2.0", 104 | "resolved": "https://registry.npmjs.org/esrever/-/esrever-0.2.0.tgz", 105 | "integrity": "sha512-1e9YJt6yQkyekt2BUjTky7LZWWVyC2cIpgdnsTAvMcnzXIZvlW/fTMPkxBcZoYhgih4d+EC+iw+yv9GIkz7vrw==" 106 | }, 107 | "follow-redirects": { 108 | "version": "1.15.2", 109 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", 110 | "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" 111 | }, 112 | "form-data": { 113 | "version": "4.0.0", 114 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 115 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 116 | "requires": { 117 | "asynckit": "^0.4.0", 118 | "combined-stream": "^1.0.8", 119 | "mime-types": "^2.1.12" 120 | } 121 | }, 122 | "he": { 123 | "version": "1.2.0", 124 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 125 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" 126 | }, 127 | "html-to-text": { 128 | "version": "9.0.4", 129 | "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.4.tgz", 130 | "integrity": "sha512-ckrQ5N2yZS7qSgKxUbqrBZ02NxD5cSy7KuYjCNIf+HWbdzY3fbjYjQsoRIl6TiaZ4+XWOi0ggFP8/pmgCK/o+A==", 131 | "requires": { 132 | "@selderee/plugin-htmlparser2": "^0.10.0", 133 | "deepmerge": "^4.3.0", 134 | "dom-serializer": "^2.0.0", 135 | "htmlparser2": "^8.0.1", 136 | "selderee": "^0.10.0" 137 | } 138 | }, 139 | "htmlparser2": { 140 | "version": "8.0.2", 141 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", 142 | "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", 143 | "requires": { 144 | "domelementtype": "^2.3.0", 145 | "domhandler": "^5.0.3", 146 | "domutils": "^3.0.1", 147 | "entities": "^4.4.0" 148 | } 149 | }, 150 | "iconv-lite": { 151 | "version": "0.6.3", 152 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 153 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 154 | "requires": { 155 | "safer-buffer": ">= 2.1.2 < 3.0.0" 156 | } 157 | }, 158 | "imap": { 159 | "version": "0.8.19", 160 | "resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz", 161 | "integrity": "sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw==", 162 | "requires": { 163 | "readable-stream": "1.1.x", 164 | "utf7": ">=1.0.2" 165 | } 166 | }, 167 | "inherits": { 168 | "version": "2.0.4", 169 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 170 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 171 | }, 172 | "isarray": { 173 | "version": "0.0.1", 174 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 175 | "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" 176 | }, 177 | "leac": { 178 | "version": "0.6.0", 179 | "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", 180 | "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==" 181 | }, 182 | "libbase64": { 183 | "version": "1.2.1", 184 | "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.2.1.tgz", 185 | "integrity": "sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==" 186 | }, 187 | "libmime": { 188 | "version": "5.2.1", 189 | "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.2.1.tgz", 190 | "integrity": "sha512-A0z9O4+5q+ZTj7QwNe/Juy1KARNb4WaviO4mYeFC4b8dBT2EEqK2pkM+GC8MVnkOjqhl5nYQxRgnPYRRTNmuSQ==", 191 | "requires": { 192 | "encoding-japanese": "2.0.0", 193 | "iconv-lite": "0.6.3", 194 | "libbase64": "1.2.1", 195 | "libqp": "2.0.1" 196 | } 197 | }, 198 | "libqp": { 199 | "version": "2.0.1", 200 | "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.0.1.tgz", 201 | "integrity": "sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==" 202 | }, 203 | "linkify-it": { 204 | "version": "4.0.1", 205 | "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", 206 | "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", 207 | "requires": { 208 | "uc.micro": "^1.0.1" 209 | } 210 | }, 211 | "lodash": { 212 | "version": "4.17.21", 213 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 214 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 215 | }, 216 | "mailparser": { 217 | "version": "3.6.4", 218 | "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.6.4.tgz", 219 | "integrity": "sha512-4bDgbLdlcBKX8jtVskfn/G93nZo3lf7pyuLbAQ031SHQLihEqxtRwHrb9SXMTqiTkEGlOdpDrZE5uH18O+2A+A==", 220 | "requires": { 221 | "encoding-japanese": "2.0.0", 222 | "he": "1.2.0", 223 | "html-to-text": "9.0.4", 224 | "iconv-lite": "0.6.3", 225 | "libmime": "5.2.1", 226 | "linkify-it": "4.0.1", 227 | "mailsplit": "5.4.0", 228 | "nodemailer": "6.9.1", 229 | "tlds": "1.236.0" 230 | } 231 | }, 232 | "mailsplit": { 233 | "version": "5.4.0", 234 | "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.0.tgz", 235 | "integrity": "sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==", 236 | "requires": { 237 | "libbase64": "1.2.1", 238 | "libmime": "5.2.0", 239 | "libqp": "2.0.1" 240 | }, 241 | "dependencies": { 242 | "libmime": { 243 | "version": "5.2.0", 244 | "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.2.0.tgz", 245 | "integrity": "sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==", 246 | "requires": { 247 | "encoding-japanese": "2.0.0", 248 | "iconv-lite": "0.6.3", 249 | "libbase64": "1.2.1", 250 | "libqp": "2.0.1" 251 | } 252 | } 253 | } 254 | }, 255 | "mime-db": { 256 | "version": "1.52.0", 257 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 258 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 259 | }, 260 | "mime-types": { 261 | "version": "2.1.35", 262 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 263 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 264 | "requires": { 265 | "mime-db": "1.52.0" 266 | } 267 | }, 268 | "node-email-reply-parser": { 269 | "version": "0.1.4", 270 | "resolved": "https://registry.npmjs.org/node-email-reply-parser/-/node-email-reply-parser-0.1.4.tgz", 271 | "integrity": "sha512-rXiLvb9J8RRNYEf/Ftf2vx+HEjZjB16ONY5Qd9PAFdN0OESkgvGINbtlgIvdIjVeZ1jPPoK/b90sx+lP1CIvqA==", 272 | "requires": { 273 | "esrever": "^0.2.0", 274 | "lodash": "^4.17.21" 275 | } 276 | }, 277 | "nodemailer": { 278 | "version": "6.9.1", 279 | "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.1.tgz", 280 | "integrity": "sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==" 281 | }, 282 | "parseley": { 283 | "version": "0.11.0", 284 | "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.11.0.tgz", 285 | "integrity": "sha512-VfcwXlBWgTF+unPcr7yu3HSSA6QUdDaDnrHcytVfj5Z8azAyKBDrYnSIfeSxlrEayndNcLmrXzg+Vxbo6DWRXQ==", 286 | "requires": { 287 | "leac": "^0.6.0", 288 | "peberminta": "^0.8.0" 289 | } 290 | }, 291 | "peberminta": { 292 | "version": "0.8.0", 293 | "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.8.0.tgz", 294 | "integrity": "sha512-YYEs+eauIjDH5nUEGi18EohWE0nV2QbGTqmxQcqgZ/0g+laPCQmuIqq7EBLVi9uim9zMgfJv0QBZEnQ3uHw/Tw==" 295 | }, 296 | "proxy-from-env": { 297 | "version": "1.1.0", 298 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 299 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 300 | }, 301 | "readable-stream": { 302 | "version": "1.1.14", 303 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 304 | "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", 305 | "requires": { 306 | "core-util-is": "~1.0.0", 307 | "inherits": "~2.0.1", 308 | "isarray": "0.0.1", 309 | "string_decoder": "~0.10.x" 310 | } 311 | }, 312 | "safer-buffer": { 313 | "version": "2.1.2", 314 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 315 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 316 | }, 317 | "selderee": { 318 | "version": "0.10.0", 319 | "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.10.0.tgz", 320 | "integrity": "sha512-DEL/RW/f4qLw/NrVg97xKaEBC8IpzIG2fvxnzCp3Z4yk4jQ3MXom+Imav9wApjxX2dfS3eW7x0DXafJr85i39A==", 321 | "requires": { 322 | "parseley": "^0.11.0" 323 | } 324 | }, 325 | "semver": { 326 | "version": "5.3.0", 327 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", 328 | "integrity": "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==" 329 | }, 330 | "string_decoder": { 331 | "version": "0.10.31", 332 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 333 | "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" 334 | }, 335 | "tlds": { 336 | "version": "1.236.0", 337 | "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.236.0.tgz", 338 | "integrity": "sha512-oP2PZ3KeGlgpHgsEfrtva3/K9kzsJUNliQSbCfrJ7JMCWFoCdtG+9YMq/g2AnADQ1v5tVlbtvKJZ4KLpy/P6MA==" 339 | }, 340 | "uc.micro": { 341 | "version": "1.0.6", 342 | "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", 343 | "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" 344 | }, 345 | "utf7": { 346 | "version": "1.0.2", 347 | "resolved": "https://registry.npmjs.org/utf7/-/utf7-1.0.2.tgz", 348 | "integrity": "sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==", 349 | "requires": { 350 | "semver": "~5.3.0" 351 | } 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gmail-chatgpt-assistant", 3 | "version": "1.1.0", 4 | "description": "This is a simple gmail assistant application that reads incoming emails, processes them using the OpenAI GPT-3.5 API, and sends responses. It uses Node.js, IMAP for reading emails, Nodemailer for sending emails, and the OpenAI API for natural language processing.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/tgeant/gmail-chatgpt-assistant.git" 12 | }, 13 | "author": "Votre Nom", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/tgeant/gmail-chatgpt-assistant/issues" 17 | }, 18 | "homepage": "https://github.com/tgeant/gmail-chatgpt-assistant#readme", 19 | "dependencies": { 20 | "axios": "^1.4.0", 21 | "dotenv": "^16.0.3", 22 | "imap": "^0.8.19", 23 | "mailparser": "^3.6.4", 24 | "node-email-reply-parser": "^0.1.4", 25 | "nodemailer": "^6.9.1" 26 | } 27 | } 28 | --------------------------------------------------------------------------------