├── .editorconfig ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── __mocks__ └── got.js ├── __tests__ ├── __mocks__ │ ├── config │ │ └── custom-mail.config.js │ └── mails │ │ ├── confirm-email │ │ ├── confirm-email.html.hbs │ │ ├── confirm-email.text.hbs │ │ └── confirm-email.watch-html.hbs │ │ ├── payment-received │ │ └── payment-received.html.hbs │ │ └── reset-password │ │ ├── reset-password.html.edge │ │ ├── reset-password.text.edge │ │ └── reset-password.watch-html.edge ├── drivers │ └── smtp.spec.js ├── mail │ ├── __snapshots__ │ │ └── mail.spec.js.snap │ └── mail.spec.js ├── request │ ├── __snapshots__ │ │ └── request.spec.js.snap │ └── request.spec.js └── views │ ├── __snapshots__ │ ├── base.spec.js.snap │ ├── edge.spec.js.snap │ └── handlebars.spec.js.snap │ ├── base.spec.js │ ├── edge.spec.js │ └── handlebars.spec.js ├── appveyor.yml ├── bin └── friendlymail ├── defaultConfig.stub.js ├── index.js ├── jest.config.js ├── mail.config.js ├── package.json └── src ├── Mail ├── Drivers │ ├── Ethereal.js │ ├── Mailgun.js │ ├── Memory.js │ ├── Ses.js │ ├── Smtp.js │ ├── SparkPost.js │ └── index.js └── Manager.js ├── Request └── index.js ├── Views ├── Base.js ├── Edge.js ├── Handlebars.js └── index.js ├── helpers └── index.js └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.json] 16 | insert_final_newline = ignore 17 | 18 | [**.min.js] 19 | indent_style = ignore 20 | insert_final_newline = ignore 21 | 22 | [MakeFile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | .DS_Store 4 | npm-debug.log 5 | .idea 6 | out 7 | .nyc_output 8 | test/tmp 9 | .env 10 | .DS_STORE 11 | .vscode/ 12 | *.log 13 | build 14 | dist 15 | yarn.lock 16 | shrinkwrap.yaml 17 | package-lock.json 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | .DS_Store 4 | npm-debug.log 5 | .travis.yml 6 | .editorconfig 7 | benchmarks 8 | .idea 9 | out 10 | .nyc_output 11 | .env 12 | __tests__ 13 | __mocks__ 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - 8.0.0 5 | sudo: false 6 | install: 7 | - npm install 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.0.2 (2019-11-03) 3 | 4 | ### Features 5 | 6 | * Add useCustomMailPaths config for specifying full path to mail 7 | 8 | 9 | # 1.0.1 (2019-11-03) 10 | 11 | ### Features 12 | 13 | * Add support for edge 14 | * Add support for passing in config via constructor 15 | 16 | 17 | # 1.0.0 (2019-03-24) 18 | 19 | 20 | ### Features 21 | 22 | * initial commit 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2018 Harminder Virk, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Friendly Mail 📩 2 | 3 | [![Build Status](https://travis-ci.org/fullstack-js-online/mail.svg?branch=master)](https://travis-ci.org/fullstack-js-online/mail) 4 | 5 | #### Elegant mail sender for node js. 6 | 7 | Friendly Mail is simple, clean, and modern and easy to use email sending package for Nodejs built on top of [nodemailer](https://github.com/nodemailer/nodemailer) and uses driver implementations from [Adonis Mail](https://github.com/adonisjs/adonis-mail). 8 | 9 | Supported mail drivers: smtp, mailgun, amazon-ses, sparkpost, ethereal 10 | 11 | 14 | 15 | ### Installation 16 | 17 | You can install the package using npm or yarn 18 | 19 | ```bash 20 | npm install --save friendly-mail 21 | # Using yarn 22 | yarn add friendly-mail 23 | ``` 24 | 25 | ### Create a mail configuration file 26 | 27 | To configure what drivers you'll be using to send emails, view engines and more, you need to generate a `mail.config.js` file in your project's root. 28 | 29 | ```bash 30 | # Using npm 31 | npx friendlymail init 32 | 33 | # Using yarn 34 | yarn friendlymail init 35 | ``` 36 | 37 | ### Setting it up 38 | 39 | Here's an example of the configuration: 40 | 41 | ```js 42 | module.exports = { 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Connection 46 | |-------------------------------------------------------------------------- 47 | | 48 | | Connection to be used for sending emails. Each connection needs to 49 | | define a driver too. 50 | | 51 | */ 52 | connection: process.env.MAIL_CONNECTION || 'smtp', 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Views 57 | |-------------------------------------------------------------------------- 58 | | 59 | | This configuration defines the folder in which all emails are stored. 60 | | If it is not defined, /mails is used as default. 61 | | 62 | */ 63 | views: '/mails', 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | View engine 68 | |-------------------------------------------------------------------------- 69 | | 70 | | This is the view engine that should be used. The currently supported are: 71 | | handlebars, edge 72 | | 73 | */ 74 | viewEngine: 'handlebars', 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | SMTP 79 | |-------------------------------------------------------------------------- 80 | | 81 | | Here we define configuration for sending emails via SMTP. 82 | | 83 | */ 84 | smtp: { 85 | driver: 'smtp', 86 | pool: true, 87 | port: process.env.SMTP_PORT || 2525, 88 | host: process.env.SMTP_HOST || 'smtp.mailtrap.io', 89 | secure: false, 90 | auth: { 91 | user: process.env.MAIL_USERNAME, 92 | pass: process.env.MAIL_PASSWORD 93 | }, 94 | maxConnections: 5, 95 | maxMessages: 100, 96 | rateLimit: 10 97 | }, 98 | 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | SparkPost 102 | |-------------------------------------------------------------------------- 103 | | 104 | | Here we define configuration for spark post. Extra options can be defined 105 | | inside the `extra` object. 106 | | 107 | | https://developer.sparkpost.com/api/transmissions.html#header-options-attributes 108 | | 109 | | extras: { 110 | | campaign_id: 'sparkpost campaign id', 111 | | options: { // sparkpost options } 112 | | } 113 | | 114 | */ 115 | sparkpost: { 116 | driver: 'sparkpost', 117 | // endpoint: 'https://api.eu.sparkpost.com/api/v1', 118 | apiKey: process.env.SPARKPOST_API_KEY, 119 | extras: {} 120 | }, 121 | 122 | /* 123 | |-------------------------------------------------------------------------- 124 | | Mailgun 125 | |-------------------------------------------------------------------------- 126 | | 127 | | Here we define configuration for mailgun. Extra options can be defined 128 | | inside the `extra` object. 129 | | 130 | | https://mailgun-documentation.readthedocs.io/en/latest/api-sending.html#sending 131 | | 132 | | extras: { 133 | | 'o:tag': '', 134 | | 'o:campaign': '',, 135 | | . . . 136 | | } 137 | | 138 | */ 139 | mailgun: { 140 | driver: 'mailgun', 141 | domain: process.env.MAILGUN_DOMAIN, 142 | apiKey: process.env.MAILGUN_API_KEY, 143 | extras: {} 144 | }, 145 | 146 | /* 147 | |-------------------------------------------------------------------------- 148 | | Ethereal 149 | |-------------------------------------------------------------------------- 150 | | 151 | | Ethereal driver to quickly test emails in your browser. A disposable 152 | | account is created automatically for you. 153 | | 154 | | https://ethereal.email 155 | | 156 | */ 157 | ethereal: { 158 | driver: 'ethereal' 159 | } 160 | } 161 | ``` 162 | 163 | The `mail.config.js` file exports an object. The following configuration variables are required: 164 | 165 | - `connection`: This represents the name of the driver to use. 166 | - `views`: This is the folder in which all your emails are stored. It defaults to `/mails` 167 | - `viewsEngine`: This defines what templating engine you are using for emails. For now, only [handlebars](http://handlebarsjs.com/) and [edge](https://edge.adonisjs.com) are supported 168 | 169 | The last configuration required is a configuration object specific to the driver. Here's an example configuration for `smtp`: 170 | ```js 171 | smtp: { 172 | driver: 'smtp', 173 | pool: true, 174 | port: process.env.SMTP_PORT || 2525, 175 | host: process.env.SMTP_HOST || 'smtp.mailtrap.io', 176 | secure: false, 177 | auth: { 178 | user: process.env.MAIL_USERNAME, 179 | pass: process.env.MAIL_PASSWORD 180 | }, 181 | maxConnections: 5, 182 | maxMessages: 100, 183 | rateLimit: 10 184 | }, 185 | ``` 186 | 187 | ### Usage 188 | 189 | Here's a sample piece of code to send an email: 190 | 191 | ```js 192 | const Mail = require('friendly-mail') 193 | 194 | const nameOfEmail = 'confirm-email' 195 | 196 | const recipientName = 'John Doe' 197 | const recipientEmail = 'john.doe@friendly.mail.ru' 198 | 199 | const subject = 'Please confirm your email address.' 200 | 201 | // Send the mail using async/await 202 | await new Mail(nameOfEmail) 203 | .to(recipientEmail, recipientName) 204 | .subject(subject) 205 | .send() 206 | ``` 207 | 208 | Note: All publicly exposed methods on the `Mail` class are chainable, except the `send` and `sendRaw` which return `Promises`. 209 | 210 | ### Common use cases 211 | 212 | #### Generating emails 213 | The package ships with a command to generate help you scaffold emails. 214 | 215 | ```bash 216 | # Using npm 217 | npx friendlymail generate activate-account 218 | # Using yarn 219 | yarn friendlymail generate activate-account 220 | ``` 221 | 222 | #### Passing data to templates 223 | The `data` method can be used to set data that will be passed to the email template. 224 | 225 | ```js 226 | await new Mail(nameOfEmail) 227 | .to(recipientEmail, recipientName) 228 | .subject(subject) 229 | .data({ 230 | name: 'John Doe', 231 | url: 'https://google.com' 232 | }) 233 | .send() 234 | ``` 235 | 236 | #### Setting cc and bcc for a mail 237 | 238 | ```js 239 | await new Mail(nameOfEmail) 240 | .inReplyTo('jane@doe.com', 'Jane Doe') 241 | .to(recipientEmail, recipientName) 242 | .subject(subject) 243 | .cc('eren.stales@yahoomail.com', 'Eren Stales') 244 | .bcc('steve.dickson@gmail.com', 'Steve Dickson') 245 | .send() 246 | ``` 247 | 248 | #### Sending emails to multiple recipients 249 | 250 | The `to` method can recieve an array of address objects to send emails to multiple users. This also works for all other methods that set user addresses like `from cc bcc inReplyTo replyTo` and `sender` 251 | 252 | ```js 253 | await new Mail(nameOfEmail) 254 | .inReplyTo([{ address: 'jane@doe.com', email: 'Jane Doe' }]) 255 | .to([{ address: 'foo@bar.com', name: 'Foo' }]) 256 | .subject('Monthly Newsletter') 257 | .cc([{ address: 'eren.stales@yahoomail.com', name: 'Eren Stales' }]) 258 | .bcc([{ address: 'steve.dickson@gmail.com', name: 'Steve Dickson' }]) 259 | .send() 260 | ``` 261 | 262 | #### Sending mails with attachments 263 | The `attach` and `attachData` methods can be used to send attachments 264 | 265 | ```js 266 | // Attaching an existing file 267 | 268 | await new Mail(nameOfEmail) 269 | .to(recipientEmail, recipientName) 270 | .subject(subject) 271 | .attach('/absolute/path/to/file') 272 | .send() 273 | 274 | // Attaching buffer as attachment with a custom file name 275 | const filename = 'hello.txt' 276 | const rawData = new Buffer('hello') 277 | await new Mail(nameOfEmail) 278 | .to(recipientEmail, recipientName) 279 | .subject(subject) 280 | .attachData(rawData, filename) 281 | .send() 282 | 283 | // Attaching readstream as attachment with a custom file name 284 | const filename = 'hello.txt' 285 | const rawData = fs.createReadStream('hello.txt') 286 | 287 | await new Mail(nameOfEmail) 288 | .to(recipientEmail, recipientName) 289 | .subject(subject) 290 | .attachData(rawData, filename) 291 | .send() 292 | 293 | // Attaching string as attachment with a custom file name 294 | const filename = 'hello.txt' 295 | const rawData = 'hello' 296 | 297 | await new Mail(nameOfEmail) 298 | .to(recipientEmail, recipientName) 299 | .subject(subject) 300 | .attachData(rawData, filename) 301 | .send() 302 | ``` 303 | -------------------------------------------------------------------------------- /__mocks__/got.js: -------------------------------------------------------------------------------- 1 | module.exports = jest.fn((a, b) => 2 | b.body.fail 3 | ? Promise.reject({ response: { body: { message: 'Unauthorized.' } } }) 4 | : Promise.resolve({ body: { url: a, data: b } }) 5 | ) 6 | -------------------------------------------------------------------------------- /__tests__/__mocks__/config/custom-mail.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testing: true, 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Connection 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Connection to be used for sending emails. Each connection needs to 9 | | define a driver too. 10 | | 11 | */ 12 | connection: process.env.MAIL_CONNECTION || 'smtp', 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Views 17 | |-------------------------------------------------------------------------- 18 | | 19 | | This configuration defines the folder in which all emails are stored. 20 | | If it's not defined, /mails is used as default. 21 | | 22 | */ 23 | views: 'mails', 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | View engine 28 | |-------------------------------------------------------------------------- 29 | | 30 | | This is the view engine that should be used. The currently supported are: 31 | | handlebars, edge 32 | | 33 | */ 34 | viewEngine: 'handlebars', 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | SMTP 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Here we define configuration for sending emails via SMTP. 42 | | 43 | */ 44 | smtp: { 45 | driver: 'smtp', 46 | pool: true, 47 | port: process.env.SMTP_PORT || 2525, 48 | host: process.env.SMTP_HOST || 'smtp.mailtrap.io', 49 | secure: false, 50 | auth: { 51 | user: process.env.MAIL_USERNAME, 52 | pass: process.env.MAIL_PASSWORD 53 | }, 54 | maxConnections: 5, 55 | maxMessages: 100, 56 | rateLimit: 10 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /__tests__/__mocks__/mails/confirm-email/confirm-email.html.hbs: -------------------------------------------------------------------------------- 1 | 2 | HEY {{ username }} 3 | 4 |
5 | CONFIRM YOUR EMAIL 6 | -------------------------------------------------------------------------------- /__tests__/__mocks__/mails/confirm-email/confirm-email.text.hbs: -------------------------------------------------------------------------------- 1 | hey {{ username }}, please confirm your email. 2 | -------------------------------------------------------------------------------- /__tests__/__mocks__/mails/confirm-email/confirm-email.watch-html.hbs: -------------------------------------------------------------------------------- 1 | hey {{ username }}, please confirm your email. 2 | -------------------------------------------------------------------------------- /__tests__/__mocks__/mails/payment-received/payment-received.html.hbs: -------------------------------------------------------------------------------- 1 | Hello. Your payment of ${{ price }} has been received. 2 | -------------------------------------------------------------------------------- /__tests__/__mocks__/mails/reset-password/reset-password.html.edge: -------------------------------------------------------------------------------- 1 | 2 | HEY {{ username }} 3 | 4 |
5 | RESET YOUR PASSWORD 6 | -------------------------------------------------------------------------------- /__tests__/__mocks__/mails/reset-password/reset-password.text.edge: -------------------------------------------------------------------------------- 1 | hey {{ username }}, please RESET YOUR PASSWORD. 2 | -------------------------------------------------------------------------------- /__tests__/__mocks__/mails/reset-password/reset-password.watch-html.edge: -------------------------------------------------------------------------------- 1 | hey {{ username }}, please RESET YOUR PASSWORD. 2 | -------------------------------------------------------------------------------- /__tests__/drivers/smtp.spec.js: -------------------------------------------------------------------------------- 1 | const SmtpDriver = require('../../src/Mail/Drivers/Smtp') 2 | 3 | describe('the Smtp driver', () => { 4 | it('can set configuration', () => { 5 | const config = { 6 | host: 'smtp.mailtrap.io' 7 | } 8 | const driver = new SmtpDriver() 9 | 10 | driver.setConfig(config) 11 | expect(driver.transporter).toBeDefined() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /__tests__/mail/__snapshots__/mail.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The Mail class can build a complete mail message 1`] = ` 4 | Object { 5 | "alternatives": Array [ 6 | Object { 7 | "content": "**Hello**", 8 | "contentType": "text/x-web-markdown", 9 | }, 10 | ], 11 | "attachments": Array [ 12 | Object { 13 | "cid": "logo", 14 | "path": "logo.png", 15 | }, 16 | Object { 17 | "content": "hello text", 18 | "filename": "hello.txt", 19 | }, 20 | Object { 21 | "contentTpe": "plain/text", 22 | "path": "absolute/path/to/file.jpg", 23 | }, 24 | ], 25 | "bcc": Array [ 26 | Object { 27 | "address": "admin@app.com", 28 | "name": "Administrator", 29 | }, 30 | ], 31 | "cc": Array [ 32 | Object { 33 | "address": "john@doe.com", 34 | "name": "John doe", 35 | }, 36 | ], 37 | "extras": undefined, 38 | "from": Array [ 39 | Object { 40 | "address": "foo@bar.com", 41 | "name": "Foo Bar", 42 | }, 43 | ], 44 | "inReplyTo": "10122121112", 45 | "replyTo": Array [ 46 | Object { 47 | "address": "anne@meyner.com", 48 | "name": "Anne Meyner", 49 | }, 50 | ], 51 | "sender": Array [ 52 | Object { 53 | "address": "mark@meyner.com", 54 | "name": "Mark Meyner", 55 | }, 56 | ], 57 | "to": Array [ 58 | Object { 59 | "address": "jane@doe.com", 60 | "name": "Jane Doe", 61 | }, 62 | ], 63 | } 64 | `; 65 | -------------------------------------------------------------------------------- /__tests__/mail/mail.spec.js: -------------------------------------------------------------------------------- 1 | const Mail = require('../../src/index') 2 | 3 | describe('The Mail class', () => { 4 | it('instantiates with preferred config if its passed as second argument ', () => { 5 | const config = require('../../mail.config') 6 | config.TEST_CONFIG = 'TEST_CONFIG' 7 | 8 | const mail = new Mail('confirm-account', config) 9 | 10 | expect(mail.Config.TEST_CONFIG).toBe('TEST_CONFIG') 11 | }) 12 | 13 | it('instantiates with a custom config', () => { 14 | process.env.MAIL_CONFIG_FILE_PATH = 15 | '__tests__/__mocks__/config/custom-mail.config.js' 16 | 17 | const mail = new Mail() 18 | 19 | expect(mail.Config.testing).toBe(true) 20 | expect(mail._configFilePath).toEqual( 21 | '__tests__/__mocks__/config/custom-mail.config.js' 22 | ) 23 | }) 24 | 25 | it('sets driver instance once instantiated', () => { 26 | expect(new Mail()._driverInstance).toBeTruthy() 27 | }) 28 | 29 | it('can set `inReplyTo` for message to be sent', () => { 30 | const mail = new Mail().inReplyTo('test@mail.ru') 31 | 32 | expect(mail.mailerMessage.inReplyTo).toEqual('test@mail.ru') 33 | }) 34 | 35 | it('can set `subject` for message to be sent', () => { 36 | const mail = new Mail() 37 | 38 | mail.subject('Test Mail') 39 | expect(mail.mailerMessage.subject).toEqual('Test Mail') 40 | }) 41 | 42 | it('can set `from` for message to be sent', () => { 43 | const mail = new Mail().inReplyTo('10122121112') 44 | 45 | expect(mail.mailerMessage.inReplyTo).toEqual('10122121112') 46 | }) 47 | 48 | it('can build a complete mail message', () => { 49 | const mail = new Mail() 50 | 51 | mail 52 | .driverExtras() 53 | .data({ 54 | name: 'Foo Bar', 55 | username: 'foo-bar-js' 56 | }) 57 | .inReplyTo('10122121112') 58 | .embed('logo.png', 'logo') 59 | .from('foo@bar.com', 'Foo Bar') 60 | .to('jane@doe.com', 'Jane Doe') 61 | .cc('john@doe.com', 'John doe') 62 | .attachData('hello text', 'hello.txt') 63 | .bcc('admin@app.com', 'Administrator') 64 | .sender('mark@meyner.com', 'Mark Meyner') 65 | .replyTo('anne@meyner.com', 'Anne Meyner') 66 | .attach('absolute/path/to/file.jpg', { contentTpe: 'plain/text' }) 67 | .alternative('**Hello**', { contentType: 'text/x-web-markdown' }) 68 | 69 | expect(mail.mailerMessage).toMatchSnapshot() 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /__tests__/request/__snapshots__/request.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The Request class can be instantiated properly 1`] = ` 4 | Request { 5 | "_auth": null, 6 | "_basicAuth": null, 7 | "_headers": Object {}, 8 | "_isJson": false, 9 | } 10 | `; 11 | 12 | exports[`The Request class can reject errors if post request fails 1`] = `""`; 13 | -------------------------------------------------------------------------------- /__tests__/request/request.spec.js: -------------------------------------------------------------------------------- 1 | const Request = require('../../src/Request') 2 | 3 | describe('The Request class', () => { 4 | it('can be instantiated properly', () => { 5 | expect(new Request()).toMatchSnapshot() 6 | }) 7 | 8 | it('can build a request', () => { 9 | const request = new Request() 10 | 11 | request 12 | .acceptJson() 13 | .basicAuth({ username: 'username', password: 'password' }) 14 | .auth('token') 15 | .headers({ 16 | 'api-version': '2019-08-09' 17 | }) 18 | 19 | expect(request._isJson).toBe(true) 20 | expect(request._headers).toEqual({ 21 | 'api-version': '2019-08-09' 22 | }) 23 | expect(request._auth).toEqual('token') 24 | expect(request._basicAuth).toEqual({ 25 | username: 'username', 26 | password: 'password' 27 | }) 28 | }) 29 | 30 | it('can post a request to an endpoint with all data on object', async () => { 31 | const request = new Request() 32 | request 33 | .acceptJson() 34 | .basicAuth({ username: 'username', password: 'password' }) 35 | .auth('token') 36 | .headers({ 37 | 'api-version': '2019-08-09' 38 | }) 39 | 40 | const response = await request.post('http://fullstackjs.online', {}) 41 | 42 | expect(response.url).toEqual('http://fullstackjs.online') 43 | expect(response.data.headers).toEqual({ 44 | Authorization: 'token', 45 | 'api-version': '2019-08-09' 46 | }) 47 | }) 48 | 49 | it('can reject errors if post request fails', async () => { 50 | const request = new Request() 51 | 52 | expect( 53 | request.post('', { fail: true }) 54 | ).rejects.toThrowErrorMatchingSnapshot() 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /__tests__/views/__snapshots__/base.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The base render engine can be instantiated properly 1`] = ` 4 | BaseRenderEngine { 5 | "Config": Object { 6 | "viewEngine": "handlebars", 7 | }, 8 | "enginesExtensionsMap": Object { 9 | "edge": "edge", 10 | "handlebars": "hbs", 11 | }, 12 | } 13 | `; 14 | 15 | exports[`The base render engine can get views content based on viewEngine and views config variables 1`] = ` 16 | Object { 17 | "html": " 18 | HEY {{ username }} 19 | 20 |
21 | CONFIRM YOUR EMAIL 22 | ", 23 | "text": "hey {{ username }}, please confirm your email. 24 | ", 25 | "watchHtml": "hey {{ username }}, please confirm your email. 26 | ", 27 | } 28 | `; 29 | 30 | exports[`The base render engine can get views content based on viewEngine and views config variables 2`] = ` 31 | Object { 32 | "html": " 33 | HEY {{ username }} 34 | 35 |
36 | RESET YOUR PASSWORD 37 | ", 38 | "text": "hey {{ username }}, please RESET YOUR PASSWORD. 39 | ", 40 | "watchHtml": "hey {{ username }}, please RESET YOUR PASSWORD. 41 | ", 42 | } 43 | `; 44 | 45 | exports[`The base render engine can gracefully ignore mail templates that are not found 1`] = ` 46 | "Hello. Your payment of \${{ price }} has been received. 47 | " 48 | `; 49 | -------------------------------------------------------------------------------- /__tests__/views/__snapshots__/edge.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The EdgeRenderEngine can render the emails content for all three email types 1`] = ` 4 | Object { 5 | "html": " 6 | HEY bahdcoder 7 | 8 |
9 | RESET YOUR PASSWORD 10 | ", 11 | "text": "hey bahdcoder, please RESET YOUR PASSWORD. 12 | ", 13 | "watchHtml": "hey bahdcoder, please RESET YOUR PASSWORD. 14 | ", 15 | } 16 | `; 17 | 18 | exports[`The EdgeRenderEngine it instantiates properly 1`] = ` 19 | EdgeRenderEngine { 20 | "Config": Object { 21 | "viewEngine": "edge", 22 | }, 23 | "enginesExtensionsMap": Object { 24 | "edge": "edge", 25 | "handlebars": "hbs", 26 | }, 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /__tests__/views/__snapshots__/handlebars.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`The HandlebarsRenderEngine can render the emails content for all three email types 1`] = ` 4 | Object { 5 | "html": " 6 | HEY bahdcoder 7 | 8 |
9 | CONFIRM YOUR EMAIL 10 | ", 11 | "text": "hey bahdcoder, please confirm your email. 12 | ", 13 | "watchHtml": "hey bahdcoder, please confirm your email. 14 | ", 15 | } 16 | `; 17 | 18 | exports[`The HandlebarsRenderEngine it instantiates properly 1`] = ` 19 | HandleBarsRenderEngine { 20 | "Config": Object { 21 | "viewEngine": "handlebars", 22 | }, 23 | "enginesExtensionsMap": Object { 24 | "edge": "edge", 25 | "handlebars": "hbs", 26 | }, 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /__tests__/views/base.spec.js: -------------------------------------------------------------------------------- 1 | const BaseRenderEngine = require('../../src/Views/Base') 2 | 3 | describe('The base render engine', () => { 4 | it('can be instantiated properly', () => { 5 | const config = { 6 | viewEngine: 'handlebars' 7 | } 8 | 9 | expect(new BaseRenderEngine(config)).toMatchSnapshot() 10 | }) 11 | 12 | it('can get views path', () => { 13 | const config = { 14 | viewEngine: 'handlebars' 15 | } 16 | 17 | const renderEngine = new BaseRenderEngine(config) 18 | 19 | expect(renderEngine._getViewsPath('confirm-email')).toEqual( 20 | `${process.cwd()}/mails/confirm-email` 21 | ) 22 | }) 23 | 24 | it('can get views path using views config ', () => { 25 | const config = { 26 | viewEngine: 'handlebars', 27 | views: 'server/mails' 28 | } 29 | 30 | const renderEngine = new BaseRenderEngine(config) 31 | 32 | expect(renderEngine._getViewsPath('confirm-email')).toEqual( 33 | `${process.cwd()}/server/mails/confirm-email` 34 | ) 35 | }) 36 | 37 | it('can get views content based on viewEngine and views config variables', () => { 38 | const config = { 39 | viewEngine: 'handlebars', 40 | views: '__tests__/__mocks__/mails' 41 | } 42 | 43 | const edgeConfig = { 44 | viewEngine: 'edge', 45 | views: '__tests__/__mocks__/mails' 46 | } 47 | 48 | expect( 49 | new BaseRenderEngine(config)._getContent('confirm-email') 50 | ).toMatchSnapshot() 51 | expect( 52 | new BaseRenderEngine(edgeConfig)._getContent('reset-password') 53 | ).toMatchSnapshot() 54 | }) 55 | 56 | it('can gracefully ignore mail templates that are not found', () => { 57 | const config = { 58 | views: '__tests__/__mocks__/mails', 59 | viewEngine: 'handlebars' 60 | } 61 | 62 | const base = new BaseRenderEngine(config) 63 | const content = base._getContent('payment-received') 64 | 65 | expect(content.text).toBeNull() 66 | expect(content.watchHtml).toBeNull() 67 | expect(content.html).toMatchSnapshot() 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /__tests__/views/edge.spec.js: -------------------------------------------------------------------------------- 1 | const EdgeRenderEngine = require('../../src/Views/Edge') 2 | 3 | describe('The EdgeRenderEngine', () => { 4 | it('it instantiates properly', () => { 5 | const config = { 6 | viewEngine: 'edge' 7 | } 8 | 9 | expect(new EdgeRenderEngine(config)).toMatchSnapshot() 10 | }) 11 | 12 | it('can render the emails content for all three email types', () => { 13 | const config = { 14 | viewEngine: 'edge', 15 | views: '__tests__/__mocks__/mails' 16 | } 17 | 18 | expect( 19 | new EdgeRenderEngine(config).render('reset-password', { 20 | username: 'bahdcoder' 21 | }) 22 | ).toMatchSnapshot() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /__tests__/views/handlebars.spec.js: -------------------------------------------------------------------------------- 1 | const HandlebarsRenderEngine = require('../../src/Views/Handlebars') 2 | 3 | describe('The HandlebarsRenderEngine', () => { 4 | it('it instantiates properly', () => { 5 | const config = { 6 | viewEngine: 'handlebars' 7 | } 8 | 9 | expect(new HandlebarsRenderEngine(config)).toMatchSnapshot() 10 | }) 11 | 12 | it('can render the emails content for all three email types', () => { 13 | const config = { 14 | viewEngine: 'handlebars', 15 | views: '__tests__/__mocks__/mails' 16 | } 17 | 18 | expect( 19 | new HandlebarsRenderEngine(config).render('confirm-email', { 20 | username: 'bahdcoder' 21 | }) 22 | ).toMatchSnapshot() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: Stable 4 | - nodejs_version: 8.0.0 5 | init: git config --global core.autocrlf true 6 | install: 7 | - ps: 'Install-Product node $env:nodejs_version' 8 | - npm install 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm run test 13 | build: 'off' 14 | clone_depth: 1 15 | matrix: 16 | fast_finish: true 17 | -------------------------------------------------------------------------------- /bin/friendlymail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Fs = require('fs') 4 | const Path = require('path') 5 | const chalk = require('chalk') 6 | const slugify = require('slugify') 7 | const program = require('commander') 8 | const CreateFolder = require('mkdirp') 9 | const GE = require('@adonisjs/generic-exceptions') 10 | const { 11 | getConfig, 12 | getSupportedEngines, 13 | getEnginesExtensionsMap 14 | } = require('../src/helpers') 15 | 16 | const config = getConfig() 17 | 18 | program 19 | .command('generate') 20 | .alias('g') 21 | .arguments('') 22 | .description('Helpful command to rapidly generate email templates.') 23 | .action(name => { 24 | const cwd = process.cwd() 25 | const { views, viewEngine } = config 26 | 27 | if(!config) { 28 | return console.log(` 29 | ❌ ${chalk.red('Mail configuration not found. Run init command to create one.')} 30 | `) 31 | } 32 | 33 | if (!views || !viewEngine) { 34 | throw GE.RuntimeException.missingConfig( 35 | `Make sure to define views and viewEngine inside configuration file.` 36 | ) 37 | } 38 | 39 | if (!getSupportedEngines().includes(viewEngine)) { 40 | throw GE.RuntimeException.missingConfig( 41 | `The view engine defined in configuration file is not supported.` 42 | ) 43 | } 44 | 45 | name = slugify(name) 46 | 47 | if (Fs.existsSync(`${cwd}/${views}/${name}`)) { 48 | return console.log(` 49 | ❌ ${chalk.red('Mail directory already exists.')} 50 | `) 51 | } 52 | 53 | CreateFolder.sync(`${cwd}/${views}/${name}`) 54 | 55 | const extension = getEnginesExtensionsMap()[viewEngine] 56 | 57 | Fs.writeFileSync(`${cwd}/${views}/${name}/${name}.html.${extension}`, '') 58 | Fs.writeFileSync(`${cwd}/${views}/${name}/${name}.text.${extension}`, '') 59 | Fs.writeFileSync(`${cwd}/${views}/${name}/${name}.watchHtml.${extension}`, '') 60 | 61 | console.log(` 62 | ✅ ${chalk.green('Mail generated successfully.')} 63 | `) 64 | }) 65 | 66 | program 67 | .command('init') 68 | .alias('i') 69 | .description('Rapidly generate configuration file for friendly mail.') 70 | .action(() => { 71 | const cwd = process.cwd() 72 | 73 | if (config) { 74 | return console.log(` 75 | ❌ ${chalk.red('Mail configuration already exists.')} 76 | `) 77 | } 78 | 79 | if (! config) { 80 | Fs.writeFileSync(`${cwd}/mail.config.js`, Fs.readFileSync(Path.resolve(__dirname, '../defaultConfig.stub.js'))) 81 | 82 | console.log(` 83 | ✅ ${chalk.green('Mail config generated successfully.')} 84 | `) 85 | } 86 | }) 87 | 88 | program.parse(process.argv) 89 | -------------------------------------------------------------------------------- /defaultConfig.stub.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* 3 | |-------------------------------------------------------------------------- 4 | | Connection 5 | |-------------------------------------------------------------------------- 6 | | 7 | | Connection to be used for sending emails. Each connection needs to 8 | | define a driver too. 9 | | 10 | */ 11 | connection: process.env.MAIL_CONNECTION || 'smtp', 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Views 16 | |-------------------------------------------------------------------------- 17 | | 18 | | This configuration defines the folder in which all emails are stored. 19 | | If it's not defined, /mails is used as default. 20 | | 21 | */ 22 | views: 'mails', 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | View engine 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This is the view engine that should be used. The currently supported are: 30 | | handlebars, edge 31 | | 32 | */ 33 | viewEngine: 'handlebars', 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | SMTP 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here we define configuration for sending emails via SMTP. 41 | | 42 | */ 43 | smtp: { 44 | driver: 'smtp', 45 | pool: true, 46 | port: process.env.SMTP_PORT || 2525, 47 | host: process.env.SMTP_HOST || 'smtp.mailtrap.io', 48 | secure: false, 49 | auth: { 50 | user: process.env.MAIL_USERNAME, 51 | pass: process.env.MAIL_PASSWORD 52 | }, 53 | maxConnections: 5, 54 | maxMessages: 100, 55 | rateLimit: 10 56 | }, 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | SparkPost 61 | |-------------------------------------------------------------------------- 62 | | 63 | | Here we define configuration for spark post. Extra options can be defined 64 | | inside the "extra" object. 65 | | 66 | | https://developer.sparkpost.com/api/transmissions.html#header-options-attributes 67 | | 68 | | extras: { 69 | | campaign_id: 'sparkpost campaign id', 70 | | options: { // sparkpost options } 71 | | } 72 | | 73 | */ 74 | sparkpost: { 75 | driver: 'sparkpost', 76 | // endpoint: 'https://api.eu.sparkpost.com/api/v1', 77 | apiKey: process.env.SPARKPOST_API_KEY, 78 | extras: {} 79 | }, 80 | 81 | /* 82 | |-------------------------------------------------------------------------- 83 | | Mailgun 84 | |-------------------------------------------------------------------------- 85 | | 86 | | Here we define configuration for mailgun. Extra options can be defined 87 | | inside the "extra" object. 88 | | 89 | | https://mailgun-documentation.readthedocs.io/en/latest/api-sending.html#sending 90 | | 91 | | extras: { 92 | | 'o:tag': '', 93 | | 'o:campaign': '',, 94 | | . . . 95 | | } 96 | | 97 | */ 98 | mailgun: { 99 | driver: 'mailgun', 100 | domain: process.env.MAILGUN_DOMAIN, 101 | apiKey: process.env.MAILGUN_API_KEY, 102 | extras: {} 103 | }, 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Ethereal 108 | |-------------------------------------------------------------------------- 109 | | 110 | | Ethereal driver to quickly test emails in your browser. A disposable 111 | | account is created automatically for you. 112 | | 113 | | https://ethereal.email 114 | | 115 | */ 116 | ethereal: { 117 | driver: 'ethereal' 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/index') 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: [ 3 | '/node_modules/', 4 | '/__tests__/__mocks__/config' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /mail.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* 3 | |-------------------------------------------------------------------------- 4 | | Connection 5 | |-------------------------------------------------------------------------- 6 | | 7 | | Connection to be used for sending emails. Each connection needs to 8 | | define a driver too. 9 | | 10 | */ 11 | connection: process.env.MAIL_CONNECTION || 'smtp', 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Views 16 | |-------------------------------------------------------------------------- 17 | | 18 | | This configuration defines the folder in which all emails are stored. 19 | | If it's not defined, /mails is used as default. 20 | | 21 | */ 22 | views: '__tests__/__mocks__/mails', 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | View engine 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This is the view engine that should be used. The currently supported are: 30 | | handlebars, edge 31 | | 32 | */ 33 | viewEngine: 'handlebars', 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | SMTP 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here we define configuration for sending emails via SMTP. 41 | | 42 | */ 43 | smtp: { 44 | driver: 'smtp', 45 | pool: true, 46 | port: process.env.SMTP_PORT || 2525, 47 | host: process.env.SMTP_HOST || 'smtp.mailtrap.io', 48 | secure: false, 49 | auth: { 50 | user: process.env.MAIL_USERNAME, 51 | pass: process.env.MAIL_PASSWORD 52 | }, 53 | maxConnections: 5, 54 | maxMessages: 100, 55 | rateLimit: 10 56 | }, 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | SparkPost 61 | |-------------------------------------------------------------------------- 62 | | 63 | | Here we define configuration for spark post. Extra options can be defined 64 | | inside the `extra` object. 65 | | 66 | | https://developer.sparkpost.com/api/transmissions.html#header-options-attributes 67 | | 68 | | extras: { 69 | | campaign_id: 'sparkpost campaign id', 70 | | options: { // sparkpost options } 71 | | } 72 | | 73 | */ 74 | sparkpost: { 75 | driver: 'sparkpost', 76 | // endpoint: 'https://api.eu.sparkpost.com/api/v1', 77 | apiKey: process.env.SPARKPOST_API_KEY, 78 | extras: {} 79 | }, 80 | 81 | /* 82 | |-------------------------------------------------------------------------- 83 | | Mailgun 84 | |-------------------------------------------------------------------------- 85 | | 86 | | Here we define configuration for mailgun. Extra options can be defined 87 | | inside the `extra` object. 88 | | 89 | | https://mailgun-documentation.readthedocs.io/en/latest/api-sending.html#sending 90 | | 91 | | extras: { 92 | | 'o:tag': '', 93 | | 'o:campaign': '',, 94 | | . . . 95 | | } 96 | | 97 | */ 98 | mailgun: { 99 | driver: 'mailgun', 100 | domain: process.env.MAILGUN_DOMAIN, 101 | apiKey: process.env.MAILGUN_API_KEY, 102 | extras: {} 103 | }, 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Ethereal 108 | |-------------------------------------------------------------------------- 109 | | 110 | | Ethereal driver to quickly test emails in your browser. A disposable 111 | | account is created automatically for you. 112 | | 113 | | https://ethereal.email 114 | | 115 | */ 116 | ethereal: { 117 | driver: 'ethereal' 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friendly-mail", 3 | "version": "1.0.2", 4 | "description": "Mail provider for elegantly sending emails in node js.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "prettier": "prettier --write './**/*.{js,json}'" 9 | }, 10 | "bin": { 11 | "friendlymail": "bin/friendlymail" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/bahdcoder/friendly-mail.git" 16 | }, 17 | "keywords": [ 18 | "node.js", 19 | "mail" 20 | ], 21 | "author": "Kati Frantz", 22 | "bugs": { 23 | "url": "https://github.com/bahdcoder/friendly-mail/issues" 24 | }, 25 | "homepage": "https://github.com/bahdcoder/friendly-mail#readme", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "coveralls": "^3.0.2", 29 | "dotenv": "^6.0.0", 30 | "jest": "^24.5.0", 31 | "mailparser": "^2.3.4", 32 | "nodemailer-ses-transport": "^1.5.1", 33 | "prettier": "^1.16.4" 34 | }, 35 | "dependencies": { 36 | "@adonisjs/generic-exceptions": "^2.0.1", 37 | "chalk": "^2.4.2", 38 | "clone": "^2.1.2", 39 | "commander": "^2.19.0", 40 | "debug": "^4.0.1", 41 | "edge.js": "^1.1.4", 42 | "form-data": "^2.3.2", 43 | "get-stream": "^4.0.0", 44 | "got": "8.3.0", 45 | "handlebars": "^4.1.1", 46 | "mkdirp": "^0.5.1", 47 | "nodemailer": "^4.6.8", 48 | "slugify": "^1.3.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Mail/Drivers/Ethereal.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | * adonis-mail 5 | * 6 | * (c) Harminder Virk 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const nodemailer = require('nodemailer') 13 | 14 | /** 15 | * Ethereal driver is used to run test emails 16 | * 17 | * @class EtherealDriver 18 | * @constructor 19 | */ 20 | class EtherealDriver { 21 | /** 22 | * This method is called by mail manager automatically 23 | * and passes the config object 24 | * 25 | * @method setConfig 26 | * 27 | * @param {Object} config 28 | */ 29 | setConfig(config) { 30 | if (config.user && config.pass) { 31 | this.setTransporter(config.user, config.pass) 32 | } else { 33 | this.transporter = null 34 | } 35 | 36 | this.log = 37 | typeof config.log === 'function' 38 | ? config.log 39 | : function(messageUrl) { 40 | console.log(messageUrl) 41 | } 42 | } 43 | 44 | /** 45 | * Initiate transporter 46 | * 47 | * @method setTransporter 48 | * 49 | * @param {String} user 50 | * @param {String} pass 51 | */ 52 | setTransporter(user, pass) { 53 | this.transporter = nodemailer.createTransport({ 54 | host: 'smtp.ethereal.email', 55 | port: 587, 56 | secure: false, 57 | auth: { user, pass } 58 | }) 59 | } 60 | 61 | /** 62 | * Creates a new transporter on fly 63 | * 64 | * @method createTransporter 65 | * 66 | * @return {String} 67 | */ 68 | createTransporter() { 69 | return new Promise((resolve, reject) => { 70 | nodemailer.createTestAccount((error, account) => { 71 | if (error) { 72 | reject(error) 73 | return 74 | } 75 | this.setTransporter(account.user, account.pass) 76 | resolve() 77 | }) 78 | }) 79 | } 80 | 81 | /** 82 | * Sends email 83 | * 84 | * @method sendEmail 85 | * 86 | * @param {Object} message 87 | * 88 | * @return {Object} 89 | */ 90 | sendEmail(message) { 91 | return new Promise((resolve, reject) => { 92 | this.transporter.sendMail(message, (error, result) => { 93 | if (error) { 94 | reject(error) 95 | } else { 96 | resolve(result) 97 | } 98 | }) 99 | }) 100 | } 101 | 102 | /** 103 | * Send a message via message object 104 | * 105 | * @method send 106 | * @async 107 | * 108 | * @param {Object} message 109 | * 110 | * @return {Object} 111 | * 112 | * @throws {Error} If promise rejects 113 | */ 114 | async send(message) { 115 | if (!this.transporter) { 116 | await this.createTransporter() 117 | } 118 | 119 | const mail = await this.sendEmail(message) 120 | this.log(nodemailer.getTestMessageUrl(mail)) 121 | 122 | return mail 123 | } 124 | } 125 | 126 | module.exports = EtherealDriver 127 | -------------------------------------------------------------------------------- /src/Mail/Drivers/Mailgun.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | * adonis-mail 5 | * 6 | * (c) Harminder Virk 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const nodemailer = require('nodemailer') 13 | const FormData = require('form-data') 14 | const Request = require('../../Request') 15 | 16 | class MailGunTransporter { 17 | constructor(config) { 18 | this.config = config 19 | this._acceptanceMessages = ['Queued', 'Success', 'Done', 'Sent'] 20 | } 21 | 22 | /** 23 | * Transport name 24 | * 25 | * @attribute name 26 | * 27 | * @return {String} 28 | */ 29 | get name() { 30 | return 'mailgun' 31 | } 32 | 33 | /** 34 | * Transport version 35 | * 36 | * @attribute version 37 | * 38 | * @return {String} 39 | */ 40 | get version() { 41 | return '1.0.0' 42 | } 43 | 44 | /** 45 | * The mailgun endpoint 46 | * 47 | * @attribute endpoint 48 | * 49 | * @return {String} 50 | */ 51 | get endpoint() { 52 | return `https://api.mailgun.net/v3/${this.config.domain}/messages.mime` 53 | } 54 | 55 | /** 56 | * The auth header value to be sent along 57 | * as header 58 | * 59 | * @attribute authHeader 60 | * 61 | * @return {String} 62 | */ 63 | get authHeader() { 64 | return `api:${this.config.apiKey}` 65 | } 66 | 67 | /** 68 | * Formats a single recipient details into mailgun formatted 69 | * string 70 | * 71 | * @method _getRecipient 72 | * 73 | * @param {Object|String} recipient 74 | * 75 | * @return {String} 76 | * 77 | * @private 78 | */ 79 | _getRecipient(recipient) { 80 | const { address, name } = 81 | typeof recipient === 'string' ? { address: recipient } : recipient 82 | return name ? `${name} <${address}>` : address 83 | } 84 | 85 | /** 86 | * Returns list of comma seperated receipents 87 | * 88 | * @method _getRecipients 89 | * 90 | * @param {Object} mail 91 | * 92 | * @return {String} 93 | * 94 | * @private 95 | */ 96 | _getRecipients(mail) { 97 | let recipients = [] 98 | recipients = recipients.concat( 99 | mail.data.to.map(this._getRecipient.bind(this)) 100 | ) 101 | recipients = recipients.concat( 102 | (mail.data.cc || []).map(this._getRecipient.bind(this)) 103 | ) 104 | recipients = recipients.concat( 105 | (mail.data.bcc || []).map(this._getRecipient.bind(this)) 106 | ) 107 | return recipients.join(',') 108 | } 109 | 110 | /** 111 | * Returns extras object by merging runtime config 112 | * with static config 113 | * 114 | * @method _getExtras 115 | * 116 | * @param {Object|Null} extras 117 | * 118 | * @return {Object} 119 | * 120 | * @private 121 | */ 122 | _getExtras(extras) { 123 | return Object.assign({}, this.config.extras, extras) 124 | } 125 | 126 | /** 127 | * Format the response message into standard output 128 | * 129 | * @method _formatSuccess 130 | * 131 | * @param {Object} response 132 | * 133 | * @return {Object} 134 | * 135 | * @private 136 | */ 137 | _formatSuccess(response) { 138 | const isAccepted = this._acceptanceMessages.find( 139 | term => response.message.indexOf(term) > -1 140 | ) 141 | return { 142 | messageId: response.id, 143 | acceptedCount: isAccepted ? 1 : 0, 144 | rejectedCount: isAccepted ? 0 : 1 145 | } 146 | } 147 | 148 | /** 149 | * Send email from transport 150 | * 151 | * @method send 152 | * 153 | * @param {Object} mail 154 | * @param {Function} callback 155 | * 156 | * @return {void} 157 | */ 158 | send(mail, callback) { 159 | const form = new FormData() 160 | form.append('to', this._getRecipients(mail)) 161 | form.append('message', mail.message.createReadStream(), { 162 | filename: 'message.txt' 163 | }) 164 | 165 | const extras = this._getExtras(mail.data.extras) 166 | Object.keys(extras).forEach(key => form.append(key, extras[key])) 167 | 168 | new Request() 169 | .basicAuth(this.authHeader) 170 | .headers(form.getHeaders()) 171 | .post(this.endpoint, form) 172 | .then(response => JSON.parse(response)) 173 | .then(response => { 174 | callback(null, this._formatSuccess(response)) 175 | }) 176 | .catch(callback) 177 | } 178 | } 179 | 180 | class MailGun { 181 | /** 182 | * This method is called by mail manager automatically 183 | * and passes the config object 184 | * 185 | * @method setConfig 186 | * 187 | * @param {Object} config 188 | */ 189 | setConfig(config) { 190 | this.transporter = nodemailer.createTransport( 191 | new MailGunTransporter(config) 192 | ) 193 | } 194 | 195 | /** 196 | * Send a message via message object 197 | * 198 | * @method send 199 | * @async 200 | * 201 | * @param {Object} message 202 | * 203 | * @return {Object} 204 | * 205 | * @throws {Error} If promise rejects 206 | */ 207 | send(message) { 208 | return new Promise((resolve, reject) => { 209 | this.transporter.sendMail(message, (error, result) => { 210 | if (error) { 211 | reject(error) 212 | } else { 213 | resolve(result) 214 | } 215 | }) 216 | }) 217 | } 218 | } 219 | 220 | module.exports = MailGun 221 | module.exports.Transport = MailGunTransporter 222 | -------------------------------------------------------------------------------- /src/Mail/Drivers/Memory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | * adonis-mail 5 | * 6 | * (c) Harminder Virk 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const nodemailer = require('nodemailer') 13 | 14 | /** 15 | * Memory driver is used to get the message back as 16 | * an object over sending it to a real user. 17 | * 18 | * @class MemoryDriver 19 | * @constructor 20 | */ 21 | class MemoryDriver { 22 | /** 23 | * This method is called by mail manager automatically 24 | * and passes the config object 25 | * 26 | * @method setConfig 27 | */ 28 | setConfig() { 29 | this.transporter = nodemailer.createTransport({ 30 | jsonTransport: true 31 | }) 32 | } 33 | 34 | /** 35 | * Send a message via message object 36 | * 37 | * @method send 38 | * @async 39 | * 40 | * @param {Object} message 41 | * 42 | * @return {Object} 43 | * 44 | * @throws {Error} If promise rejects 45 | */ 46 | send(message) { 47 | return new Promise((resolve, reject) => { 48 | this.transporter.sendMail(message, (error, result) => { 49 | if (error) { 50 | reject(error) 51 | } else { 52 | /** 53 | * Parsing and mutating the message to a JSON object 54 | */ 55 | result.message = JSON.parse(result.message) 56 | resolve(result) 57 | } 58 | }) 59 | }) 60 | } 61 | } 62 | 63 | module.exports = MemoryDriver 64 | -------------------------------------------------------------------------------- /src/Mail/Drivers/Ses.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | * adonis-mail 5 | * 6 | * (c) Harminder Virk 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const nodemailer = require('nodemailer') 13 | 14 | class SesDriver { 15 | /** 16 | * This method is called by mail manager automatically 17 | * and passes the config object 18 | * 19 | * @method setConfig 20 | * 21 | * @param {Object} config 22 | */ 23 | setConfig(config) { 24 | this.transporter = nodemailer.createTransport({ 25 | SES: new (require('aws-sdk')).SES(config) 26 | }) 27 | } 28 | 29 | /** 30 | * Send a message via message object 31 | * 32 | * @method send 33 | * @async 34 | * 35 | * @param {Object} message 36 | * 37 | * @return {Object} 38 | * 39 | * @throws {Error} If promise rejects 40 | */ 41 | send(message) { 42 | return new Promise((resolve, reject) => { 43 | this.transporter.sendMail(message, (error, result) => { 44 | if (error) { 45 | reject(error) 46 | } else { 47 | resolve(result) 48 | } 49 | }) 50 | }) 51 | } 52 | } 53 | 54 | module.exports = SesDriver 55 | -------------------------------------------------------------------------------- /src/Mail/Drivers/Smtp.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | * adonis-mail 5 | * 6 | * (c) Harminder Virk 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const nodemailer = require('nodemailer') 13 | 14 | /** 15 | * Smtp driver is used to send email via stmp protocol. 16 | * It uses nodemailer internally and allows all the 17 | * config options from node mailer directly. 18 | * 19 | * @class SmtpDriver 20 | * @constructor 21 | */ 22 | class SmtpDriver { 23 | /** 24 | * This method is called by mail manager automatically 25 | * and passes the config object 26 | * 27 | * @method setConfig 28 | * 29 | * @param {Object} config 30 | */ 31 | setConfig(config) { 32 | this.transporter = nodemailer.createTransport(config) 33 | } 34 | 35 | /** 36 | * Send a message via message object 37 | * 38 | * @method send 39 | * @async 40 | * 41 | * @param {Object} message 42 | * 43 | * @return {Object} 44 | * 45 | * @throws {Error} If promise rejects 46 | */ 47 | send(message) { 48 | return new Promise((resolve, reject) => { 49 | this.transporter.sendMail(message, (error, result) => { 50 | if (error) { 51 | reject(error) 52 | } else { 53 | resolve(result) 54 | } 55 | }) 56 | }) 57 | } 58 | } 59 | 60 | module.exports = SmtpDriver 61 | -------------------------------------------------------------------------------- /src/Mail/Drivers/SparkPost.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | * adonis-mail 5 | * 6 | * (c) Harminder Virk 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | const nodemailer = require('nodemailer') 12 | const getStream = require('get-stream') 13 | const Request = require('../../Request') 14 | 15 | /** 16 | * The core transportor node-mailer 17 | * 18 | * @class SparkPostTransporter 19 | * @constructor 20 | */ 21 | class SparkPostTransporter { 22 | constructor(config) { 23 | this.config = config 24 | } 25 | 26 | /** 27 | * The api endpoint for sparkpost 28 | * 29 | * @attribute endpoint 30 | * 31 | * @return {String} 32 | */ 33 | get endpoint() { 34 | return `${this.config.endpoint || 35 | 'https://api.sparkpost.com/api/v1'}/transmissions` 36 | } 37 | 38 | /** 39 | * Transport name 40 | * 41 | * @attribute name 42 | * 43 | * @return {String} 44 | */ 45 | get name() { 46 | return 'sparkpost' 47 | } 48 | 49 | /** 50 | * Transport version 51 | * 52 | * @attribute version 53 | * 54 | * @return {String} 55 | */ 56 | get version() { 57 | return '1.0.0' 58 | } 59 | 60 | /** 61 | * Validations to make sure to config is complete 62 | * 63 | * @method _runValidations 64 | * 65 | * @return {String} 66 | * 67 | * @private 68 | */ 69 | _runValidations() { 70 | if (!this.config.apiKey) { 71 | throw new Error('Please define the sparkpost API key to send emails') 72 | } 73 | } 74 | 75 | /** 76 | * Returns the name and email formatted as spark 77 | * recipient 78 | * 79 | * @method _getReceipent 80 | * 81 | * @param {String|Object} item 82 | * 83 | * @return {Object} 84 | * 85 | * @private 86 | */ 87 | _getRecipient(item) { 88 | return typeof item === 'string' 89 | ? { email: item } 90 | : { email: item.address, name: item.name } 91 | } 92 | 93 | /** 94 | * Returns an array of recipients formatted 95 | * as per spark post standard. 96 | * 97 | * @method _getRecipients 98 | * 99 | * @param {Object} mail 100 | * 101 | * @return {Array} 102 | * 103 | * @private 104 | */ 105 | _getRecipients(mail) { 106 | let recipients = [] 107 | 108 | /** 109 | * To addresses 110 | */ 111 | recipients = recipients.concat( 112 | mail.data.to.map(address => { 113 | return { address: this._getRecipient(address) } 114 | }) 115 | ) 116 | 117 | /** 118 | * Cc addresses 119 | */ 120 | recipients = recipients.concat( 121 | (mail.data.cc || []).map(address => { 122 | return { address: this._getRecipient(address) } 123 | }) 124 | ) 125 | 126 | /** 127 | * Bcc addresses 128 | */ 129 | recipients = recipients.concat( 130 | (mail.data.bcc || []).map(address => { 131 | return { address: this._getRecipient(address) } 132 | }) 133 | ) 134 | 135 | return recipients 136 | } 137 | 138 | /** 139 | * Format success message 140 | * 141 | * @method _formatSuccess 142 | * 143 | * @param {Object} response 144 | * 145 | * @return {String} 146 | * 147 | * @private 148 | */ 149 | _formatSuccess(response) { 150 | if (!response.results) { 151 | return response 152 | } 153 | 154 | return { 155 | messageId: response.results.id, 156 | acceptedCount: response.results.total_accepted_recipients, 157 | rejectedCount: response.results.total_rejected_recipients 158 | } 159 | } 160 | 161 | /** 162 | * Returns options to be sent with email 163 | * 164 | * @method _getOptions 165 | * 166 | * @param {Object} extras 167 | * 168 | * @return {Object|Null} 169 | * 170 | * @private 171 | */ 172 | _getOptions(extras) { 173 | extras = extras || this.config.extras 174 | return extras && extras.options ? extras.options : null 175 | } 176 | 177 | /** 178 | * Returns the campaign id for the email 179 | * 180 | * @method _getCampaignId 181 | * 182 | * @param {Object} extras 183 | * 184 | * @return {String|null} 185 | * 186 | * @private 187 | */ 188 | _getCampaignId(extras) { 189 | extras = extras || this.config.extras 190 | return extras && extras.campaign_id ? extras.campaign_id : null 191 | } 192 | 193 | /** 194 | * Sending email from transport 195 | * 196 | * @method send 197 | * 198 | * @param {Object} mail 199 | * @param {Function} callback 200 | * 201 | * @return {void} 202 | */ 203 | send(mail, callback) { 204 | this._runValidations() 205 | const recipients = this._getRecipients(mail) 206 | const options = this._getOptions(mail.data.extras) 207 | const campaignId = this._getCampaignId(mail.data.extras) 208 | 209 | /** 210 | * Post body 211 | * 212 | * @type {Object} 213 | */ 214 | const body = { recipients } 215 | 216 | /** 217 | * If email has options sent them along 218 | */ 219 | if (options) { 220 | body.options = options 221 | } 222 | 223 | /** 224 | * If email has campaign id sent it along 225 | */ 226 | if (campaignId) { 227 | body.campaign_id = campaignId 228 | } 229 | 230 | getStream(mail.message.createReadStream()) 231 | .then(content => { 232 | body.content = { email_rfc822: content } 233 | return new Request() 234 | .auth(this.config.apiKey) 235 | .acceptJson() 236 | .post(this.endpoint, body) 237 | }) 238 | .then(response => { 239 | callback(null, this._formatSuccess(response)) 240 | }) 241 | .catch(callback) 242 | } 243 | } 244 | 245 | /** 246 | * Spark post driver for adonis mail 247 | * 248 | * @class SparkPost 249 | * @constructor 250 | */ 251 | class SparkPost { 252 | /** 253 | * This method is called by mail manager automatically 254 | * and passes the config object 255 | * 256 | * @method setConfig 257 | * 258 | * @param {Object} config 259 | */ 260 | setConfig(config) { 261 | this.transporter = nodemailer.createTransport( 262 | new SparkPostTransporter(config) 263 | ) 264 | } 265 | 266 | /** 267 | * Send a message via message object 268 | * 269 | * @method send 270 | * @async 271 | * 272 | * @param {Object} message 273 | * 274 | * @return {Object} 275 | * 276 | * @throws {Error} If promise rejects 277 | */ 278 | send(message) { 279 | return new Promise((resolve, reject) => { 280 | this.transporter.sendMail(message, (error, result) => { 281 | if (error) { 282 | reject(error) 283 | } else { 284 | resolve(result) 285 | } 286 | }) 287 | }) 288 | } 289 | } 290 | 291 | module.exports = SparkPost 292 | module.exports.Transport = SparkPostTransporter 293 | -------------------------------------------------------------------------------- /src/Mail/Drivers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | * adonis-mail 5 | * 6 | * (c) Harminder Virk 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | module.exports = { 13 | smtp: require('./Smtp'), 14 | sparkpost: require('./SparkPost'), 15 | mailgun: require('./Mailgun'), 16 | ses: require('./Ses'), 17 | memory: require('./Memory'), 18 | ethereal: require('./Ethereal') 19 | } 20 | -------------------------------------------------------------------------------- /src/Mail/Manager.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | * adonis-mail 5 | * 6 | * (c) Harminder Virk 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const GE = require('@adonisjs/generic-exceptions') 13 | const Drivers = require('./Drivers') 14 | 15 | /** 16 | * Mail manager manages the drivers and also 17 | * exposes the api to add new drivers. 18 | * 19 | * @class MailManager 20 | * @constructor 21 | */ 22 | class MailManager { 23 | constructor() { 24 | this._drivers = {} 25 | } 26 | 27 | /** 28 | * Exposing api to be extend, IoC container will 29 | * use this method when someone tries to 30 | * extend mail provider 31 | * 32 | * @method extend 33 | * 34 | * @param {String} name 35 | * @param {Object} implementation 36 | * 37 | * @return {void} 38 | */ 39 | extend(name, implementation) { 40 | this._drivers[name] = implementation 41 | } 42 | 43 | /** 44 | * Returns an instance of sender with the defined 45 | * driver. 46 | * 47 | * @method driver 48 | * 49 | * @param {String} name 50 | * @param {Object} config 51 | * @param {Object} viewInstance 52 | * 53 | * @return {Object} 54 | */ 55 | driver(name, config) { 56 | if (!name) { 57 | throw GE.InvalidArgumentException.invalidParameter( 58 | 'Cannot get driver instance without a name' 59 | ) 60 | } 61 | 62 | name = name.toLowerCase() 63 | const Driver = Drivers[name] || this._drivers[name] 64 | 65 | if (!Driver) { 66 | throw GE.InvalidArgumentException.invalidParameter( 67 | `${name} is not a valid mail driver` 68 | ) 69 | } 70 | 71 | const driverInstance = new Driver() 72 | driverInstance.setConfig(config) 73 | 74 | return driverInstance 75 | } 76 | } 77 | 78 | module.exports = new MailManager() 79 | -------------------------------------------------------------------------------- /src/Request/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | * adonis-mail 5 | * 6 | * (c) Harminder Virk 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | const got = require('got') 13 | 14 | class Request { 15 | constructor() { 16 | this._headers = {} 17 | this._basicAuth = null 18 | this._auth = null 19 | this._isJson = false 20 | } 21 | 22 | /** 23 | * Accept json 24 | * 25 | * @method isJson 26 | * 27 | * @chainable 28 | */ 29 | acceptJson() { 30 | this._isJson = true 31 | return this 32 | } 33 | 34 | /** 35 | * Set auth header 36 | * 37 | * @method auth 38 | * 39 | * @param {String} val 40 | * 41 | * @chainable 42 | */ 43 | auth(val) { 44 | this._auth = val 45 | return this 46 | } 47 | 48 | /** 49 | * Set basic auth onrequest headers 50 | * 51 | * @method basicAuth 52 | * 53 | * @param {String} val 54 | * 55 | * @chainable 56 | */ 57 | basicAuth(val) { 58 | this._basicAuth = val 59 | return this 60 | } 61 | 62 | /** 63 | * Set headers on request 64 | * 65 | * @method headers 66 | * 67 | * @param {Object} headers 68 | * 69 | * @chainable 70 | */ 71 | headers(headers) { 72 | this._headers = headers 73 | return this 74 | } 75 | 76 | /** 77 | * Make a post http request 78 | * 79 | * @method post 80 | * 81 | * @param {String} url 82 | * @param {Object} body 83 | * 84 | * @return {void} 85 | */ 86 | async post(url, body) { 87 | const headers = this._auth 88 | ? Object.assign({ Authorization: this._auth }, this._headers) 89 | : this._headers 90 | try { 91 | const response = await got(url, { 92 | headers, 93 | body, 94 | json: this._isJson, 95 | auth: this._basicAuth 96 | }) 97 | return response.body 98 | } catch ({ response, message }) { 99 | const error = new Error(message) 100 | error.errors = response.body 101 | throw error 102 | } 103 | } 104 | } 105 | 106 | module.exports = Request 107 | -------------------------------------------------------------------------------- /src/Views/Base.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @fullstackjs/mail 3 | * 4 | * (c) Kati Frantz 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | const Fs = require('fs') 11 | const Path = require('path') 12 | const GE = require('@adonisjs/generic-exceptions') 13 | const { getSupportedEngines, getEnginesExtensionsMap } = require('../helpers') 14 | 15 | /** 16 | * This class is the base for all render engines. Contains 17 | * helpful methods used in all the render engines. 18 | * 19 | * @class BaseRenderEngine 20 | * @constructor 21 | */ 22 | class BaseRenderEngine { 23 | /** 24 | * Initialize the base render engine. 25 | * 26 | * @return {Null} 27 | */ 28 | constructor(config) { 29 | this.Config = config 30 | 31 | const supportedViewEngines = getSupportedEngines() 32 | 33 | if (!supportedViewEngines.includes(this.Config.viewEngine)) { 34 | throw GE.RuntimeException.missingConfig( 35 | `The View engine to be used for sending mails is not defined.` 36 | ) 37 | } 38 | 39 | this.enginesExtensionsMap = getEnginesExtensionsMap() 40 | } 41 | 42 | /** 43 | * This method gets the content of the view we want to render 44 | * 45 | * @param {String} path the name of the view 46 | * @return {any} content 47 | */ 48 | _getContent(view) { 49 | return { 50 | html: this._getFileContent(view, 'html'), 51 | text: this._getFileContent(view, 'text'), 52 | watchHtml: this._getFileContent(view, 'watch-html') 53 | } 54 | } 55 | 56 | /** 57 | * This method gracefully tries to get the content of the template file. 58 | * It returns null if file is not found. 59 | * 60 | * @param {String} view 61 | * @param {String} type 62 | * 63 | * @private 64 | * 65 | * @return {String|Null} 66 | * 67 | */ 68 | _getFileContent(view, type) { 69 | const engine = this.Config.viewEngine 70 | 71 | try { 72 | return Fs.readFileSync( 73 | this._getViewsPath( 74 | `${view}/${view}.${type}.${this.enginesExtensionsMap[engine]}` 75 | ), 76 | 'utf8' 77 | ) 78 | } catch (e) { 79 | return null 80 | } 81 | } 82 | 83 | /** 84 | * This method resolves the path to the where all mails are stored. 85 | * It uses the default which is a folder called mails. 86 | * 87 | * @param {String} view 88 | * 89 | * @private 90 | * 91 | * @return {String} 92 | * 93 | */ 94 | _getViewsPath(view) { 95 | if (this.Config.useCustomMailPaths) return `${this.Config.views}/${view}` 96 | 97 | const currentWorkingDirectory = process.cwd() 98 | 99 | return Path.resolve( 100 | currentWorkingDirectory, 101 | this.Config.views || 'mails', 102 | view 103 | ) 104 | } 105 | } 106 | 107 | module.exports = BaseRenderEngine 108 | -------------------------------------------------------------------------------- /src/Views/Edge.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @fullstackjs/mail 3 | * 4 | * (c) Kati Frantz 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | const Edge = require('edge.js') 11 | const BaseRenderEngine = require('./Base') 12 | 13 | /** 14 | * This class defines a render method which will be used to 15 | * parse the view file in which the email was drafted. 16 | * This is specifically for edge view engine. 17 | * 18 | * @class EdgeRenderEngine 19 | * @constructor 20 | */ 21 | class EdgeRenderEngine extends BaseRenderEngine { 22 | /** 23 | * Initialize the base render engine. 24 | * 25 | * @return {Null} 26 | */ 27 | constructor(config) { 28 | super(config) 29 | 30 | this.Config = config 31 | } 32 | 33 | /** 34 | * Render the content 35 | * 36 | * @param {String} path 37 | * @param {Object} data 38 | */ 39 | render(view, data = {}) { 40 | const { html, text, watchHtml } = this._getContent(view) 41 | 42 | return { 43 | html: html ? Edge.renderString(html, data) : null, 44 | text: text ? Edge.renderString(text, data) : null, 45 | watchHtml: watchHtml ? Edge.renderString(watchHtml, data) : null 46 | } 47 | } 48 | } 49 | 50 | module.exports = EdgeRenderEngine 51 | -------------------------------------------------------------------------------- /src/Views/Handlebars.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @fullstackjs/mail 3 | * 4 | * (c) Kati Frantz 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | const HandleBars = require('handlebars') 11 | const BaseRenderEngine = require('./Base') 12 | 13 | /** 14 | * This class defines a render method which will be used to 15 | * parse the view file in which the email was drafted. 16 | * This is specifically for handlebars view engine. 17 | * 18 | * @class HandleBarsRenderEngine 19 | * @constructor 20 | */ 21 | class HandleBarsRenderEngine extends BaseRenderEngine { 22 | /** 23 | * Initialize the base render engine. 24 | * 25 | * @return {Null} 26 | */ 27 | constructor(config) { 28 | super(config) 29 | 30 | this.Config = config 31 | } 32 | 33 | /** 34 | * Render the content 35 | * 36 | * @param {String} path 37 | * @param {Object} data 38 | */ 39 | render(view, data = {}) { 40 | const { html, text, watchHtml } = this._getContent(view) 41 | 42 | return { 43 | html: html ? HandleBars.compile(html)(data) : null, 44 | text: text ? HandleBars.compile(text)(data) : null, 45 | watchHtml: watchHtml ? HandleBars.compile(watchHtml)(data) : null 46 | } 47 | } 48 | } 49 | 50 | module.exports = HandleBarsRenderEngine 51 | -------------------------------------------------------------------------------- /src/Views/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @fullstackjs/mail 3 | * 4 | * (c) Kati Frantz 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | const Edge = require('./Edge') 11 | const HandleBars = require('./Handlebars') 12 | 13 | module.exports = { 14 | edge: Edge, 15 | handlebars: HandleBars, 16 | } 17 | -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | 3 | module.exports = { 4 | /** 5 | * Get extendtions map for all engines 6 | * supported. 7 | * 8 | * @return {Object} 9 | * 10 | */ 11 | getEnginesExtensionsMap: () => ({ 12 | edge: 'edge', 13 | handlebars: 'hbs' 14 | }), 15 | 16 | /** 17 | * Get all supported engines by package. 18 | * 19 | * @return {Array[String]} 20 | * 21 | */ 22 | getSupportedEngines: () => ['handlebars', 'edge'], 23 | 24 | /** 25 | * Get the configuration file for package. Checks to see 26 | * if a custom file was defined in environment. 27 | * 28 | * @return {String} 29 | * 30 | */ 31 | getConfig: () => { 32 | try { 33 | return require(`${process.cwd()}/mail.config.js`) 34 | } catch (e) { 35 | return null 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @fullstackjs/mail 3 | * 4 | * (c) Kati Frantz 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | const ViewEngines = require('./Views') 11 | const MailManager = require('./Mail/Manager') 12 | const GE = require('@adonisjs/generic-exceptions') 13 | 14 | /** 15 | * This class is used to compose and send aa mail 16 | * 17 | * @class Mail 18 | * @constructor 19 | */ 20 | class Mail { 21 | /** 22 | * Initialize the configuration file for this mail 23 | * Set the config file and driver for mail. 24 | * 25 | * The second argument is another way of providing the configuration 26 | * for sending the mail 27 | */ 28 | constructor(template = null, config = null) { 29 | /** 30 | * Sets the template to use for this mail. 31 | * 32 | * @type {String} 33 | */ 34 | this.template = template 35 | 36 | /** 37 | * Set the mail configuration on this mail object 38 | * 39 | * @type {Object} 40 | */ 41 | this.Config = config ? config : this._getConfig() 42 | 43 | /** 44 | * Set the view engine to be used for mails 45 | * 46 | * @type {Object} 47 | */ 48 | this.View = new ViewEngines[this.Config.viewEngine](this.Config) 49 | 50 | /** 51 | * The mail message details such as to, 52 | * from, address, subject, etc 53 | * 54 | * @type {Object} 55 | */ 56 | this.mailerMessage = {} 57 | 58 | this._initialize() 59 | } 60 | 61 | /** 62 | * Get the configuration file for mails. The defaults can 63 | * be changed using environment variables. 64 | * `MAIL_CUSTOM_FILE_PATH` 65 | * `MAIL_CUSTOM_FILE_NAME` 66 | * 67 | * @method _getConfig 68 | * 69 | * @private 70 | * 71 | * @return {Object} 72 | * 73 | */ 74 | _getConfig() { 75 | const currentWorkingDirectory = process.cwd() 76 | 77 | this._configFilePath = process.env.MAIL_CONFIG_FILE_PATH || 'mail.config.js' 78 | 79 | return require(`${currentWorkingDirectory}/${this._configFilePath}`) 80 | } 81 | 82 | /** 83 | * Initialize the driver for sending mail. 84 | * 85 | * @method _instantiate 86 | * 87 | * @private 88 | * 89 | * @return {Null} 90 | * 91 | */ 92 | _initialize() { 93 | this.connection() 94 | } 95 | 96 | /** 97 | * Parse and set address object/array on 98 | * the address key 99 | * 100 | * @method _setAddress 101 | * 102 | * @param {String} key 103 | * @param {String|Array} address 104 | * @param {String} [name] 105 | * 106 | * @private 107 | */ 108 | _setAddress(key, address, name) { 109 | this.mailerMessage[key] = this.mailerMessage[key] || [] 110 | 111 | /** 112 | * If address is an array of address object, then concat 113 | * it directly 114 | */ 115 | if (address instanceof Array === true) { 116 | this.mailerMessage[key] = this.mailerMessage[key].concat(address) 117 | return 118 | } 119 | 120 | const addressObj = name ? { name, address } : address 121 | this.mailerMessage[key].push(addressObj) 122 | } 123 | 124 | /** 125 | * Change the email connection at runtime 126 | * 127 | * @method connection 128 | * 129 | * @param {String} connection 130 | * 131 | * @chainable 132 | * 133 | */ 134 | connection(connection = null) { 135 | const name = connection || this.Config.connection 136 | 137 | /** 138 | * Cannot get default connection 139 | */ 140 | if (!name) { 141 | throw GE.RuntimeException.missingConfig( 142 | `Make sure to define connection inside ${this._configFilePath} file` 143 | ) 144 | } 145 | 146 | /** 147 | * Get connection config 148 | */ 149 | const connectionConfig = this.Config[name] 150 | 151 | /** 152 | * Cannot get config for the defined connection 153 | */ 154 | if (!connectionConfig) { 155 | throw GE.RuntimeException.missingConfig(name, `${this._configFilePath}`) 156 | } 157 | 158 | /** 159 | * Throw exception when config doesn't have driver property 160 | * on it 161 | */ 162 | if (!connectionConfig.driver) { 163 | throw GE.RuntimeException.missingConfig( 164 | `${name}.driver`, 165 | `${this._configFilePath}` 166 | ) 167 | } 168 | 169 | this._driverInstance = MailManager.driver( 170 | connectionConfig.driver, 171 | connectionConfig 172 | ) 173 | 174 | return this 175 | } 176 | 177 | /** 178 | * Set `from` on the email. 179 | * 180 | * @method from 181 | * 182 | * @param {String|Array} address 183 | * @param {String} [name] 184 | * 185 | * @chainable 186 | * 187 | * @example 188 | * ``` 189 | * // just email 190 | * message.from('foo@bar.com') 191 | * 192 | * // name + email 193 | * message.from('foo@bar.com', 'Foo') 194 | * 195 | * // Address object 196 | * message.from([{ address: 'foo@bar.com', name: 'Foo' }]) 197 | * ``` 198 | */ 199 | from(address, name) { 200 | this._setAddress('from', address, name) 201 | return this 202 | } 203 | 204 | /** 205 | * Set `to` on the email. 206 | * 207 | * @method to 208 | * 209 | * @param {String|Array} address 210 | * @param {String} [name] 211 | * 212 | * @chainable 213 | * 214 | * @example 215 | * ``` 216 | * // just email 217 | * message.to('foo@bar.com') 218 | * 219 | * // name + email 220 | * message.to('foo@bar.com', 'Foo') 221 | * 222 | * // Address object 223 | * message.to([{ address: 'foo@bar.com', name: 'Foo' }]) 224 | * ``` 225 | */ 226 | to(address, name) { 227 | this._setAddress('to', address, name) 228 | return this 229 | } 230 | 231 | /** 232 | * Set `cc` on the email. 233 | * 234 | * @method cc 235 | * 236 | * @param {String|Array} address 237 | * @param {String} [name] 238 | * 239 | * @chainable 240 | * 241 | * @example 242 | * ``` 243 | * // just email 244 | * message.cc('foo@bar.com') 245 | * 246 | * // name + email 247 | * message.cc('foo@bar.com', 'Foo') 248 | * 249 | * // Address object 250 | * message.cc([{ address: 'foo@bar.com', name: 'Foo' }]) 251 | * ``` 252 | */ 253 | cc(address, name) { 254 | this._setAddress('cc', address, name) 255 | return this 256 | } 257 | 258 | /** 259 | * Set `bcc` on the email. 260 | * 261 | * @method bcc 262 | * 263 | * @param {String|Array} address 264 | * @param {String} [name] 265 | * 266 | * @chainable 267 | * 268 | * @example 269 | * ``` 270 | * // just email 271 | * message.bcc('foo@bar.com') 272 | * 273 | * // name + email 274 | * message.bcc('foo@bar.com', 'Foo') 275 | * 276 | * // Address object 277 | * message.bcc([{ address: 'foo@bar.com', name: 'Foo' }]) 278 | * ``` 279 | */ 280 | bcc(address, name) { 281 | this._setAddress('bcc', address, name) 282 | return this 283 | } 284 | 285 | /** 286 | * Set `sender` on the email. 287 | * 288 | * @method sender 289 | * 290 | * @param {String|Array} address 291 | * @param {String} [name] 292 | * 293 | * @chainable 294 | * 295 | * @example 296 | * ``` 297 | * // just email 298 | * message.sender('foo@bar.com') 299 | * 300 | * // name + email 301 | * message.sender('foo@bar.com', 'Foo') 302 | * 303 | * // Address object 304 | * message.sender([{ address: 'foo@bar.com', name: 'Foo' }]) 305 | * ``` 306 | */ 307 | sender(address, name) { 308 | this._setAddress('sender', address, name) 309 | return this 310 | } 311 | 312 | /** 313 | * Set `replyTo` on the email. 314 | * 315 | * @method replyTo 316 | * 317 | * @param {String|Array} address 318 | * @param {String} [name] 319 | * 320 | * @chainable 321 | * 322 | * @example 323 | * ``` 324 | * // just email 325 | * message.replyTo('foo@bar.com') 326 | * 327 | * // name + email 328 | * message.replyTo('foo@bar.com', 'Foo') 329 | * 330 | * // Address object 331 | * message.replyTo([{ address: 'foo@bar.com', name: 'Foo' }]) 332 | * ``` 333 | */ 334 | replyTo(address, name) { 335 | this._setAddress('replyTo', address, name) 336 | return this 337 | } 338 | 339 | /** 340 | * Set in reply to message id 341 | * 342 | * @method inReplyTo 343 | * 344 | * @param {String} messageId 345 | * 346 | * @chainable 347 | * 348 | * ```js 349 | * message.inReplyTo('101002001') 350 | * ``` 351 | */ 352 | inReplyTo(messageId) { 353 | this.mailerMessage.inReplyTo = messageId 354 | return this 355 | } 356 | 357 | /** 358 | * Set subject for the emaul 359 | * 360 | * @method subject 361 | * 362 | * @param {String} subject 363 | * 364 | * @chainable 365 | */ 366 | subject(subject) { 367 | this.mailerMessage.subject = subject 368 | return this 369 | } 370 | 371 | /** 372 | * Set the data to be use to compile templates 373 | * 374 | * @method data 375 | * 376 | * @param {Object} data 377 | * 378 | * @chainable 379 | * 380 | */ 381 | data(data = {}) { 382 | this.data = data 383 | 384 | return this 385 | } 386 | 387 | /** 388 | * Set email text body 389 | * 390 | * @method text 391 | * 392 | * @param {String} text 393 | * 394 | * @chainable 395 | */ 396 | _setText(text) { 397 | this.mailerMessage.text = text 398 | 399 | return this 400 | } 401 | 402 | /** 403 | * Set email html body 404 | * 405 | * @method html 406 | * 407 | * @param {String} html 408 | * 409 | * @chainable 410 | */ 411 | _setHtml(html) { 412 | this.mailerMessage.html = html 413 | 414 | return this 415 | } 416 | 417 | /** 418 | * Set html for apple watch 419 | * 420 | * @method watchHtml 421 | * 422 | * @param {String} html 423 | * 424 | * @chainable 425 | */ 426 | _setWatchHtml(html) { 427 | this.mailerMessage.watchHtml = html 428 | 429 | return this 430 | } 431 | 432 | /** 433 | * Add a new attachment to the mail 434 | * 435 | * @method attach 436 | * 437 | * @param {String} content 438 | * @param {Object} [options] 439 | * 440 | * @chainable 441 | * 442 | * @example 443 | * ```js 444 | * message.attach('absolute/path/to/file') 445 | * message.attach('absolute/path/to/file', { contentTpe: 'plain/text' }) 446 | * ``` 447 | */ 448 | attach(filePath, options) { 449 | this.mailerMessage.attachments = this.mailerMessage.attachments || [] 450 | const attachment = Object.assign({ path: filePath }, options || {}) 451 | this.mailerMessage.attachments.push(attachment) 452 | return this 453 | } 454 | 455 | /** 456 | * Attach raw data as attachment with a custom file name 457 | * 458 | * @method attachData 459 | * 460 | * @param {String|Buffer|Stream} content 461 | * @param {String} filename 462 | * @param {Object} [options] 463 | * 464 | * @chainable 465 | * 466 | * @example 467 | * ```js 468 | * message.attachData('hello', 'hello.txt') 469 | * message.attachData(new Buffer('hello'), 'hello.txt') 470 | * message.attachData(fs.createReadStream('hello.txt'), 'hello.txt') 471 | * ``` 472 | */ 473 | attachData(content, filename, options) { 474 | if (!filename) { 475 | throw GE.InvalidArgumentException.invalidParameter( 476 | 'Define filename as 2nd argument when calling mail.attachData' 477 | ) 478 | } 479 | 480 | this.mailerMessage.attachments = this.mailerMessage.attachments || [] 481 | 482 | const attachment = Object.assign({ content, filename }, options || {}) 483 | this.mailerMessage.attachments.push(attachment) 484 | 485 | return this 486 | } 487 | 488 | /** 489 | * Set alternative content for the email. 490 | * 491 | * @method alternative 492 | * 493 | * @param {String} content 494 | * @param {Object} [options] 495 | * 496 | * @chainable 497 | * 498 | * @example 499 | * ```js 500 | * message.alternative('**Hello**', { contentType: 'text/x-web-markdown' }) 501 | * ``` 502 | */ 503 | alternative(content, options) { 504 | this.mailerMessage.alternatives = this.mailerMessage.alternatives || [] 505 | const alternative = Object.assign({ content }, options) 506 | this.mailerMessage.alternatives.push(alternative) 507 | return this 508 | } 509 | 510 | /** 511 | * Embed image to the content. This is done 512 | * via cid. 513 | * 514 | * @method embed 515 | * 516 | * @param {String} filePath 517 | * @param {String} cid - Must be unique to single email 518 | * @param {Object} [options] 519 | * 520 | * @chainable 521 | * 522 | * @example 523 | * ``` 524 | * message.embed('logo.png', 'logo') 525 | * // inside html 526 | * 527 | * ``` 528 | */ 529 | embed(filePath, cid, options) { 530 | return this.attach(filePath, Object.assign({ cid }, options)) 531 | } 532 | 533 | /** 534 | * Set extras to be sent to the current driver in 535 | * use. It is the responsibility of the driver 536 | * to parse and use the extras 537 | * 538 | * @method driverExtras 539 | * 540 | * @param {Object} extras 541 | * 542 | * @chainable 543 | */ 544 | driverExtras(extras) { 545 | this.mailerMessage.extras = extras 546 | 547 | return this 548 | } 549 | 550 | /** 551 | * Send the message using the defined driver. The 552 | * callback will receive the message builder 553 | * instance 554 | * 555 | * @method send 556 | * @async 557 | * 558 | * @param {String|Array} views 559 | * @param {Object} [data] 560 | * @param {Function} callback 561 | * 562 | * @return {Object} 563 | * 564 | * @example 565 | * ```js 566 | * await sender.send('welcome', {}, (message) => { 567 | * message.from('foo@bar.com') 568 | * }) 569 | * 570 | * await sender.send(['welcome', 'welcome.text', 'welcome.watch'], {}, (message) => { 571 | * message.from('foo@bar.com') 572 | * }) 573 | * ``` 574 | * 575 | * @throws {Error} If promise fails 576 | */ 577 | async send() { 578 | const { html, text, watchHtml } = this.View.render(this.template, this.data) 579 | 580 | if (html) { 581 | this._setHtml(html) 582 | } 583 | 584 | if (text) { 585 | this._setText(text) 586 | } 587 | 588 | if (watchHtml) { 589 | this._setWatchHtml(watchHtml) 590 | } 591 | 592 | return this._driverInstance.send(this.mailerMessage) 593 | } 594 | 595 | /** 596 | * Send email via raw text 597 | * 598 | * @method raw 599 | * @async 600 | * 601 | * @param {String} body 602 | * @param {Function} callback 603 | * 604 | * @return {Object} 605 | * 606 | * @example 607 | * ```js 608 | * await sender.raw('Your security code is 301030', (message) => { 609 | * message.from('foo@bar.com') 610 | * }) 611 | * ``` 612 | */ 613 | async sendRaw(body) { 614 | if (/^\s*