├── db └── last-sent-email-index.txt ├── .gitignore ├── resources └── email-list.txt ├── demo └── puppeteer-gmail-gif.gif ├── package.json ├── LICENSE.MD ├── index.js ├── README.MD └── email-sender.js /db/last-sent-email-index.txt: -------------------------------------------------------------------------------- 1 | 0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .vscode -------------------------------------------------------------------------------- /resources/email-list.txt: -------------------------------------------------------------------------------- 1 | test@gmail.com 2 | test@gmail.com 3 | test@gmail.com -------------------------------------------------------------------------------- /demo/puppeteer-gmail-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpw1/puppeteer-gmail/HEAD/demo/puppeteer-gmail-gif.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-scraping", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "chalk": "^2.4.2", 8 | "dotenv": "^8.0.0", 9 | "mz": "^2.7.0", 10 | "puppeteer": "^1.15.0", 11 | "puppeteer-extra": "^2.1.3", 12 | "puppeteer-extra-plugin-stealth": "^2.2.2", 13 | "request": "^2.88.0", 14 | "request-promise": "^4.2.4", 15 | "simple-youtube-api": "^5.1.1", 16 | "yt-search": "^0.4.1" 17 | }, 18 | "devDependencies": {}, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "keywords": [], 23 | "author": "", 24 | "license": "ISC" 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 by Diego Fortes 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 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer-extra"); 2 | const emailSender = require("./email-sender"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const pluginStealth = require("puppeteer-extra-plugin-stealth"); 6 | 7 | const emailList = fs 8 | .readFileSync(path.join(__dirname, "./", `resources/email-list.txt`), "utf8") 9 | .split("\n"); 10 | 11 | /* Customizable variables */ 12 | const subject = "this is a test"; 13 | const message = 14 | "Hello there,\n\rI hope you're having a great day.\n\rKind regards,\rJohn."; 15 | 16 | (async () => { 17 | puppeteer.use(pluginStealth()); 18 | const browser = await puppeteer.launch({ 19 | headless: false, 20 | timeout: 0 21 | }); 22 | 23 | const lastEmailIndex = await emailSender.getLastSentEmailIndex(); 24 | 25 | const page = await browser.newPage(); 26 | await emailSender.login(page); 27 | 28 | for (let i = lastEmailIndex; i < emailList.length; i++) { 29 | await emailSender.writeNewEmail(page, { 30 | index: i, 31 | subject, 32 | email: emailList[i], 33 | message 34 | }); 35 | } 36 | 37 | await browser.close(); 38 | })(); 39 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Puppeteer Gmail 2 | 3 | Puppeteer Gmail is a simple tool to automate e-mail sending within the Gmail website. 4 | 5 | ![Puppeteer Gmail](/demo/puppeteer-gmail-gif.gif) 6 | 7 | ## Features 8 | 9 | - It uses [Puppeteer Stealth](https://www.npmjs.com/package/puppeteer-extra-plugin-stealth) to avoid major issues; 10 | - It does not use SMTP. Therefore you can send from 500 to 2000 e-mails a day; (depending on your account limits) 11 | - Sends about 3 e-mails/minute; (it varies from computer to computer) 12 | - It makes use of a simple .txt file to save the index of the last sent e-mail. It means that when you stop the software it will restart from the last e-mail sent; 13 | - Contains console.log messages to keep you on track of what is going on. 14 | 15 | ## Usage: 16 | 17 | ### Install dependencies 18 | 19 | Install dependencies using npm. 20 | 21 | ``` 22 | npm install 23 | ``` 24 | 25 | ### Setup your account 26 | 27 | Create a .env file in the root directory. In there, add your gmail e-mail and password: 28 | 29 | ``` 30 | EMAIL_ACCOUNT = myemail@gmail.com 31 | EMAIL_PASSWORD = pass123 32 | ``` 33 | 34 | ### E-mail list 35 | 36 | Go to to resources/email-list.txt and add the e-mails you'd like to send. For testing purposes I recommend using your own e-mail three times. 37 | 38 | ``` 39 | myemail@gmail.com 40 | myemail@gmail.com 41 | myemail@gmail.com 42 | ``` 43 | 44 | ### E-mail content 45 | 46 | Go to index.js and change the variables "subject" and "message". Use "\r" for a line break and "\n\r" for two lines break. 47 | 48 | ### Start 49 | 50 | Go to the root folder and run the project. 51 | 52 | ``` 53 | node index.js 54 | ``` 55 | 56 | Enjoy! :) 57 | 58 | ## Heads up: 59 | 60 | - You may need to change the CSS selectors for each field in the writeNewEmail function found at email-sender.js. Since my Gmail account is from Brazil some fields were in Portuguese, yours may be different. Please keep that in mind. 61 | - Whenever you start a new project make sure you reset the value at db/last-sent-email-index.txt to 0. 62 | - Misuse of this software may have legal consequences. Please follow [Gmail rules](https://support.google.com/a/answer/166852?hl=en) and use it at your own risk. 63 | 64 | ## License 65 | 66 | This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/dpw1/puppeteer-gmail/blob/master/LICENSE.MD) file for details 67 | -------------------------------------------------------------------------------- /email-sender.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const chalk = require("chalk"); 4 | const path = require("path"); 5 | const fs = require("fs").promises; 6 | 7 | const lastEmailIndex = path.join( 8 | __dirname, 9 | "./", 10 | "db/", 11 | "last-sent-email-index.txt" 12 | ); 13 | 14 | /* Customizable variables */ 15 | const user = process.env.EMAIL_ACCOUNT; 16 | const password = process.env.EMAIL_PASSWORD; 17 | const delayBetweenEmails = 1500; 18 | const delayBetweenSteps = 150; 19 | 20 | const emailSender = { 21 | login: async page => { 22 | console.log(chalk.whiteBright.inverse("Logging in Gmail...")); 23 | 24 | await page.goto( 25 | "https://accounts.google.com/AccountChooser?service=mail&continue=https://mail.google.com/mail/" 26 | ); 27 | 28 | await page.waitForSelector(`input[type='email']`); 29 | await page.type(`input[type='email']`, user, { delay: 15 }); 30 | await page.keyboard.press("Enter"); 31 | 32 | await page.waitForNavigation(["networkidle0", "load", "domcontentloaded"]); 33 | await page.waitFor(3550); 34 | await page.waitForSelector(`input[type='password']`); 35 | await page.type(`input[type='password']`, password, { delay: 15 }); 36 | await page.keyboard.press("Enter"); 37 | await page.waitForNavigation(["networkidle0", "load", "domcontentloaded"]); 38 | 39 | console.log(chalk.whiteBright.inverse("Logged in succesfully.")); 40 | await page.waitFor(5000); 41 | }, 42 | writeNewEmail: async (page, { index, subject, email, message }) => { 43 | console.log(chalk.whiteBright.inverse(`${index}. Writing new e-mail...`)); 44 | 45 | const $newEmailButton = `[jscontroller] > [id] > [class] > [id] div[style][role='button'][class]`; 46 | const $emailInput = `textarea[name = "to"]`; 47 | const $subjectInput = `input[name='subjectbox']`; 48 | const $messageInput = `[aria-label*='mensagem'][role=textbox]`; 49 | const $emailIsBeingSent = `[aria-live="assertive"] > div > div:nth-child(2) > span > span:nth-child(1)`; 50 | 51 | await page.waitForSelector($newEmailButton); 52 | await page.click($newEmailButton); 53 | 54 | await page.waitForSelector($emailInput); 55 | await page.type($emailInput, email); 56 | await page.waitFor(delayBetweenSteps); 57 | 58 | await page.waitForSelector($subjectInput); 59 | await page.type($subjectInput, subject); 60 | await page.waitFor(delayBetweenSteps); 61 | 62 | await page.waitForSelector($messageInput); 63 | await page.type($messageInput, message); 64 | await page.waitFor(delayBetweenSteps); 65 | 66 | await page.keyboard.press("Tab"); 67 | await page.waitFor(delayBetweenSteps); 68 | await page.keyboard.press("Enter"); 69 | await page.waitFor(delayBetweenSteps); 70 | await page.waitForSelector($emailIsBeingSent); 71 | 72 | try { 73 | emailSender.saveLastSentEmailIndex(index); 74 | console.log(`${chalk.whiteBright(email)} finished.`); 75 | } catch (error) { 76 | console.log(`${Number(index)}. Couldn't check if e-mail was delivered.`); 77 | } 78 | 79 | await page.waitFor(delayBetweenEmails); 80 | }, 81 | getLastSentEmailIndex: async _ => { 82 | try { 83 | const index = await fs.readFile(lastEmailIndex, "utf8"); 84 | return Number(index); 85 | } catch (err) { 86 | console.error(err); 87 | } 88 | }, 89 | saveLastSentEmailIndex: async index => { 90 | try { 91 | await fs.writeFile(lastEmailIndex, index); 92 | console.log(chalk.green("Updated index succesfully.")); 93 | } catch (err) { 94 | console.error(err); 95 | } 96 | } 97 | }; 98 | 99 | module.exports = emailSender; 100 | --------------------------------------------------------------------------------