├── .gitignore ├── LICENSE ├── README.md ├── package.json └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | data.json 4 | *emails.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, freeCodeCamp 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Massification 2 | 3 | A mass emailer created with Node and Love... 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "massification", 3 | "version": "0.0.1", 4 | "description": "An emailing service built on Amazon SES and Node", 5 | "main": "lib/index.js", 6 | "repository": "https://github.com/FreeCodeCamp/massification.git", 7 | "scripts": { 8 | "start": "babel-node src", 9 | "test": "babel-node src -- -p" 10 | }, 11 | "keywords": [ 12 | "email" 13 | ], 14 | "author": "Berkeley Martinez (http://RoboTie.com)", 15 | "license": "BSD-3-Clause", 16 | "dependencies": { 17 | "dotenv": "^1.2.0", 18 | "lodash": "^3.10.1", 19 | "nodemailer": "^1.8.0", 20 | "nodemailer-ses-transport": "^1.3.0", 21 | "nodemailer-smtp-pool": "^1.1.3", 22 | "rx": "^4.0.6", 23 | "validator": "^4.2.0", 24 | "yargs": "^4.7.0" 25 | }, 26 | "devDependencies": { 27 | "babel": "^5.8.23", 28 | "babel-eslint": "^4.1.3", 29 | "eslint": "^1.7.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-exit */ 2 | import _ from 'lodash'; 3 | import { Observable, Scheduler } from 'rx'; 4 | import { isEmail } from 'validator'; 5 | import nodemailer from 'nodemailer'; 6 | import ses from 'nodemailer-ses-transport'; 7 | import dotenv from 'dotenv'; 8 | import data from '../data.json'; 9 | import { argv } from 'yargs'; 10 | 11 | dotenv.load(); 12 | 13 | let emails; 14 | if (argv._[0] === '-p') { 15 | emails = require('../test-emails.json'); 16 | } else { 17 | emails = require('../emails.json'); 18 | } 19 | 20 | const maxRate = 90; 21 | const options = { 22 | accessKeyId: process.env.access_key, 23 | secretAccessKey: process.env.priv_key, 24 | rateLimit: maxRate 25 | }; 26 | 27 | const mailOptions = { 28 | from: 'Quincy ', 29 | subject: data.subject 30 | }; 31 | 32 | const createText = _.template(data.text); 33 | const transporter = nodemailer.createTransport(ses(options)); 34 | const send$ = Observable.fromNodeCallback(transporter.sendMail, transporter); 35 | const emailLength = emails.length; 36 | let counter = 0; 37 | 38 | const startTime = Date.now(); 39 | let endTime; 40 | let lastEmail; 41 | function getPercent(val) { 42 | const percent = (val / emailLength) * 100; 43 | return Math.round(percent * 100) / 100; 44 | } 45 | 46 | const email$ = Observable.from(emails, null, null, Scheduler.default) 47 | .filter(email => isEmail(email)) 48 | .controlled(); 49 | 50 | let bufferCount = 0; 51 | email$ 52 | .flatMap(email => { 53 | const filledOptions = Object.assign( 54 | {}, 55 | mailOptions, 56 | { 57 | to: email, 58 | text: createText({ email }) 59 | } 60 | ); 61 | return send$(filledOptions) 62 | .catch(e => { 63 | console.log('encountered an error sending to %s', email, e.message); 64 | return Observable.just(false); 65 | }); 66 | }) 67 | .doOnNext(info => { 68 | bufferCount += 1; 69 | counter += 1; 70 | if (bufferCount >= maxRate) { 71 | bufferCount = 0; 72 | email$.request(maxRate); 73 | } 74 | lastEmail = info && info.envelope ? info.envelope.to : lastEmail; 75 | console.log('%d percent done', getPercent(counter)); 76 | }) 77 | .count() 78 | .doOnNext(() => (endTime = Date.now())) 79 | .subscribe( 80 | count => console.log( 81 | 'sent %d emails in %d ms', 82 | count, 83 | endTime - startTime 84 | ), 85 | err => { 86 | console.log('err on last email %s', lastEmail); 87 | throw err; 88 | }, 89 | () => { 90 | console.log('process complete'); 91 | console.log('last email sent to %s', lastEmail); 92 | process.exit(0); 93 | } 94 | ); 95 | 96 | email$.request(maxRate); 97 | --------------------------------------------------------------------------------