├── .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 | [](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*