├── .gitignore ├── package.json ├── readme.md ├── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | /node_modules 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micromailer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "body-parser": "^1.17.2", 13 | "dotenv": "^4.0.0", 14 | "express": "^4.15.3", 15 | "nodemailer": "^4.0.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # micromailer 2 | 3 | A Node.js microservice for sending email to a whitelisted email address. 4 | 5 | Works great with [now.sh](https://zeit.co/now). 6 | 7 | ## Example 8 | 9 | Start the service and send an email: 10 | 11 | ``` 12 | PORT=3000 node index.js 13 | curl -X POST --data "fromName=Test&subject=Hi&body=Hello&replyTo=test@example.com" http://localhost:3000/send 14 | ``` 15 | 16 | ## Installation 17 | 18 | Clone somewhere then create an `.env` file: 19 | 20 | ``` 21 | FROM_EMAIL=you@example.com 22 | TO_EMAIL=you@example.com 23 | SMTP_HOST=email-smtp.us-east-1.amazonaws.com 24 | SMTP_PORT=465 25 | SMTP_USER=XXX 26 | SMTP_PASSWORD=XXX 27 | MAX_BODY_SIZE=10000 28 | ``` 29 | 30 | Deploy to now with: 31 | 32 | ``` 33 | now --dotenv 34 | ``` 35 | 36 | Or run locally: 37 | 38 | ``` 39 | node index.js 40 | ``` 41 | 42 | ## API 43 | 44 | ``` 45 | POST /send 46 | ``` 47 | 48 | The data payload can be JSON or URL-encoded (x-www-form-urlencoded). 49 | 50 | * `fromName` - name of sender 51 | * `subject` - email subject 52 | * `body` - plain-text email body 53 | * `replyTo` (optional) - reply-to email 54 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | const dotenv = require('dotenv'); 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | 6 | dotenv.config(); 7 | 8 | let fromEmail = process.env.FROM_EMAIL; 9 | if (!fromEmail) { 10 | console.log('Missing FROM_EMAIL env var'); 11 | process.exit(1); 12 | } 13 | 14 | let toEmail = process.env.TO_EMAIL; 15 | if (!toEmail) { 16 | console.log('Missing TO_EMAIL env var'); 17 | process.exit(1); 18 | } 19 | 20 | const transporter = nodemailer.createTransport({ 21 | host: process.env.SMTP_HOST, 22 | port: parseInt(process.env.SMTP_PORT, 10) || 465, 23 | secure: true, // secure:true for port 465, secure:false for port 587 24 | auth: { 25 | user: process.env.SMTP_USER, 26 | pass: process.env.SMTP_PASSWORD 27 | } 28 | }); 29 | 30 | const app = express(); 31 | app.set('port', process.env.PORT || 3000); 32 | app.use(bodyParser.json()); 33 | app.use(bodyParser.urlencoded({extended: true})); 34 | 35 | app.get('/', (req, res) => { 36 | res.send('ready'); 37 | // res.send( 38 | // '
' + 39 | // '' + 40 | // '' + 41 | // '' + 42 | // '' + 43 | // '' + 44 | // '
' 45 | // ); 46 | }); 47 | 48 | app.post('/send', (req, res) => { 49 | let fromName = req.body.fromName; 50 | if (!fromName || typeof fromName !== 'string') { 51 | res.status(400).send('Missing fromName'); 52 | return; 53 | } 54 | let subject = req.body.subject; 55 | if (!subject || typeof subject !== 'string') { 56 | res.status(400).send('Missing subject'); 57 | return; 58 | } 59 | let body = req.body.body; 60 | if (!body || typeof body !== 'string') { 61 | res.status(400).send('Missing body'); 62 | return; 63 | } 64 | let replyTo = req.body.replyTo; 65 | if (replyTo && (typeof replyTo !== 'string' || !/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(replyTo))) { 66 | res.status(400).send('Invalid replyTo'); 67 | return; 68 | } 69 | 70 | fromName = fromName.substring(0, 100).replace(/[^a-zA-Z0-9\-\.\_\ ]/, ''); 71 | subject = subject.substring(0, 200); 72 | body = body.substring(0, parseInt(process.env.MAX_BODY_SIZE, 10) || 10000); 73 | 74 | const mailOptions = { 75 | from: '"' + fromName + '" <' + fromEmail + '>', 76 | to: toEmail, 77 | subject: subject, 78 | text: body, 79 | }; 80 | if (replyTo) { 81 | mailOptions.replyTo = replyTo; 82 | } 83 | 84 | transporter.sendMail(mailOptions, (err, info) => { 85 | if (err) { 86 | console.log(err); 87 | res.status(500).send(err.message); 88 | return; 89 | } 90 | console.log('Message %s sent: %s', info.messageId, info.response); 91 | res.send('sent'); 92 | }); 93 | }); 94 | 95 | const server = app.listen(app.get('port'), () => { 96 | const port = server.address().port; 97 | console.log('Server running at http://localhost:' + port); 98 | }); 99 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.3: 6 | version "1.3.3" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 8 | dependencies: 9 | mime-types "~2.1.11" 10 | negotiator "0.6.1" 11 | 12 | array-flatten@1.1.1: 13 | version "1.1.1" 14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 15 | 16 | body-parser@^1.17.2: 17 | version "1.17.2" 18 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee" 19 | dependencies: 20 | bytes "2.4.0" 21 | content-type "~1.0.2" 22 | debug "2.6.7" 23 | depd "~1.1.0" 24 | http-errors "~1.6.1" 25 | iconv-lite "0.4.15" 26 | on-finished "~2.3.0" 27 | qs "6.4.0" 28 | raw-body "~2.2.0" 29 | type-is "~1.6.15" 30 | 31 | bytes@2.4.0: 32 | version "2.4.0" 33 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" 34 | 35 | content-disposition@0.5.2: 36 | version "0.5.2" 37 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 38 | 39 | content-type@~1.0.2: 40 | version "1.0.2" 41 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 42 | 43 | cookie-signature@1.0.6: 44 | version "1.0.6" 45 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 46 | 47 | cookie@0.3.1: 48 | version "0.3.1" 49 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 50 | 51 | debug@2.6.7: 52 | version "2.6.7" 53 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" 54 | dependencies: 55 | ms "2.0.0" 56 | 57 | depd@1.1.0, depd@~1.1.0: 58 | version "1.1.0" 59 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 60 | 61 | destroy@~1.0.4: 62 | version "1.0.4" 63 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 64 | 65 | dotenv@^4.0.0: 66 | version "4.0.0" 67 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" 68 | 69 | ee-first@1.1.1: 70 | version "1.1.1" 71 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 72 | 73 | encodeurl@~1.0.1: 74 | version "1.0.1" 75 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 76 | 77 | escape-html@~1.0.3: 78 | version "1.0.3" 79 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 80 | 81 | etag@~1.8.0: 82 | version "1.8.0" 83 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" 84 | 85 | express@^4.15.3: 86 | version "4.15.3" 87 | resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662" 88 | dependencies: 89 | accepts "~1.3.3" 90 | array-flatten "1.1.1" 91 | content-disposition "0.5.2" 92 | content-type "~1.0.2" 93 | cookie "0.3.1" 94 | cookie-signature "1.0.6" 95 | debug "2.6.7" 96 | depd "~1.1.0" 97 | encodeurl "~1.0.1" 98 | escape-html "~1.0.3" 99 | etag "~1.8.0" 100 | finalhandler "~1.0.3" 101 | fresh "0.5.0" 102 | merge-descriptors "1.0.1" 103 | methods "~1.1.2" 104 | on-finished "~2.3.0" 105 | parseurl "~1.3.1" 106 | path-to-regexp "0.1.7" 107 | proxy-addr "~1.1.4" 108 | qs "6.4.0" 109 | range-parser "~1.2.0" 110 | send "0.15.3" 111 | serve-static "1.12.3" 112 | setprototypeof "1.0.3" 113 | statuses "~1.3.1" 114 | type-is "~1.6.15" 115 | utils-merge "1.0.0" 116 | vary "~1.1.1" 117 | 118 | finalhandler@~1.0.3: 119 | version "1.0.3" 120 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" 121 | dependencies: 122 | debug "2.6.7" 123 | encodeurl "~1.0.1" 124 | escape-html "~1.0.3" 125 | on-finished "~2.3.0" 126 | parseurl "~1.3.1" 127 | statuses "~1.3.1" 128 | unpipe "~1.0.0" 129 | 130 | forwarded@~0.1.0: 131 | version "0.1.0" 132 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" 133 | 134 | fresh@0.5.0: 135 | version "0.5.0" 136 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" 137 | 138 | http-errors@~1.6.1: 139 | version "1.6.1" 140 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" 141 | dependencies: 142 | depd "1.1.0" 143 | inherits "2.0.3" 144 | setprototypeof "1.0.3" 145 | statuses ">= 1.3.1 < 2" 146 | 147 | iconv-lite@0.4.15: 148 | version "0.4.15" 149 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" 150 | 151 | inherits@2.0.3: 152 | version "2.0.3" 153 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 154 | 155 | ipaddr.js@1.3.0: 156 | version "1.3.0" 157 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" 158 | 159 | media-typer@0.3.0: 160 | version "0.3.0" 161 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 162 | 163 | merge-descriptors@1.0.1: 164 | version "1.0.1" 165 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 166 | 167 | methods@~1.1.2: 168 | version "1.1.2" 169 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 170 | 171 | mime-db@~1.27.0: 172 | version "1.27.0" 173 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" 174 | 175 | mime-types@~2.1.11, mime-types@~2.1.15: 176 | version "2.1.15" 177 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" 178 | dependencies: 179 | mime-db "~1.27.0" 180 | 181 | mime@1.3.4: 182 | version "1.3.4" 183 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 184 | 185 | ms@2.0.0: 186 | version "2.0.0" 187 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 188 | 189 | negotiator@0.6.1: 190 | version "0.6.1" 191 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 192 | 193 | nodemailer@^4.0.1: 194 | version "4.0.1" 195 | resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.0.1.tgz#b95864b07facee8287e8232effd6f1d56ec75ab2" 196 | 197 | on-finished@~2.3.0: 198 | version "2.3.0" 199 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 200 | dependencies: 201 | ee-first "1.1.1" 202 | 203 | parseurl@~1.3.1: 204 | version "1.3.1" 205 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 206 | 207 | path-to-regexp@0.1.7: 208 | version "0.1.7" 209 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 210 | 211 | proxy-addr@~1.1.4: 212 | version "1.1.4" 213 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" 214 | dependencies: 215 | forwarded "~0.1.0" 216 | ipaddr.js "1.3.0" 217 | 218 | qs@6.4.0: 219 | version "6.4.0" 220 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" 221 | 222 | range-parser@~1.2.0: 223 | version "1.2.0" 224 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 225 | 226 | raw-body@~2.2.0: 227 | version "2.2.0" 228 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" 229 | dependencies: 230 | bytes "2.4.0" 231 | iconv-lite "0.4.15" 232 | unpipe "1.0.0" 233 | 234 | send@0.15.3: 235 | version "0.15.3" 236 | resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309" 237 | dependencies: 238 | debug "2.6.7" 239 | depd "~1.1.0" 240 | destroy "~1.0.4" 241 | encodeurl "~1.0.1" 242 | escape-html "~1.0.3" 243 | etag "~1.8.0" 244 | fresh "0.5.0" 245 | http-errors "~1.6.1" 246 | mime "1.3.4" 247 | ms "2.0.0" 248 | on-finished "~2.3.0" 249 | range-parser "~1.2.0" 250 | statuses "~1.3.1" 251 | 252 | serve-static@1.12.3: 253 | version "1.12.3" 254 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" 255 | dependencies: 256 | encodeurl "~1.0.1" 257 | escape-html "~1.0.3" 258 | parseurl "~1.3.1" 259 | send "0.15.3" 260 | 261 | setprototypeof@1.0.3: 262 | version "1.0.3" 263 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 264 | 265 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 266 | version "1.3.1" 267 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 268 | 269 | type-is@~1.6.15: 270 | version "1.6.15" 271 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 272 | dependencies: 273 | media-typer "0.3.0" 274 | mime-types "~2.1.15" 275 | 276 | unpipe@1.0.0, unpipe@~1.0.0: 277 | version "1.0.0" 278 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 279 | 280 | utils-merge@1.0.0: 281 | version "1.0.0" 282 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 283 | 284 | vary@~1.1.1: 285 | version "1.1.1" 286 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" 287 | --------------------------------------------------------------------------------