├── .gitignore ├── .npmignore ├── ISSUE_TEMPLATE.md ├── LICENSE.txt ├── README.md ├── bin └── docs ├── docs ├── .nojekyll ├── README.md ├── _sidebar.md ├── api │ ├── bounces.md │ ├── complaints.md │ ├── credentials.md │ ├── domain.md │ ├── events.md │ ├── list.md │ ├── members.md │ ├── message.md │ ├── routes.md │ ├── stats.md │ ├── tags.md │ ├── tracking.md │ └── unsubscribes.md ├── attach.md ├── batch.md ├── debug.md ├── email_validation.md ├── generic.md ├── index.html ├── promises.md ├── testmode.md ├── tests.md └── webhook.md ├── lib ├── attachment.js ├── build.js ├── mailgun.js ├── request.js └── schema.js ├── package-lock.json ├── package.json └── test ├── bounces.test.js ├── complaints.test.js ├── constructor.test.js ├── data ├── fixture.json ├── mailgun_logo.png └── message.eml ├── domains.test.js ├── events.test.js ├── genericrequests.test.js ├── mailinglists.test.js ├── messages.test.js ├── routes.test.js ├── sendmime.test.js ├── stats.test.js ├── tags.test.js ├── testmode.test.js ├── unsubscribes.test.js └── validation.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.orig 2 | .idea 3 | .DS_Store 4 | node_modules 5 | dump.rdb 6 | npm-debug.log 7 | main.js 8 | /test/data/auth.json 9 | _book 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # From .gitignore 2 | /npm-debug.log 3 | *.orig 4 | .idea 5 | dump.rdb 6 | main.js 7 | _book 8 | 9 | # Additions for .npmignore only 10 | test 11 | /docs/ 12 | /bin/ 13 | /.npmignore 14 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 1) What version of the module is the issue happening on? Does the issue happen on latest version? 2 | 3 | 2) What platform and Node.js version? (For example Node.js 6.9.1 on Mac OS X) 4 | 5 | 3) Does the action work when you manually perform request against mailgun using curl (or other means)? 6 | 7 | 4) Sample source code or steps to reproduce 8 | 9 | (Write description of your issue here, stack traces from errors and code that reproduces the issue are helpful) 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 - 2017 OneLobby and Bojan D. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mailgun.js 2 | 3 | Simple Node.js module for [Mailgun](http://www.mailgun.com). 4 | 5 | # Maintenance Notice 6 | 7 | This library is no longer maintained. Mailgun now has a supported, official library at https://github.com/mailgun/mailgun-js. It is also available as an npm at https://www.npmjs.com/package/mailgun.js. I'd encourage everyone to migrate from this library to the official library. If you have questions or issues with the official library, please raise issues there. 8 | -------------------------------------------------------------------------------- /bin/docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var assert = require('assert'), 4 | fs = require('fs'), 5 | Mailgun = require('../'), 6 | mailgun = new Mailgun({apiKey: 'asdf'}), 7 | inflection = require('inflection'), 8 | path = require('path'), 9 | resources = require('../lib/schema').definitions, 10 | resource, 11 | resourceName; 12 | 13 | for (resourceName in resources) { 14 | resource = resources[resourceName]; 15 | createDocumentation(resourceName, resource); 16 | } 17 | 18 | function createDocumentation (resourceName, resource) { 19 | var file = createFile(resourceName); 20 | 21 | process.stdout.write('\nBuilding docs for ' + resourceName + ':\n'); 22 | 23 | fs.appendFileSync(file, '# ' + resourceName + '\n\n'); 24 | if (resource.description) fs.appendFileSync(file, resource.description + '\n\n'); 25 | 26 | addActions(file, resource.links); 27 | addAttributes(file, resource.attributes); 28 | } 29 | 30 | function getName(name) { 31 | name = name.toLowerCase(); 32 | name = inflection.dasherize(name).replace(/-/g, '_'); 33 | name = inflection.camelize(name, true); 34 | 35 | return name; 36 | } 37 | 38 | function createFile (resourceName) { 39 | var fileName = getName(resourceName), 40 | file = path.join(__dirname, '../docs/api/' + fileName + '.md'); 41 | 42 | fs.writeFileSync(file, ''); 43 | 44 | return file; 45 | } 46 | 47 | function addActions (file, actions) { 48 | var action, actionName, i; 49 | 50 | process.stdout.write(' Building actions:'); 51 | fs.appendFileSync(file, '## Actions\n\n'); 52 | 53 | for (i in actions) { 54 | action = actions[i]; 55 | actionName = action.title; 56 | 57 | process.stdout.write('\n ' + actionName); 58 | addAction(file, actionName, action); 59 | } 60 | } 61 | 62 | function addAction (file, actionName, action) { 63 | 64 | fs.appendFileSync(file, '### `' + getName(actionName) + '`\n\n'); 65 | if (resource.description) fs.appendFileSync(file, action.description + '\n\n'); 66 | fs.appendFileSync(file, '`' + getFunctionCall(actionName, action) + '`\n\n'); 67 | fs.appendFileSync(file, 'Method | '); 68 | fs.appendFileSync(file, 'Path\n'); 69 | fs.appendFileSync(file, '--- | '); 70 | fs.appendFileSync(file, '---\n'); 71 | fs.appendFileSync(file, action.method + ' | '); 72 | 73 | // HACK append the domain to href 74 | var realHref = appendDomainToAction(action.method, action.href); 75 | 76 | fs.appendFileSync(file, prettifyHref(realHref) + '\n\n'); 77 | addActionAttributes(file, action); 78 | 79 | checkAction(actionName, action); 80 | } 81 | 82 | function appendDomainToAction(method, href) { 83 | var d = '/{domain}'; 84 | 85 | //filter out API calls that do not require a domain specified 86 | if ((href.indexOf('/routes') >= 0) 87 | || (href.indexOf('/lists') >= 0) 88 | || (href.indexOf('/address') >= 0) 89 | || (href.indexOf('/domains') >= 0 )) { 90 | d = ''; 91 | } 92 | else if ((href.indexOf('/messages') >= 0) 93 | && (method === 'GET' || method === 'DELETE')) { 94 | d = '/domains/{domain}'; 95 | } 96 | 97 | return d.concat(href); 98 | } 99 | 100 | function getFunctionCall (actionName, action) { 101 | // HACKY special case for members bulk add 102 | var actionPath = action.href.replace(/\.json/gi,''); 103 | var actionPath = actionPath.replace(/\.mime/gi,''); 104 | 105 | var path = actionPath.split(/\//), 106 | segments = path.slice(1, path.length), 107 | functionCall = 'mailgun', 108 | i, 109 | segment; 110 | 111 | for (i = 0; i < segments.length; i ++) { 112 | segment = segments[i]; 113 | 114 | if (segment.match(/{[^}]+}/)) { 115 | continue; 116 | } else { 117 | functionCall += '.' + getName(segment); 118 | 119 | if (segments[i + 1] && segments[i + 1].match(/{[^}]+}/)) { 120 | functionCall += '(' + prettifySegment(segments[i + 1]) + ')'; 121 | } else { 122 | functionCall += '()'; 123 | } 124 | } 125 | } 126 | 127 | functionCall += '.' + getName(actionName) + '('; 128 | 129 | if (['PATCH', 'POST', 'PUT'].indexOf(action.method) > -1) { 130 | functionCall += '{attributes}, '; 131 | } 132 | 133 | functionCall += '{callback});'; 134 | 135 | return functionCall; 136 | } 137 | 138 | function prettifyHref (href) { 139 | var segments = href.split(/\//); 140 | segments = segments.slice(1, segments.length); 141 | segments = segments.map(prettifySegment) 142 | href = '/' + segments.join('/'); 143 | return href; 144 | } 145 | 146 | function prettifySegment (segment) { 147 | var unescaped = unescape(segment), 148 | identities; 149 | 150 | if (unescaped === segment) { 151 | return segment; 152 | } 153 | 154 | identities = getIdentitiesFromParam(unescaped); 155 | return '{' + identities[0] + '_' + identities.slice(1, identities.length).sort().join('_or_') + '}'; 156 | } 157 | 158 | function getIdentitiesFromParam(param) { 159 | var identities = param.replace(/[{}()#]/g, '').split("/"), 160 | resourceName; 161 | 162 | identities = identities.slice(1, identities.length); 163 | identities = resources[identities[1]].definitions.identity.anyOf; 164 | 165 | identities = identities.map(function (identity) { 166 | var parts = identity.$ref.split('/'); 167 | resourceName = parts[2]; 168 | return parts.slice(-1)[0] 169 | }); 170 | 171 | identities.unshift(resourceName); 172 | 173 | return identities; 174 | } 175 | 176 | function addActionAttributes (file, action) { 177 | if (!action.attributes) return; 178 | 179 | addActionAttributeGroup(file, action.attributes.optional, 'Optional'); 180 | fs.appendFileSync(file, '\n'); 181 | 182 | addActionAttributeGroup(file, action.attributes.required, 'Required'); 183 | fs.appendFileSync(file, '\n'); 184 | } 185 | 186 | function addActionAttributeGroup (file, attributes, type) { 187 | if (!attributes) return; 188 | 189 | fs.appendFileSync(file, '#### ' + type + ' Attributes\n\n'); 190 | 191 | attributes.forEach(function (attribute) { 192 | fs.appendFileSync(file, '- ' + attribute + '\n'); 193 | }); 194 | } 195 | 196 | function addAttributes (file, attributes) { 197 | var attributeName, attribute; 198 | 199 | if (!attributes) return; 200 | 201 | process.stdout.write('\n Building attributes\n'); 202 | fs.appendFileSync(file, '## Attributes\n\n'); 203 | 204 | for (attributeName in attributes) { 205 | attribute = attributes[attributeName]; 206 | addAttribute(file, attributeName, attribute); 207 | } 208 | } 209 | 210 | function addAttribute (file, attributeName, attribute) { 211 | fs.appendFileSync(file, '### `' + attributeName + '`\n\n'); 212 | 213 | fs.appendFileSync(file, '*' + attribute.description + '*\n\n'); 214 | 215 | fs.appendFileSync(file, 'Example | '); 216 | fs.appendFileSync(file, 'Serialized? | '); 217 | fs.appendFileSync(file, 'Type\n'); 218 | fs.appendFileSync(file, '--- | '); 219 | fs.appendFileSync(file, '--- | '); 220 | fs.appendFileSync(file, '---\n'); 221 | fs.appendFileSync(file, '`' + attribute.example + '` | '); 222 | fs.appendFileSync(file, attribute.serialized + ' | '); 223 | fs.appendFileSync(file, attribute.type + '\n\n'); 224 | } 225 | 226 | function checkAction (actionName, action) { 227 | var functionToTest = getFunctionCall(actionName, action).replace(/{.*}/g, '').slice(0, -3) 228 | assert.strictEqual(eval('typeof ' + functionToTest), 'function', functionToTest + ' is not a Function'); 229 | process.stdout.write(' ' + String.fromCharCode(0x2713)); 230 | } 231 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailgun/mailgun-js-boland/0190873362bf0b79d2813d711f6492e0d351047e/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # mailgun.js 2 | 3 | Simple Node.js module for [Mailgun](http://www.mailgun.com). 4 | 5 | [![npm version](https://img.shields.io/npm/v/mailgun-js.svg?style=flat-square)](https://www.npmjs.com/package/mailgun-js) 6 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square)](https://standardjs.com) 7 | [![License](https://img.shields.io/github/license/bojand/mailgun-js.svg?style=flat-square)](https://raw.githubusercontent.com/bojand/mailgun-js/master/LICENSE.txt) 8 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?style=flat-square)](https://www.paypal.me/bojandj) 9 | [![Buy me a coffee](https://img.shields.io/badge/buy%20me-a%20coffee-orange.svg?style=flat-square)](https://www.buymeacoffee.com/bojand) 10 | 11 | ## Installation 12 | 13 | `npm install mailgun-js` 14 | 15 | ## Usage overview 16 | 17 | This is a simple Node.js module for interacting with the [Mailgun](http://www.mailgun.com) API. This module is intended to be used within Node.js environment and **not** from the browser. For browser use the [mailgun.js](https://github.com/mailgun/mailgun-js) module. 18 | 19 | Please see [Mailgun Documentation](https://documentation.mailgun.com) for full Mailgun API reference. 20 | 21 | This module works by providing proxy objects for interacting with different resources through the Mailgun API. 22 | Most methods take a `data` parameter, which is a Javascript object that would contain the arguments for the Mailgun API. 23 | All methods take a final parameter callback with two parameters: `error`, and `body`. 24 | We try to parse the `body` into a javascript object, and return it to the callback as such for easier use and inspection by the client. 25 | If there was an error a new `Error` object will be passed to the callback in the `error` parameter. 26 | If the error originated from the (Mailgun) server, the response code will be available in the `statusCode` property 27 | of the `error` object passed in the callback. 28 | See the `/docs` folder for detailed documentation. For full usage examples see the `/test` folder. 29 | 30 | ```js 31 | var api_key = 'XXXXXXXXXXXXXXXXXXXXXXX'; 32 | var domain = 'www.mydomain.com'; 33 | var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain}); 34 | 35 | var data = { 36 | from: 'Excited User ', 37 | to: 'serobnic@mail.ru', 38 | subject: 'Hello', 39 | text: 'Testing some Mailgun awesomeness!' 40 | }; 41 | 42 | mailgun.messages().send(data, function (error, body) { 43 | console.log(body); 44 | }); 45 | ``` 46 | 47 | Note that the `to` field is required and should contain all recipients ("TO", "CC" and "BCC") of the message (see https://documentation.mailgun.com/api-sending.html#sending). Additionally `cc` and `bcc` fields can be specified. Recipients in those fields will be addressed as such. 48 | 49 | Messages stored using the Mailgun `store()` action can be retrieved using `messages().info()` function. 50 | Optionally the MIME representation of the message can be retrieved if `MIME` argument is passed in and set to `true`. 51 | 52 | Something more elaborate. Get mailing list info, create a member and get mailing list members and update member. 53 | Notice that the proxy objects can be reused. 54 | 55 | ```js 56 | var list = mailgun.lists('mylist@mycompany.com'); 57 | 58 | list.info(function (err, data) { 59 | // `data` is mailing list info 60 | console.log(data); 61 | }); 62 | 63 | var bob = { 64 | subscribed: true, 65 | address: 'bob@gmail.com', 66 | name: 'Bob Bar', 67 | vars: {age: 26} 68 | }; 69 | 70 | list.members().create(bob, function (err, data) { 71 | // `data` is the member details 72 | console.log(data); 73 | }); 74 | 75 | list.members().list(function (err, members) { 76 | // `members` is the list of members 77 | console.log(members); 78 | }); 79 | 80 | list.members('bob@gmail.com').update({ name: 'Foo Bar' }, function (err, body) { 81 | console.log(body); 82 | }); 83 | ``` 84 | 85 | #### Options 86 | 87 | `Mailgun` object constructor options: 88 | 89 | * `apiKey` - Your Mailgun API KEY 90 | * `publicApiKey` - Your public Mailgun API KEY 91 | * `domain` - Your Mailgun Domain (Please note: domain field is `MY-DOMAIN-NAME.com`, not https://api.mailgun.net/v3/MY-DOMAIN-NAME.com) 92 | * `mute` - Set to `true` if you wish to mute the console error logs in `validateWebhook()` function 93 | * `proxy` - The proxy URI in format `http[s]://[auth@]host:port`. ex: `'http://proxy.example.com:8080'` 94 | * `timeout` - Request timeout in milliseconds 95 | * `host` - the mailgun host (default: 'api.mailgun.net') 96 | * `protocol` - the mailgun protocol (default: 'https:', possible values: 'http:' or 'https:') 97 | * `port` - the mailgun port (default: '443') 98 | * `endpoint` - the mailgun host (default: '/v3') 99 | * `retry` - the number of **total attempts** to do when performing requests. Default is `1`. 100 | That is, we will try an operation only once with no retries on error. 101 | 102 | ## Notes 103 | 104 | This project is not endorsed by or affiliated with [Mailgun](http://www.mailgun.com). 105 | The general design and some code was heavily inspired by [node-heroku-client](https://github.com/jclem/node-heroku-client). 106 | 107 | ## License 108 | 109 | Copyright (c) 2012 - 2017 OneLobby and Bojan D. 110 | 111 | Licensed under the MIT License. 112 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Guide 2 | - [Introduction](/README) 3 | - [Attachments](/attach) 4 | - [Batch Sending](/batch) 5 | - [Generic Requests](/generic) 6 | - [Promises](/promises) 7 | - [Webhook Validation](/webhook) 8 | - [Email Validation](/email_validation) 9 | - [Debug Logging](/debug) 10 | - [Test Mode](/testmode) 11 | - [Tests](/tests) 12 | - API Reference 13 | - [Message](/api/message) 14 | - [Domain](/api/domain) 15 | - [Credentials](/api/credentials) 16 | - [Tracking](/api/tracking) 17 | - [Complaints](/api/complaints) 18 | - [Events](/api/events) 19 | - [Stats](/api/stats) 20 | - [Tags](/api/tags) 21 | - [Bounces](/api/bounces) 22 | - [Unsubscribes](/api/unsubscribes) 23 | - [Routes](/api/routes) 24 | - [Mailing Lists](/api/list) 25 | - [Mailing List Members](/api/members) 26 | -------------------------------------------------------------------------------- /docs/api/bounces.md: -------------------------------------------------------------------------------- 1 | # bounces 2 | 3 | Mailgun automatically handles bounced emails. The list of bounced addresses can be accessed programmatically. 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | Fetches the list of bounces. 10 | 11 | `mailgun.bounces().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /{domain}/bounces 16 | 17 | ### `info` 18 | 19 | Fetches a single bounce event by a given email address. 20 | 21 | `mailgun.bounces({address}).info({callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | GET | /{domain}/bounces/{address} 26 | 27 | ### `delete` 28 | 29 | Clears a given bounce event. 30 | 31 | `mailgun.bounces({address}).delete({callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | DELETE | /{domain}/bounces/{address} 36 | 37 | ### `create` 38 | 39 | Adds a permanent bounce to the bounces table. Updates the existing record if already here. 40 | 41 | `mailgun.bounces().create({attributes}, {callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | POST | /{domain}/bounces 46 | 47 | -------------------------------------------------------------------------------- /docs/api/complaints.md: -------------------------------------------------------------------------------- 1 | # complaints 2 | 3 | This API allows you to programmatically download the list of users who have complained, add a complaint, or delete a complaint. 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | Fetches the list of complaints. 10 | 11 | `mailgun.complaints().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /{domain}/complaints 16 | 17 | ### `create` 18 | 19 | Adds an address to the complaints table. 20 | 21 | `mailgun.complaints().create({attributes}, {callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | POST | /{domain}/complaints 26 | 27 | ### `info` 28 | 29 | Fetches a single spam complaint by a given email address. 30 | 31 | `mailgun.complaints({address}).info({callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | GET | /{domain}/complaints/{address} 36 | 37 | ### `delete` 38 | 39 | Removes a given spam complaint. 40 | 41 | `mailgun.complaints({address}).delete({callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | DELETE | /{domain}/complaints/{address} 46 | 47 | -------------------------------------------------------------------------------- /docs/api/credentials.md: -------------------------------------------------------------------------------- 1 | # credentials 2 | 3 | Programmatically get and modify domain credentials. 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | Returns a list of SMTP credentials for the defined domain. 10 | 11 | `mailgun.domains({domain}).credentials().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /domains/{domain}/credentials 16 | 17 | ### `create` 18 | 19 | Creates a new set of SMTP credentials for the defined domain. 20 | 21 | `mailgun.domains({domain}).credentials().create({attributes}, {callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | POST | /domains/{domain}/credentials 26 | 27 | ### `update` 28 | 29 | Updates the specified SMTP credentials. Currently only the password can be changed. 30 | 31 | `mailgun.domains({domain}).credentials({login}).update({attributes}, {callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | PUT | /domains/{domain}/credentials/{login} 36 | 37 | ### `delete` 38 | 39 | Deletes the defined SMTP credentials. 40 | 41 | `mailgun.domains({domain}).credentials({login}).delete({callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | DELETE | /domains/{domain}/credentials/{login} 46 | 47 | -------------------------------------------------------------------------------- /docs/api/domain.md: -------------------------------------------------------------------------------- 1 | # domain 2 | 3 | This API allows you to create, access, and validate domains programmatically. 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | Returns a list of domains under your account in JSON. 10 | 11 | `mailgun.domains().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /domains 16 | 17 | ### `info` 18 | 19 | Returns a single domain, including credentials and DNS records. 20 | 21 | `mailgun.domains({domain}).info({callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | GET | /domains/{domain} 26 | 27 | ### `create` 28 | 29 | Create a new domain. 30 | 31 | `mailgun.domains().create({attributes}, {callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | POST | /domains 36 | 37 | ### `delete` 38 | 39 | Delete a domain from your account. 40 | 41 | `mailgun.domains({domain}).delete({callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | DELETE | /domains/{domain} 46 | 47 | ### `verify` 48 | 49 | Verifies and returns a single domain, including credentials and DNS records. 50 | 51 | `mailgun.domains({domain}).verify().verify({attributes}, {callback});` 52 | 53 | Method | Path 54 | --- | --- 55 | PUT | /domains/{domain}/verify 56 | 57 | -------------------------------------------------------------------------------- /docs/api/events.md: -------------------------------------------------------------------------------- 1 | # events 2 | 3 | Query events that happen to your emails. See http://documentation.mailgun.com/api-events.html 4 | 5 | ## Actions 6 | 7 | ### `get` 8 | 9 | Queries event records. 10 | 11 | `mailgun.events().get({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /{domain}/events 16 | 17 | -------------------------------------------------------------------------------- /docs/api/list.md: -------------------------------------------------------------------------------- 1 | # list 2 | 3 | You can programmatically work with mailing lists and mailing list members using Mailgun Mailing List API. 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | Returns a list of mailing lists under your account. 10 | 11 | `mailgun.lists().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /lists 16 | 17 | ### `info` 18 | 19 | Returns a single mailing list by a given address. 20 | 21 | `mailgun.lists({address}).info({callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | GET | /lists/{address} 26 | 27 | ### `create` 28 | 29 | Creates a new mailing list. 30 | 31 | `mailgun.lists().create({attributes}, {callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | POST | /lists 36 | 37 | ### `update` 38 | 39 | Update mailing list properties, such as address, description or name. 40 | 41 | `mailgun.lists({address}).update({attributes}, {callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | PUT | /lists/{address} 46 | 47 | ### `delete` 48 | 49 | Deletes a mailing list. 50 | 51 | `mailgun.lists({address}).delete({callback});` 52 | 53 | Method | Path 54 | --- | --- 55 | DELETE | /lists/{address} 56 | 57 | -------------------------------------------------------------------------------- /docs/api/members.md: -------------------------------------------------------------------------------- 1 | # members 2 | 3 | Programatically work with mailing lists members. 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | Fetches the list of mailing list members. 10 | 11 | `mailgun.lists({address}).members().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /lists/{address}/members 16 | 17 | ### `page` 18 | 19 | Paginate over list members in the given mailing list 20 | 21 | `mailgun.lists({address}).members().pages().page({callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | GET | /lists/{address}/members/pages 26 | 27 | ### `info` 28 | 29 | Retrieves a mailing list member. 30 | 31 | `mailgun.lists({address}).members({member_address}).info({callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | GET | /lists/{address}/members/{member_address} 36 | 37 | ### `create` 38 | 39 | Adds a member to the mailing list. 40 | 41 | `mailgun.lists({address}).members().create({attributes}, {callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | POST | /lists/{address}/members 46 | 47 | ### `add` 48 | 49 | Adds multiple members, up to 1,000 per call, to a Mailing List. 50 | 51 | `mailgun.lists({address}).members().add({attributes}, {callback});` 52 | 53 | Method | Path 54 | --- | --- 55 | POST | /lists/{address}/members.json 56 | 57 | ### `update` 58 | 59 | Updates a mailing list member with given properties. 60 | 61 | `mailgun.lists({address}).members({member_address}).update({attributes}, {callback});` 62 | 63 | Method | Path 64 | --- | --- 65 | PUT | /lists/{address}/members/{member_address} 66 | 67 | ### `delete` 68 | 69 | Delete a mailing list member. 70 | 71 | `mailgun.lists({address}).members({member_address}).delete({callback});` 72 | 73 | Method | Path 74 | --- | --- 75 | DELETE | /lists/{address}/members/{member_address} 76 | 77 | -------------------------------------------------------------------------------- /docs/api/message.md: -------------------------------------------------------------------------------- 1 | # message 2 | 3 | This API allows you to send, access, and delete mesages programmatically. 4 | 5 | ## Actions 6 | 7 | ### `info` 8 | 9 | Returns a single message in JSON format. To get full MIME message set MIME to true 10 | 11 | `mailgun.messages({message}).info({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /domains/{domain}/messages/{message} 16 | 17 | ### `send` 18 | 19 | Sends a message by assembling it from the components. 20 | 21 | `mailgun.messages().send({attributes}, {callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | POST | /{domain}/messages 26 | 27 | ### `sendMime` 28 | 29 | Sends a message in MIME format. 30 | 31 | `mailgun.messages().sendMime({attributes}, {callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | POST | /{domain}/messages.mime 36 | 37 | ### `delete` 38 | 39 | To delete an inbound message that has been stored via the store() action. 40 | 41 | `mailgun.messages({message}).delete({callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | DELETE | /domains/{domain}/messages/{message} 46 | 47 | -------------------------------------------------------------------------------- /docs/api/routes.md: -------------------------------------------------------------------------------- 1 | # routes 2 | 3 | Mailgun Routes are a powerful way to handle the incoming traffic. This API allows you to work with routes programmatically. 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | Fetches the list of routes. 10 | 11 | `mailgun.routes().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /routes 16 | 17 | ### `info` 18 | 19 | Returns a single route object based on its ID. 20 | 21 | `mailgun.routes({id}).info({callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | GET | /routes/{id} 26 | 27 | ### `create` 28 | 29 | Creates a new route. 30 | 31 | `mailgun.routes().create({attributes}, {callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | POST | /routes 36 | 37 | ### `update` 38 | 39 | Updates a given route by ID. 40 | 41 | `mailgun.routes({id}).update({attributes}, {callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | PUT | /routes/{id} 46 | 47 | ### `delete` 48 | 49 | Deletes a route based on the id. 50 | 51 | `mailgun.routes({id}).delete({callback});` 52 | 53 | Method | Path 54 | --- | --- 55 | DELETE | /routes/{id} 56 | 57 | -------------------------------------------------------------------------------- /docs/api/stats.md: -------------------------------------------------------------------------------- 1 | # stats 2 | 3 | Various data and event statistics for you mailgun account. See http://documentation.mailgun.com/api-stats.html 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | Returns a list of event stat items. Each record represents counts for one event per one day. 10 | 11 | `mailgun.stats().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /{domain}/stats 16 | 17 | -------------------------------------------------------------------------------- /docs/api/tags.md: -------------------------------------------------------------------------------- 1 | # tags 2 | 3 | Deletes all counters for particular tag and the tag itself. See http://documentation.mailgun.com/api-stats.html 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | List all tags. 10 | 11 | `mailgun.tags().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /{domain}/tags 16 | 17 | ### `info` 18 | 19 | Gets a specific tag. 20 | 21 | `mailgun.tags({tag}).info({callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | GET | /{domain}/tags/{tag} 26 | 27 | ### `info` 28 | 29 | Returns statistics for a given tag. 30 | 31 | `mailgun.tags({tag}).stats().info({callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | GET | /{domain}/tags/{tag}/stats 36 | 37 | ### `list` 38 | 39 | Returns a list of countries of origin for a given domain for different event types. 40 | 41 | `mailgun.tags({tag}).stats().aggregates().countries().list({callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | GET | /{domain}/tags/{tag}/stats/aggregates/countries 46 | 47 | ### `list` 48 | 49 | Returns a list of email providers for a given domain for different event types. 50 | 51 | `mailgun.tags({tag}).stats().aggregates().providers().list({callback});` 52 | 53 | Method | Path 54 | --- | --- 55 | GET | /{domain}/tags/{tag}/stats/aggregates/providers 56 | 57 | ### `list` 58 | 59 | Returns a list of devices for a given domain that have triggered event types. 60 | 61 | `mailgun.tags({tag}).stats().aggregates().devices().list({callback});` 62 | 63 | Method | Path 64 | --- | --- 65 | GET | /{domain}/tags/{tag}/stats/aggregates/devices 66 | 67 | ### `delete` 68 | 69 | Deletes all counters for particular tag and the tag itself. 70 | 71 | `mailgun.tags({tag}).delete({callback});` 72 | 73 | Method | Path 74 | --- | --- 75 | DELETE | /{domain}/tags/{tag} 76 | 77 | -------------------------------------------------------------------------------- /docs/api/tracking.md: -------------------------------------------------------------------------------- 1 | # tracking 2 | 3 | Programmatically get and modify domain tracking settings. 4 | 5 | ## Actions 6 | 7 | ### `info` 8 | 9 | Returns tracking settings for a domain. 10 | 11 | `mailgun.domains({domain}).tracking().info({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /domains/{domain}/tracking 16 | 17 | ### `update` 18 | 19 | Updates the open tracking settings for a domain. 20 | 21 | `mailgun.domains({domain}).tracking().open().update({attributes}, {callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | PUT | /domains/{domain}/tracking/open 26 | 27 | ### `update` 28 | 29 | Updates the click tracking settings for a domain. 30 | 31 | `mailgun.domains({domain}).tracking().click().update({attributes}, {callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | PUT | /domains/{domain}/tracking/click 36 | 37 | ### `update` 38 | 39 | Updates the unsubscribe tracking settings for a domain. 40 | 41 | `mailgun.domains({domain}).tracking().unsubscribe().update({attributes}, {callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | PUT | /domains/{domain}/tracking/unsubscribe 46 | 47 | -------------------------------------------------------------------------------- /docs/api/unsubscribes.md: -------------------------------------------------------------------------------- 1 | # unsubscribes 2 | 3 | This API allows you to programmatically download the list of recipients who have unsubscribed from your emails. You can also programmatically “clear” the unsubscribe event. 4 | 5 | ## Actions 6 | 7 | ### `list` 8 | 9 | Fetches the list of unsubscribes. 10 | 11 | `mailgun.unsubscribes().list({callback});` 12 | 13 | Method | Path 14 | --- | --- 15 | GET | /{domain}/unsubscribes 16 | 17 | ### `info` 18 | 19 | Retreives a single unsubscribe record. 20 | 21 | `mailgun.unsubscribes({address}).info({callback});` 22 | 23 | Method | Path 24 | --- | --- 25 | GET | /{domain}/unsubscribes/{address} 26 | 27 | ### `delete` 28 | 29 | Removes an address from the unsubscribes table. 30 | 31 | `mailgun.unsubscribes({address}).delete({callback});` 32 | 33 | Method | Path 34 | --- | --- 35 | DELETE | /{domain}/unsubscribes/{address} 36 | 37 | ### `create` 38 | 39 | Adds address to unsubscribed table. 40 | 41 | `mailgun.unsubscribes().create({attributes}, {callback});` 42 | 43 | Method | Path 44 | --- | --- 45 | POST | /{domain}/unsubscribes 46 | 47 | -------------------------------------------------------------------------------- /docs/attach.md: -------------------------------------------------------------------------------- 1 | ### Attachments 2 | 3 | Attachments can be sent using either the `attachment` or `inline` parameters. `inline` parameter can be use to send an 4 | attachment with `inline` disposition. It can be used to send inline images. Both types are supported with same mechanisms 5 | as described, we will just use `attachment` parameter in the documentation below but same stands for `inline`. 6 | 7 | Sending attachments can be done in a few ways. We can use the path to a file in the `attachment` parameter. 8 | If the `attachment` parameter is of type `string` it is assumed to be the path to a file. 9 | 10 | ```js 11 | var filepath = path.join(__dirname, 'mailgun_logo.png'); 12 | 13 | var data = { 14 | from: 'Excited User ', 15 | to: 'serobnic@mail.ru', 16 | subject: 'Hello', 17 | text: 'Testing some Mailgun awesomeness!', 18 | attachment: filepath 19 | }; 20 | 21 | mailgun.messages().send(data, function (error, body) { 22 | console.log(body); 23 | }); 24 | ``` 25 | 26 | We can pass a buffer (has to be a `Buffer` object) of the data. If a buffer is used the data will be attached using a 27 | generic filename "file". 28 | 29 | ```js 30 | var filepath = path.join(__dirname, 'mailgun_logo.png'); 31 | var file = fs.readFileSync(filepath); 32 | 33 | var data = { 34 | from: 'Excited User ', 35 | to: 'serobnic@mail.ru', 36 | subject: 'Hello', 37 | text: 'Testing some Mailgun awesomeness!', 38 | attachment: file 39 | }; 40 | 41 | mailgun.messages().send(data, function (error, body) { 42 | console.log(body); 43 | }); 44 | ``` 45 | 46 | We can also pass in a stream of the data. This is useful if you're attaching a file from the internet. 47 | 48 | ```js 49 | var request = require('request'); 50 | var file = request("https://www.google.ca/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"); 51 | 52 | var data = { 53 | from: 'Excited User ', 54 | to: 'serobnic@mail.ru', 55 | subject: 'Hello', 56 | text: 'Testing some Mailgun awesomeness!', 57 | attachment: file 58 | }; 59 | 60 | mailgun.messages().send(data, function (error, body) { 61 | console.log(body); 62 | }); 63 | ``` 64 | 65 | Finally we provide a `Mailgun.Attachment` class to add attachments with a bit more customization. The Attachment 66 | constructor takes an `options` object. The `options` parameters can have the following fields: 67 | * `data` - can be one of 68 | * a string representing file path to the attachment 69 | * a buffer of file data 70 | * an instance of `Stream` which means it is a readable stream. 71 | * `filename` - the file name to be used for the attachment. Default is 'file' 72 | * `contentType` - the content type. Required for case of `Stream` data. Ex. `image/jpeg`. 73 | * `knownLength` - the content length in bytes. Required for case of `Stream` data. 74 | 75 | If an attachment object does not satisfy those valid conditions it is ignored. Multiple attachments can be sent by 76 | passing an array in the `attachment` parameter. The array elements can be of any one of the valid types and each one 77 | will be handled appropriately. 78 | 79 | ```js 80 | var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain}); 81 | var filename = 'mailgun_logo.png'; 82 | var filepath = path.join(__dirname, filename); 83 | var file = fs.readFileSync(filepath); 84 | 85 | var attch = new mailgun.Attachment({data: file, filename: filename}); 86 | 87 | var data = { 88 | from: 'Excited User ', 89 | to: 'serobnic@mail.ru', 90 | subject: 'Hello', 91 | text: 'Testing some Mailgun awesomeness!', 92 | attachment: attch 93 | }; 94 | 95 | mailgun.messages().send(data, function (error, body) { 96 | console.log(body); 97 | }); 98 | ``` 99 | 100 | ```js 101 | var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain}); 102 | var filename = 'mailgun_logo.png'; 103 | var filepath = path.join(__dirname, filename); 104 | var fileStream = fs.createReadStream(filepath); 105 | var fileStat = fs.statSync(filepath); 106 | 107 | msg.attachment = new mailgun.Attachment({ 108 | data: fileStream, 109 | filename: 'my_custom_name.png', 110 | knownLength: fileStat.size, 111 | contentType: 'image/png'}); 112 | 113 | mailgun.messages().send(data, function (error, body) { 114 | console.log(body); 115 | }); 116 | ``` 117 | 118 | #### Sending MIME messages 119 | 120 | Sending messages in MIME format can be accomplished using the `sendMime()` function of the `messages()` proxy object. 121 | The `data` parameter for the function has to have `to` and `message` properties. The `message` property can be a full 122 | file path to the MIME file, a stream of the file (that is a `Stream` object), or a string representation of the MIME 123 | message. To build a MIME string you can use the [nodemailer](https://www.npmjs.org/package/nodemailer) library. 124 | Some examples: 125 | 126 | ```js 127 | var domain = 'www.mydomain.com'; 128 | var mailgun = require('mailgun-js')({ apiKey: "YOUR API KEY", domain: domain }); 129 | var MailComposer = require('nodemailer/lib/mail-composer'); 130 | 131 | var mailOptions = { 132 | from: 'you@samples.mailgun.org', 133 | to: 'mm@samples.mailgun.org', 134 | subject: 'Test email subject', 135 | text: 'Test email text' 136 | }; 137 | 138 | var mail = new MailComposer(mailOptions); 139 | 140 | mail.compile().build((err, message) => { 141 | var dataToSend = { 142 | to: 'mm@samples.mailgun.org', 143 | message: message.toString('ascii') 144 | }; 145 | 146 | mailgun.messages().sendMime(dataToSend, (err, body) => { 147 | if (err) { 148 | console.log(err); 149 | return; 150 | } 151 | 152 | console.log(body); 153 | }); 154 | }); 155 | ``` 156 | #### Referencing MIME file 157 | 158 | ```js 159 | var filepath = '/path/to/message.mime'; 160 | 161 | var data = { 162 | to: fixture.message.to, 163 | message: filepath 164 | }; 165 | 166 | mailgun.messages().sendMime(data, function (err, body) { 167 | console.log(body); 168 | }); 169 | ``` 170 | 171 | ```js 172 | var filepath = '/path/to/message.mime'; 173 | 174 | var data = { 175 | to: fixture.message.to, 176 | message: fs.createReadStream(filepath) 177 | }; 178 | 179 | mailgun.messages().sendMime(data, function (err, body) { 180 | console.log(body); 181 | }); 182 | ``` 183 | -------------------------------------------------------------------------------- /docs/batch.md: -------------------------------------------------------------------------------- 1 | ## Batch Sending 2 | 3 | Mailgun-js can be used for batch sending. Please see Mailgun [documentation](https://documentation.mailgun.com/user_manual.html#batch-sending) for more details. 4 | Example source code: 5 | 6 | ```js 7 | var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain}); 8 | 9 | var recipientVars = { 10 | 'bob@companyx.com': { 11 | first: 'Bob', 12 | id: 1 13 | }, 14 | 'alice@companyy.com': { 15 | first: 'Alice', 16 | id: 2 17 | } 18 | }; 19 | 20 | var data = { 21 | from: 'Excited User ', 22 | to: ['bob@companyx.com', 'alice@companyy.com'], 23 | subject: 'Hey, %recipient.first%', 24 | 'recipient-variables': recipientVars, 25 | text: 'If you wish to unsubscribe, click http://mailgun/unsubscribe/%recipient.id%', 26 | }; 27 | 28 | mailgun.messages().send(data, function (error, body) { 29 | console.log(body); 30 | }); 31 | ``` 32 | 33 | When sending using `sendMime()` we have to set the MIME header `X-Mailgun-Recipient-Variables` appropriately to the recipient variables data. For example using [mailcomposer](https://nodemailer.com/extras/mailcomposer/): 34 | 35 | ```js 36 | const MailComposer = require('nodemailer/lib/mail-composer') 37 | const mailgun = require('mailgun-js')({ apiKey: api_key, domain: domain }) 38 | 39 | const toArray = ['recipient+test1@gmail.com', 'recipient+test2@gmail.com'] 40 | 41 | const recipientVars = { 42 | 'recipient+test1@gmail.com': { 43 | first: 'Recv1', 44 | id: 1 45 | }, 46 | 'recipient+test2@gmail.com': { 47 | first: 'Recv2', 48 | id: 2 49 | } 50 | } 51 | 52 | const mailOptions = { 53 | from: 'test+sender@gmail.com', 54 | to: toArray, 55 | subject: 'Hey, %recipient.first%', 56 | text: 'Hello %recipient.id%', 57 | html: 'Hello %recipient.id%', 58 | headers: { 59 | 'X-Mailgun-Recipient-Variables': JSON.stringify(recipientVars) 60 | } 61 | } 62 | 63 | const mail = new MailComposer(mailOptions) 64 | mail.compile().build(function (err, message) { 65 | const data = { 66 | to: toArray, 67 | message: message.toString('ascii'), 68 | 'recipient-variables': recipientVars 69 | } 70 | 71 | mailgun.messages().sendMime(data, function (err, body) { 72 | console.log(body) 73 | }) 74 | }) 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/debug.md: -------------------------------------------------------------------------------- 1 | ## Debug logging 2 | 3 | [debug](https://npmjs.com/package/debug) package is used for debug logging. 4 | 5 | ```sh 6 | DEBUG=mailgun-js node app.js 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/email_validation.md: -------------------------------------------------------------------------------- 1 | ## Email Addresses validation 2 | 3 | Please check Mailgun [email validation documentation](https://documentation.mailgun.com/api-email-validation.html) for more responses details. 4 | 5 | ### Validate Email Address 6 | 7 | **mailgun.validate(address, private, options, fn)** 8 | 9 | Checks if email is valid. 10 | 11 | - `private` - wether it's private validate 12 | - `options` - any additional options 13 | 14 | Example usage: 15 | 16 | ```js 17 | var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain}); 18 | 19 | mailgun.validate('test@mail.com', function (err, body) { 20 | if (body && body.is_valid) { 21 | // do something 22 | } 23 | }); 24 | ``` 25 | 26 | ### Parse Email Addresses list 27 | 28 | **mailgun.parse(address, private, options, fn)** 29 | 30 | - `private` - wether it's private validate 31 | - `options` - any additional options 32 | 33 | Parses list of email addresses and returns two lists: 34 | - parsed email addresses 35 | - unparseable email addresses 36 | 37 | Example usage: 38 | 39 | ```js 40 | var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain}); 41 | 42 | mailgun.parse([ 'test@mail.com', 'test2@mail.com' ], function (err, body) { 43 | if (error) { 44 | // handle error 45 | } else { 46 | // do something with parsed addresses: body.parsed; 47 | // do something with unparseable addresses: body.unparseable; 48 | } 49 | }); 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/generic.md: -------------------------------------------------------------------------------- 1 | ## Generic requests 2 | 3 | Mailgun-js also provides helper methods to allow users to interact with parts of the api that are not exposed already. 4 | These are not tied to the domain passed in the constructor, and thus require the full path with the domain 5 | passed in the `resource` argument. 6 | 7 | * `mailgun.get(resource, data, callback)` - sends GET request to the specified resource on api. 8 | * `mailgun.post(resource, data, callback)` - sends POST request to the specified resource on api. 9 | * `mailgun.delete(resource, data, callback)` - sends DELETE request to the specified resource on api. 10 | * `mailgun.put(resource, data, callback)` - sends PUT request to the specified resource on api. 11 | 12 | Example: Get some stats 13 | 14 | ```js 15 | mailgun.get('/samples.mailgun.org/stats', { event: ['sent', 'delivered'] }, function (error, body) { 16 | console.log(body); 17 | }); 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mailgun-js - Simple Node.js helper module for Mailgun API 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/promises.md: -------------------------------------------------------------------------------- 1 | ## Promises 2 | 3 | Module works with Node-style callbacks, but also implements (native) Promise based API. When a callback is not passed into to a function a Promise is returned that is resolved or rejected when the action succeeds to fails. 4 | 5 | ```js 6 | mailgun.lists('mylist@mydomain.com').info().then(function (data) { 7 | console.log(data) 8 | }, function (err) { 9 | console.log(err) 10 | }) 11 | ``` 12 | 13 | With `async` / `await`: 14 | 15 | ```js 16 | async function main () { 17 | const data = await mailgun.lists('mylist@mydomain.com').info() 18 | console.log(data) 19 | } 20 | 21 | main() 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/testmode.md: -------------------------------------------------------------------------------- 1 | ## Test mode 2 | 3 | Test mode can be turned on using `testMode` option. When on, no requests are actually sent to Mailgun, rather we log the request options and applicable payload and form data. By default we log to `console.log`, unless `DEBUG` is turned on, in which case we use debug logging. 4 | 5 | ```js 6 | mailgun = require('mailgun-js')({ apiKey: api_key, domain: domain, testMode: true }) 7 | 8 | const data = { 9 | from: 'mailgunjs+test1@gmail.com', 10 | to: 'mailgunjstest+recv1@gmail.com', 11 | subject: 'Test email subject', 12 | text: 'Test email text' 13 | }; 14 | 15 | mailgun.messages().send(data, function (error, body) { 16 | console.log(body); 17 | }); 18 | ``` 19 | 20 | ``` 21 | options: { hostname: 'api.mailgun.net', 22 | port: 443, 23 | protocol: 'https:', 24 | path: '/v3/sandbox12345.mailgun.org/messages', 25 | method: 'POST', 26 | headers: 27 | { 'Content-Type': 'application/x-www-form-urlencoded', 28 | 'Content-Length': 127 }, 29 | auth: 'api:key-0e8pwgtt5ylx0m94xwuzqys2-o0x4-77', 30 | agent: false, 31 | timeout: undefined } 32 | payload: 'to=mailgunjs%2Btest1%40gmail.com&from=mailgunjstest%2Brecv1%40gmail.com&subject=Test%20email%20subject&text=Test%20email%20text' 33 | form: undefined 34 | undefined 35 | ``` 36 | 37 | Note that in test mode no error or body are returned as a result. 38 | 39 | The logging can be customized using `testModeLogger` option which is a function to perform custom logging. 40 | 41 | ```js 42 | const logger = (httpOptions, payload, form) => { 43 | const { method, path } = httpOptions 44 | const hasPayload = !!payload 45 | const hasForm = !!form 46 | 47 | console.log(`%s %s payload: %s form: %s`, method, path, hasPayload, hasForm) 48 | } 49 | 50 | mailgun = require('mailgun-js')({ apiKey: api_key, domain: domain, testMode: true, testModeLogger: logger }) 51 | 52 | const data = { 53 | from: 'mailgunjs+test1@gmail.com', 54 | to: 'mailgunjstest+recv1@gmail.com', 55 | subject: 'Test email subject', 56 | text: 'Test email text' 57 | }; 58 | 59 | mailgun.messages().send(data, function (error, body) { 60 | console.log(body); 61 | }); 62 | ``` 63 | 64 | Sample output: 65 | 66 | ``` 67 | POST /v3/sandbox12345.mailgun.org/messages payload: true form: false 68 | undefined 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/tests.md: -------------------------------------------------------------------------------- 1 | ## Tests 2 | 3 | To run the test suite you must first have a Mailgun account with a domain setup. Then create a file named _./test/data/auth.json_, which contains your credentials as JSON, for example: 4 | 5 | ```json 6 | { "api_key": "XXXXXXXXXXXXXXXXXXXXXXX", "public_api_key": "XXXXXXXXXXXXXXXXXXXXXXX", "domain": "mydomain.mailgun.org" } 7 | ``` 8 | 9 | You should edit _./test/data/fixture.json_ and modify the data to match your context. 10 | 11 | Then install the dev dependencies and execute the test suite: 12 | 13 | ``` 14 | $ npm install 15 | $ npm test 16 | ``` 17 | 18 | The tests will call Mailgun API, and will send a test email, create route(s), mailing list and mailing list member. 19 | -------------------------------------------------------------------------------- /docs/webhook.md: -------------------------------------------------------------------------------- 1 | ## Webhook validation 2 | 3 | The Mailgun object also has a helper function for validating Mailgun Webhook requests 4 | (as per the [mailgun docs for securing webhooks](http://documentation.mailgun.com/user_manual.html#securing-webhooks)). 5 | This code came from [this gist](https://gist.github.com/coolaj86/81a3b61353d2f0a2552c). 6 | 7 | Example usage: 8 | 9 | ```js 10 | var mailgun = require('mailgun-js')({apiKey: api_key, domain: domain}); 11 | 12 | function router(app) { 13 | app.post('/webhooks/mailgun/*', function (req, res, next) { 14 | var body = req.body; 15 | 16 | if (!mailgun.validateWebhook(body.timestamp, body.token, body.signature)) { 17 | console.error('Request came, but not from Mailgun'); 18 | res.send({ error: { message: 'Invalid signature. Are you even Mailgun?' } }); 19 | return; 20 | } 21 | 22 | next(); 23 | }); 24 | 25 | app.post('/webhooks/mailgun/catchall', function (req, res) { 26 | // actually handle request here 27 | }); 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /lib/attachment.js: -------------------------------------------------------------------------------- 1 | const isStream = require('is-stream') 2 | 3 | /** 4 | * Creates an Attachment object. 5 | * @param {Object} options Buffer representing attachment data 6 | * data - one of: 7 | * - string representing the full file path 8 | * - buffer of the data 9 | * - readable interface (stream) 10 | * filename - optionally the filename to be used for the attachment, should be used if passing 11 | * buffer or stream in the data param 12 | * contentType - the content type for header info. Should be passed in if using stream for data 13 | * knownLength - the known length of the data. Should be passed in if using stream for data 14 | * @constructor 15 | */ 16 | class Attachment { 17 | constructor (options) { 18 | const data = options.data 19 | 20 | if (data) { 21 | if (typeof data === 'string' || Buffer.isBuffer(data) || isStream(data)) { 22 | this.data = data 23 | } 24 | } 25 | 26 | this.filename = options.filename 27 | this.contentType = options.contentType 28 | this.knownLength = options.knownLength 29 | } 30 | 31 | getType () { 32 | if (this.data) { 33 | if (typeof this.data === 'string') { 34 | return 'path' 35 | } else if (Buffer.isBuffer(this.data)) { 36 | return 'buffer' 37 | } else if (isStream(this.data)) { 38 | return 'stream' 39 | } 40 | } 41 | 42 | return 'unknown' 43 | } 44 | } 45 | 46 | module.exports = Attachment 47 | -------------------------------------------------------------------------------- /lib/build.js: -------------------------------------------------------------------------------- 1 | const inflection = require('inflection') 2 | const pathProxy = require('path-proxy') 3 | const promisifyCall = require('promisify-call') 4 | 5 | class Builder { 6 | constructor (baseObj, resources) { 7 | this.baseObj = baseObj 8 | this.resources = resources 9 | } 10 | 11 | build () { 12 | Object.keys(this.resources).forEach((key) => { 13 | // console.log('building ' + key); 14 | this.buildResource(this.resources[key]) 15 | }) 16 | } 17 | 18 | buildResource (resource) { 19 | resource.links.forEach(this.buildAction, this) 20 | } 21 | 22 | buildAction (action) { 23 | const actionName = action.title 24 | const properties = action.properties 25 | const requiredProps = action.required 26 | 27 | // HACKY special case for members bulk add and send MIME endpoints 28 | const path = action.href.replace(/\.json/gi, '').replace(/\.mime/gi, '') 29 | const constructor = pathProxy.pathProxy(this.baseObj, path) 30 | 31 | function impl (data, fn) { 32 | let requestPath = action.href 33 | const pathParams = action.href.match(/{[^}]+}/g) || [] 34 | 35 | if (typeof data === 'function') { 36 | fn = data 37 | data = undefined 38 | } 39 | 40 | let err 41 | 42 | if (this.params.length !== pathParams.length) { 43 | err = new Error(`Invalid number of params in path (expected ${pathParams.length}, got ${this.params.length}).`) 44 | 45 | return fn(err) 46 | } 47 | 48 | this.params.forEach((param) => { 49 | requestPath = requestPath.replace(/{[^}]+}/, param) 50 | }) 51 | 52 | // check required payload properties 53 | if (requiredProps && requiredProps.length > 0) { 54 | if (!data) { 55 | err = new Error('Missing parameters.') 56 | } else { 57 | for (let i = 0; i < requiredProps.length; i++) { 58 | const prop = requiredProps[i] 59 | 60 | if (typeof data[prop] === 'undefined') { 61 | err = new Error(`Missing parameter '${prop}'`) 62 | break 63 | } 64 | } 65 | } 66 | } 67 | 68 | if (err) { 69 | return fn(err) 70 | } 71 | 72 | // check payload property types 73 | for (const key in properties) { 74 | if (data && data[key]) { 75 | const type = properties[key].type 76 | 77 | let dataType = typeof data[key] 78 | 79 | if (Array.isArray(data[key])) { 80 | dataType = 'array' 81 | } 82 | 83 | if (Array.isArray(type)) { 84 | if (type.indexOf(dataType) === -1) { 85 | err = new Error(`Invalid parameter type. ${key} must be of type: ${type}.`) 86 | break 87 | } 88 | } else if (dataType !== type) { 89 | err = new Error(`Invalid parameter type. ${key} must be of type: ${type}.`) 90 | break 91 | } 92 | } 93 | } 94 | 95 | if (err) { 96 | return fn(err) 97 | } 98 | 99 | this.client = this.base 100 | 101 | return this.client.request(action.method, requestPath, data, fn) 102 | } 103 | 104 | function promisifed (data, fn) { 105 | return promisifyCall(this, impl, data, fn) 106 | } 107 | 108 | constructor.prototype[getName(actionName)] = promisifed 109 | } 110 | } 111 | 112 | function getName (name) { 113 | name = name.toLowerCase() 114 | name = inflection.dasherize(name).replace(/-/g, '_') 115 | name = inflection.camelize(name, true) 116 | 117 | return name 118 | } 119 | 120 | function build (baseObj, resources) { 121 | const b = new Builder(baseObj, resources) 122 | 123 | b.build() 124 | } 125 | 126 | exports.build = build 127 | -------------------------------------------------------------------------------- /lib/mailgun.js: -------------------------------------------------------------------------------- 1 | const tsscmp = require('tsscmp') 2 | const crypto = require('crypto') 3 | 4 | const Attachment = require('./attachment') 5 | const Request = require('./request') 6 | const builder = require('./build') 7 | const resources = require('./schema').definitions 8 | 9 | const mailgunExpirey = 15 * 60 * 1000 10 | const mailgunHashType = 'sha256' 11 | const mailgunSignatureEncoding = 'hex' 12 | 13 | class Mailgun { 14 | constructor (options) { 15 | if (!options.apiKey) { 16 | throw new Error('apiKey value must be defined!') 17 | } 18 | this.username = 'api' 19 | this.apiKey = options.apiKey 20 | this.publicApiKey = options.publicApiKey 21 | this.domain = options.domain 22 | this.auth = [this.username, this.apiKey].join(':') 23 | this.mute = options.mute || false 24 | this.timeout = options.timeout 25 | 26 | this.host = options.host || 'api.mailgun.net' 27 | this.endpoint = options.endpoint || '/v3' 28 | this.protocol = options.protocol || 'https:' 29 | this.port = options.port || 443 30 | this.retry = options.retry || 1 31 | 32 | this.testMode = options.testMode 33 | this.testModeLogger = options.testModeLogger 34 | 35 | if (options.proxy) { 36 | this.proxy = options.proxy 37 | } 38 | 39 | this.options = { 40 | host: this.host, 41 | endpoint: this.endpoint, 42 | protocol: this.protocol, 43 | port: this.port, 44 | auth: this.auth, 45 | proxy: this.proxy, 46 | timeout: this.timeout, 47 | retry: this.retry, 48 | testMode: this.testMode, 49 | testModeLogger: this.testModeLogger 50 | } 51 | 52 | this.mailgunTokens = {} 53 | } 54 | 55 | getDomain (method, resource) { 56 | let d = this.domain 57 | 58 | // filter out API calls that do not require a domain specified 59 | if ((resource.indexOf('/routes') >= 0) || 60 | (resource.indexOf('/lists') >= 0) || 61 | (resource.indexOf('/address') >= 0) || 62 | (resource.indexOf('/domains') >= 0)) { 63 | d = '' 64 | } else if ((resource.indexOf('/messages') >= 0) && 65 | (method === 'GET' || method === 'DELETE')) { 66 | d = `domains/${this.domain}` 67 | } 68 | 69 | return d 70 | } 71 | 72 | getRequestOptions (resource) { 73 | let o = this.options 74 | 75 | // use public API key if we have it for the routes that require it 76 | if ((resource.indexOf('/address/validate') >= 0 || 77 | (resource.indexOf('/address/parse') >= 0)) && 78 | this.publicApiKey) { 79 | const copy = Object.assign({}, this.options) 80 | 81 | copy.auth = [this.username, this.publicApiKey].join(':') 82 | o = copy 83 | } 84 | 85 | return o 86 | } 87 | 88 | request (method, resource, data, fn) { 89 | let fullpath = resource 90 | const domain = this.getDomain(method, resource) 91 | 92 | if (domain) { 93 | fullpath = '/'.concat(domain, resource) 94 | } 95 | 96 | const req = new Request(this.options) 97 | 98 | return req.request(method, fullpath, data, fn) 99 | } 100 | 101 | post (path, data, fn) { 102 | const req = new Request(this.options) 103 | 104 | return req.request('POST', path, data, fn) 105 | } 106 | 107 | get (path, data, fn) { 108 | const req = new Request(this.options) 109 | 110 | return req.request('GET', path, data, fn) 111 | } 112 | 113 | delete (path, data, fn) { 114 | const req = new Request(this.options) 115 | 116 | return req.request('DELETE', path, data, fn) 117 | } 118 | 119 | put (path, data, fn) { 120 | const req = new Request(this.options) 121 | 122 | return req.request('PUT', path, data, fn) 123 | } 124 | 125 | validateWebhook (timestamp, token, signature) { 126 | const adjustedTimestamp = parseInt(timestamp, 10) * 1000 127 | const fresh = (Math.abs(Date.now() - adjustedTimestamp) < mailgunExpirey) 128 | 129 | if (!fresh) { 130 | if (!this.mute) { 131 | console.error('[mailgun] Stale Timestamp: this may be an attack') 132 | console.error('[mailgun] However, this is most likely your fault\n') 133 | console.error('[mailgun] run `ntpdate ntp.ubuntu.com` and check your system clock\n') 134 | console.error(`[mailgun] System Time: ${new Date().toString()}`) 135 | console.error(`[mailgun] Mailgun Time: ${new Date(adjustedTimestamp).toString()}`, timestamp) 136 | console.error(`[mailgun] Delta: ${Date.now() - adjustedTimestamp}`) 137 | } 138 | 139 | return false 140 | } 141 | 142 | if (this.mailgunTokens[token]) { 143 | if (!this.mute) { 144 | console.error('[mailgun] Replay Attack') 145 | } 146 | 147 | return false 148 | } 149 | 150 | this.mailgunTokens[token] = true 151 | 152 | const tokenTimeout = setTimeout(() => { 153 | delete this.mailgunTokens[token] 154 | }, mailgunExpirey + (5 * 1000)) 155 | 156 | tokenTimeout.unref() 157 | 158 | return tsscmp( 159 | signature, crypto.createHmac(mailgunHashType, this.apiKey) 160 | .update(Buffer.from(timestamp + token, 'utf-8')) 161 | .digest(mailgunSignatureEncoding) 162 | ) 163 | } 164 | 165 | validate (address, isPrivate, opts, fn) { 166 | if (typeof opts === 'function') { 167 | fn = opts 168 | opts = {} 169 | } 170 | 171 | if (typeof isPrivate === 'object') { 172 | opts = isPrivate 173 | isPrivate = false 174 | } 175 | 176 | if (typeof isPrivate === 'function') { 177 | fn = isPrivate 178 | isPrivate = false 179 | opts = {} 180 | } 181 | 182 | let resource = '/address/validate' 183 | 184 | if (isPrivate) { 185 | resource = '/address/private/validate' 186 | } 187 | 188 | const options = this.getRequestOptions(resource) 189 | 190 | const req = new Request(options) 191 | const data = Object.assign({}, { 192 | address 193 | }, opts) 194 | 195 | return req.request('GET', resource, data, fn) 196 | } 197 | 198 | parse (addresses, isPrivate, opts, fn) { 199 | if (typeof opts === 'function') { 200 | fn = opts 201 | opts = {} 202 | } 203 | 204 | if (typeof isPrivate === 'object') { 205 | opts = isPrivate 206 | isPrivate = false 207 | } 208 | 209 | if (typeof isPrivate === 'function') { 210 | fn = isPrivate 211 | isPrivate = false 212 | opts = {} 213 | } 214 | 215 | let resource = '/address/parse' 216 | 217 | if (isPrivate) { 218 | resource = '/address/private/parse' 219 | } 220 | 221 | const options = this.getRequestOptions(resource) 222 | 223 | const req = new Request(options) 224 | const data = Object.assign({}, { 225 | addresses 226 | }, opts) 227 | 228 | return req.request('GET', resource, data, fn) 229 | } 230 | } 231 | 232 | builder.build(Mailgun, resources) 233 | 234 | Mailgun.prototype.Attachment = Attachment 235 | 236 | Mailgun.prototype.Mailgun = Mailgun 237 | 238 | function create (options) { 239 | return new Mailgun(options) 240 | } 241 | 242 | module.exports = create 243 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | const https = require('https') 2 | const http = require('http') 3 | const ProxyAgent = require('proxy-agent') 4 | const qs = require('querystring') 5 | const fs = require('fs') 6 | const Readable = require('stream').Readable 7 | const FormData = require('form-data') 8 | const Attachment = require('./attachment') 9 | const retry = require('async').retry 10 | const promisifyCall = require('promisify-call') 11 | 12 | const debug = require('debug')('mailgun-js') 13 | 14 | function isOk (i) { 15 | return typeof i !== 'undefined' && i !== null 16 | } 17 | 18 | function getDataValue (key, input) { 19 | if (isSpecialParam(key) && (typeof input === 'object')) { 20 | return JSON.stringify(input) 21 | } else if (typeof input === 'number' || typeof input === 'boolean') { 22 | return input.toString() 23 | } 24 | 25 | return input 26 | } 27 | 28 | function isSpecialParam (paramKey) { 29 | const key = paramKey.toLowerCase() 30 | 31 | return ((key === 'vars' || key === 'members' || key === 'recipient-variables') || (key.indexOf('v:') === 0)) 32 | } 33 | 34 | function isMultiUnsubsribe (path, data) { 35 | return path.indexOf('/unsubscribes') && data && Array.isArray(data) 36 | } 37 | 38 | function prepareData (data) { 39 | const params = {} 40 | 41 | for (const key in data) { 42 | if (key !== 'attachment' && key !== 'inline' && isOk(data[key])) { 43 | const value = getDataValue(key, data[key]) 44 | 45 | if (isOk(value)) { 46 | params[key] = value 47 | } 48 | } else { 49 | params[key] = data[key] 50 | } 51 | } 52 | 53 | return params 54 | } 55 | 56 | class Request { 57 | constructor (options) { 58 | this.host = options.host 59 | this.protocol = options.protocol 60 | this.port = options.port 61 | this.endpoint = options.endpoint 62 | this.auth = options.auth 63 | this.proxy = options.proxy 64 | this.timeout = options.timeout 65 | this.retry = options.retry || 1 66 | this.testMode = options.testMode 67 | this.testModeLogger = typeof options.testModeLogger === 'function' 68 | ? options.testModeLogger : this.defaultTestModeLogger 69 | } 70 | 71 | _request (method, resource, data, fn) { 72 | let path = ''.concat(this.endpoint, resource) 73 | 74 | const params = prepareData(data) 75 | 76 | this.payload = '' 77 | 78 | const isMIME = path.indexOf('/messages.mime') >= 0 79 | 80 | this.headers = {} 81 | if (method === 'GET' || method === 'DELETE') { 82 | this.payload = qs.stringify(params) 83 | if (this.payload) path = path.concat('?', this.payload) 84 | } else { 85 | if (isMIME) { 86 | this.headers['Content-Type'] = 'multipart/form-data' 87 | } else if (method === 'POST' && isMultiUnsubsribe(path, data)) { 88 | this.headers['Content-Type'] = 'application/json' 89 | } else { 90 | this.headers['Content-Type'] = 'application/x-www-form-urlencoded' 91 | } 92 | 93 | if (params && (params.attachment || params.inline || (isMIME && params.message))) { 94 | this.prepareFormData(params) 95 | } else { 96 | if (method === 'POST' && isMultiUnsubsribe(path, data)) { 97 | this.payload = JSON.stringify(data) 98 | } else { 99 | this.payload = qs.stringify(params) 100 | } 101 | 102 | if (this.payload) { 103 | this.headers['Content-Length'] = Buffer.byteLength(this.payload) 104 | } else { 105 | this.headers['Content-Length'] = 0 106 | } 107 | } 108 | } 109 | 110 | // check for MIME is true in case of messages GET 111 | if (method === 'GET' && 112 | path.indexOf('/messages') >= 0 && 113 | params && params.MIME === true) { 114 | this.headers.Accept = 'message/rfc2822' 115 | } 116 | 117 | debug('%s %s', method, path) 118 | 119 | const opts = { 120 | 'hostname': this.host, 121 | 'port': this.port, 122 | 'protocol': this.protocol, 123 | path, 124 | method, 125 | 'headers': this.headers, 126 | 'auth': this.auth, 127 | 'agent': false, 128 | 'timeout': this.timeout 129 | } 130 | 131 | if (this.proxy) { 132 | opts.agent = new ProxyAgent(this.proxy) 133 | } 134 | 135 | if (this.testMode) { 136 | this.testModeLogger(opts, this.payload, this.form) 137 | return fn() 138 | } 139 | 140 | if (typeof this.retry === 'object' || this.retry > 1) { 141 | retry(this.retry, (retryCb) => { 142 | this.callback = retryCb 143 | this.performRequest(opts) 144 | }, fn) 145 | } else { 146 | this.callback = fn 147 | this.performRequest(opts) 148 | } 149 | } 150 | 151 | request (method, resource, data, fn) { 152 | if (typeof data === 'function' && !fn) { 153 | fn = data 154 | data = {} 155 | } 156 | 157 | if (!data) { 158 | data = {} 159 | } 160 | 161 | return promisifyCall(this, this._request, method, resource, data, fn) 162 | } 163 | 164 | prepareFormData (data) { 165 | this.form = new FormData() 166 | 167 | for (const key in data) { 168 | if ({}.hasOwnProperty.call(data, key)) { 169 | const obj = data[key] 170 | 171 | if (isOk(obj)) { 172 | if (key === 'attachment' || key === 'inline') { 173 | if (Array.isArray(obj)) { 174 | for (let i = 0; i < obj.length; i++) { 175 | this.handleAttachmentObject(key, obj[i]) 176 | } 177 | } else { 178 | this.handleAttachmentObject(key, obj) 179 | } 180 | } else if (key === 'message') { 181 | this.handleMimeObject(key, obj) 182 | } else if (Array.isArray(obj)) { 183 | obj.forEach((element) => { 184 | if (isOk(element)) { 185 | const value = getDataValue(key, element) 186 | 187 | if (isOk(value)) { 188 | this.form.append(key, value) 189 | } 190 | } 191 | }) 192 | } else { 193 | const value = getDataValue(key, obj) 194 | 195 | if (isOk(value)) { 196 | this.form.append(key, value) 197 | } 198 | } 199 | } 200 | } 201 | } 202 | 203 | this.headers = this.form.getHeaders() 204 | } 205 | 206 | handleMimeObject (key, obj) { 207 | if (typeof obj === 'string') { 208 | if (fs.existsSync(obj) && fs.statSync(obj).isFile()) { 209 | this.form.append('message', fs.createReadStream(obj)) 210 | } else { 211 | this.form.append('message', Buffer.from(obj), { 212 | 'filename': 'message.mime', 213 | 'contentType': 'message/rfc822', 214 | 'knownLength': obj.length 215 | }) 216 | } 217 | } else if (obj instanceof Readable) { 218 | this.form.append('message', obj) 219 | } 220 | } 221 | 222 | handleAttachmentObject (key, obj) { 223 | if (!this.form) this.form = new FormData() 224 | 225 | if (Buffer.isBuffer(obj)) { 226 | debug('appending buffer to form data. key: %s', key) 227 | this.form.append(key, obj, { 228 | 'filename': 'file' 229 | }) 230 | } else if (typeof obj === 'string') { 231 | debug('appending stream to form data. key: %s obj: %s', key, obj) 232 | this.form.append(key, fs.createReadStream(obj)) 233 | } else if ((typeof obj === 'object') && (obj.readable === true)) { 234 | debug('appending readable stream to form data. key: %s obj: %s', key, obj) 235 | this.form.append(key, obj) 236 | } else if ((typeof obj === 'object') && (obj instanceof Attachment)) { 237 | const attachmentType = obj.getType() 238 | 239 | if (attachmentType === 'path') { 240 | debug('appending attachment stream to form data. key: %s data: %s filename: %s', key, obj.data, obj.filename) 241 | this.form.append(key, fs.createReadStream(obj.data), { 242 | 'filename': obj.filename || 'attached file' 243 | }) 244 | } else if (attachmentType === 'buffer') { 245 | debug('appending attachment buffer to form data. key: %s filename: %s', key, obj.filename) 246 | const formOpts = { 247 | 'filename': obj.filename || 'attached file' 248 | } 249 | 250 | if (obj.contentType) { 251 | formOpts.contentType = obj.contentType 252 | } 253 | 254 | if (obj.knownLength) { 255 | formOpts.knownLength = obj.knownLength 256 | } 257 | 258 | this.form.append(key, obj.data, formOpts) 259 | } else if (attachmentType === 'stream') { 260 | if ((obj.knownLength && !obj.contentType) || (!obj.knownLength && obj.contentType)) { 261 | debug('missing content type or length for attachment stream. key: %s', key) 262 | } else { 263 | debug('appending attachment stream to form data. key: %s', key) 264 | 265 | // add all known options 266 | let formOpts = {} 267 | if (obj.filename) { 268 | formOpts.filename = obj.filename 269 | } 270 | if (obj.contentType) { 271 | formOpts.contentType = obj.contentType 272 | } 273 | if (obj.knownLength) { 274 | formOpts.knownLength = obj.knownLength 275 | } 276 | 277 | this.form.append(key, obj.data, formOpts) 278 | } 279 | } 280 | } else { 281 | debug('unknown attachment type. key: %s', key) 282 | } 283 | } 284 | 285 | handleResponse (res) { 286 | let chunks = '' 287 | let error 288 | 289 | res.on('data', (chunk) => { 290 | chunks += chunk 291 | }) 292 | 293 | res.on('error', (err) => { 294 | error = err 295 | }) 296 | 297 | res.on('end', () => { 298 | let body 299 | 300 | debug('response status code: %s content type: %s error: %s', res.statusCode, res.headers['content-type'], error) 301 | 302 | // FIXME: An ugly hack to overcome invalid response type in mailgun api (see http://bit.ly/1eF30fU). 303 | // We skip content-type validation for 'campaings' endpoint assuming it is JSON. 304 | const skipContentTypeCheck = res.req && res.req.path && res.req.path.match(/\/campaigns/) 305 | const isJSON = res.headers['content-type'] && res.headers['content-type'].indexOf('application/json') >= 0 306 | 307 | if (chunks && !error && (skipContentTypeCheck || isJSON)) { 308 | try { 309 | body = JSON.parse(chunks) 310 | } catch (e) { 311 | error = e 312 | } 313 | } 314 | 315 | if (process.env.DEBUG_MAILGUN_FORCE_RETRY) { 316 | error = new Error('Force retry error') 317 | delete process.env.DEBUG_MAILGUN_FORCE_RETRY 318 | } 319 | 320 | if (!error && res.statusCode !== 200) { 321 | let msg = body || chunks || res.statusMessage 322 | 323 | if (body) { 324 | msg = body.message || body.response 325 | } 326 | 327 | error = new Error(msg) 328 | error.statusCode = res.statusCode 329 | } 330 | 331 | return this.callback(error, body) 332 | }) 333 | } 334 | 335 | performRequest (options) { 336 | const method = options.method 337 | 338 | if (this.form && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { 339 | let alreadyHandled = false 340 | this.form.submit(options, (err, res) => { 341 | if (alreadyHandled) { 342 | return 343 | } 344 | alreadyHandled = true 345 | 346 | if (err) { 347 | return this.callback(err) 348 | } 349 | 350 | return this.handleResponse(res) 351 | }) 352 | } else { 353 | let req 354 | 355 | if (options.protocol === 'http:') { 356 | req = http.request(options, (res) => { 357 | return this.handleResponse(res) 358 | }) 359 | } else { 360 | req = https.request(options, (res) => { 361 | return this.handleResponse(res) 362 | }) 363 | } 364 | 365 | if (options.timeout) { 366 | req.setTimeout(options.timeout, () => { 367 | // timeout occurs 368 | req.abort() 369 | }) 370 | } 371 | 372 | req.on('error', (e) => { 373 | return this.callback(e) 374 | }) 375 | 376 | if (this.payload && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { 377 | req.write(this.payload) 378 | } 379 | 380 | req.end() 381 | } 382 | } 383 | 384 | defaultTestModeLogger (httpOptions, payload, formData) { 385 | const testlog = debug.enabled ? debug : console.log 386 | testlog('options: %o', httpOptions) 387 | testlog('payload: %o', payload) 388 | testlog('form: %o', formData) 389 | } 390 | } 391 | 392 | module.exports = Request 393 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'definitions': { 3 | 'message': { 4 | 'description': 'This API allows you to send, access, and delete mesages programmatically.', 5 | 'links': [{ 6 | 'description': 'Returns a single message in JSON format. To get full MIME message set MIME to true', 7 | 'href': '/messages/{message}', 8 | 'method': 'GET', 9 | 'title': 'info', 10 | 'properties': { 11 | 'MIME': { 12 | 'type': 'boolean' 13 | } 14 | } 15 | }, 16 | { 17 | 'description': 'Sends a message by assembling it from the components.', 18 | 'href': '/messages', 19 | 'method': 'POST', 20 | 'title': 'send', 21 | 'properties': { 22 | 'from': { 23 | 'type': 'string' 24 | } 25 | }, 26 | 'required': ['from'] 27 | }, 28 | { 29 | 'description': 'Sends a message in MIME format.', 30 | 'href': '/messages.mime', 31 | 'method': 'POST', 32 | 'title': 'send-mime', 33 | 'properties': { 34 | 'message': { 35 | 'type': ['string', 'object'] 36 | } 37 | } 38 | }, 39 | { 40 | 'description': 'To delete an inbound message that has been stored via the store() action.', 41 | 'href': '/messages/{message}', 42 | 'method': 'DELETE', 43 | 'title': 'delete' 44 | } 45 | ] 46 | }, 47 | 'domain': { 48 | 'description': 'This API allows you to create, access, and validate domains programmatically.', 49 | 'links': [{ 50 | 'description': 'Returns a list of domains under your account in JSON.', 51 | 'href': '/domains', 52 | 'method': 'GET', 53 | 'title': 'list', 54 | 'properties': { 55 | 'limit': { 56 | 'type': 'number' 57 | }, 58 | 'skip': { 59 | 'type': 'number' 60 | } 61 | } 62 | }, 63 | { 64 | 'description': 'Returns a single domain, including credentials and DNS records.', 65 | 'href': '/domains/{domain}', 66 | 'method': 'GET', 67 | 'title': 'info' 68 | }, 69 | { 70 | 'description': 'Create a new domain.', 71 | 'href': '/domains', 72 | 'method': 'POST', 73 | 'title': 'create', 74 | 'properties': { 75 | 'name': { 76 | 'type': 'string' 77 | }, 78 | 'wildcard': { 79 | 'type': 'boolean' 80 | } 81 | }, 82 | 'required': ['name'] 83 | }, 84 | { 85 | 'description': 'Delete a domain from your account.', 86 | 'href': '/domains/{domain}', 87 | 'method': 'DELETE', 88 | 'title': 'delete' 89 | }, 90 | { 91 | 'description': 'Verifies and returns a single domain, including credentials and DNS records.', 92 | 'href': '/domains/{domain}/verify', 93 | 'method': 'PUT', 94 | 'title': 'verify' 95 | } 96 | ] 97 | }, 98 | 'credentials': { 99 | 'description': 'Programmatically get and modify domain credentials.', 100 | 'links': [{ 101 | 'description': 'Returns a list of SMTP credentials for the defined domain.', 102 | 'href': '/domains/{domain}/credentials', 103 | 'method': 'GET', 104 | 'title': 'list', 105 | 'properties': { 106 | 'limit': { 107 | 'type': 'number' 108 | }, 109 | 'skip': { 110 | 'type': 'number' 111 | } 112 | } 113 | }, 114 | { 115 | 'description': 'Creates a new set of SMTP credentials for the defined domain.', 116 | 'href': '/domains/{domain}/credentials', 117 | 'method': 'POST', 118 | 'title': 'create', 119 | 'properties': { 120 | 'login': { 121 | 'type': 'string' 122 | }, 123 | 'password': { 124 | 'type': 'string' 125 | } 126 | }, 127 | 'required': ['login', 'password'] 128 | }, 129 | { 130 | 'description': 'Updates the specified SMTP credentials. Currently only the password can be changed.', 131 | 'href': '/domains/{domain}/credentials/{login}', 132 | 'method': 'PUT', 133 | 'title': 'update', 134 | 'properties': { 135 | 'password': { 136 | 'type': 'string' 137 | } 138 | }, 139 | 'required': ['password'] 140 | }, 141 | { 142 | 'description': 'Deletes the defined SMTP credentials.', 143 | 'href': '/domains/{domain}/credentials/{login}', 144 | 'method': 'DELETE', 145 | 'title': 'delete' 146 | } 147 | ] 148 | }, 149 | 'complaints': { 150 | 'description': 'This API allows you to programmatically download the list of users who have complained, add a complaint, or delete a complaint.', 151 | 'links': [{ 152 | 'description': 'Fetches the list of complaints.', 153 | 'href': '/complaints', 154 | 'method': 'GET', 155 | 'title': 'list', 156 | 'properties': { 157 | 'limit': { 158 | 'type': 'number' 159 | }, 160 | 'skip': { 161 | 'type': 'number' 162 | } 163 | } 164 | }, 165 | { 166 | 'description': 'Adds an address to the complaints table.', 167 | 'href': '/complaints', 168 | 'method': 'POST', 169 | 'title': 'create', 170 | 'properties': { 171 | 'address': { 172 | 'type': 'string' 173 | } 174 | }, 175 | 'required': ['address'] 176 | }, 177 | { 178 | 'description': 'Fetches a single spam complaint by a given email address.', 179 | 'href': '/complaints/{address}', 180 | 'method': 'GET', 181 | 'title': 'info' 182 | }, 183 | { 184 | 'description': 'Removes a given spam complaint.', 185 | 'href': '/complaints/{address}', 186 | 'method': 'DELETE', 187 | 'title': 'delete' 188 | } 189 | ] 190 | }, 191 | 'unsubscribes': { 192 | 'description': 'This API allows you to programmatically download the list of recipients who have unsubscribed from your emails. You can also programmatically “clear” the unsubscribe event.', 193 | 'links': [{ 194 | 'description': 'Fetches the list of unsubscribes.', 195 | 'href': '/unsubscribes', 196 | 'method': 'GET', 197 | 'title': 'list', 198 | 'properties': { 199 | 'limit': { 200 | 'type': 'number' 201 | }, 202 | 'skip': { 203 | 'type': 'number' 204 | } 205 | } 206 | }, 207 | { 208 | 'description': 'Retreives a single unsubscribe record.', 209 | 'href': '/unsubscribes/{address}', 210 | 'method': 'GET', 211 | 'title': 'info' 212 | }, 213 | { 214 | 'description': 'Removes an address from the unsubscribes table.', 215 | 'href': '/unsubscribes/{address}', 216 | 'method': 'DELETE', 217 | 'title': 'delete' 218 | }, 219 | { 220 | 'description': 'Adds address to unsubscribed table.', 221 | 'href': '/unsubscribes', 222 | 'method': 'POST', 223 | 'title': 'create' 224 | } 225 | ] 226 | }, 227 | 'bounces': { 228 | 'description': 'Mailgun automatically handles bounced emails. The list of bounced addresses can be accessed programmatically.', 229 | 'links': [{ 230 | 'description': 'Fetches the list of bounces.', 231 | 'href': '/bounces', 232 | 'method': 'GET', 233 | 'title': 'list', 234 | 'properties': { 235 | 'limit': { 236 | 'type': 'number' 237 | }, 238 | 'skip': { 239 | 'type': 'number' 240 | } 241 | } 242 | }, 243 | { 244 | 'description': 'Fetches a single bounce event by a given email address.', 245 | 'href': '/bounces/{address}', 246 | 'method': 'GET', 247 | 'title': 'info' 248 | }, 249 | { 250 | 'description': 'Clears a given bounce event.', 251 | 'href': '/bounces/{address}', 252 | 'method': 'DELETE', 253 | 'title': 'delete' 254 | }, 255 | { 256 | 'description': 'Adds a permanent bounce to the bounces table. Updates the existing record if already here.', 257 | 'href': '/bounces', 258 | 'method': 'POST', 259 | 'title': 'create', 260 | 'properties': { 261 | 'address': { 262 | 'type': 'string' 263 | }, 264 | 'code': { 265 | 'type': 'number' 266 | }, 267 | 'error': { 268 | 'type': 'string' 269 | } 270 | }, 271 | 'required': ['address'] 272 | } 273 | ] 274 | }, 275 | 'routes': { 276 | 'description': 'Mailgun Routes are a powerful way to handle the incoming traffic. This API allows you to work with routes programmatically.', 277 | 'links': [{ 278 | 'description': 'Fetches the list of routes.', 279 | 'href': '/routes', 280 | 'method': 'GET', 281 | 'title': 'list', 282 | 'properties': { 283 | 'limit': { 284 | 'type': 'number' 285 | }, 286 | 'skip': { 287 | 'type': 'number' 288 | } 289 | } 290 | }, 291 | { 292 | 'description': 'Returns a single route object based on its ID.', 293 | 'href': '/routes/{id}', 294 | 'method': 'GET', 295 | 'title': 'info' 296 | }, 297 | { 298 | 'description': 'Creates a new route.', 299 | 'href': '/routes', 300 | 'method': 'POST', 301 | 'title': 'create', 302 | 'properties': { 303 | 'limit': { 304 | 'priority': 'number' 305 | }, 306 | 'description': { 307 | 'type': 'string' 308 | }, 309 | 'expression': { 310 | 'type': 'string' 311 | } 312 | }, 313 | 'required': ['expression'] 314 | }, 315 | { 316 | 'description': 'Updates a given route by ID.', 317 | 'href': '/routes/{id}', 318 | 'method': 'PUT', 319 | 'title': 'update', 320 | 'properties': { 321 | 'limit': { 322 | 'priority': 'number' 323 | }, 324 | 'description': { 325 | 'type': 'string' 326 | }, 327 | 'expression': { 328 | 'type': 'string' 329 | } 330 | } 331 | }, 332 | { 333 | 'description': 'Deletes a route based on the id.', 334 | 'href': '/routes/{id}', 335 | 'method': 'DELETE', 336 | 'title': 'delete' 337 | } 338 | ] 339 | }, 340 | 'list': { 341 | 'description': 'You can programmatically work with mailing lists and mailing list members using Mailgun Mailing List API.', 342 | 'links': [{ 343 | 'description': 'Returns a list of mailing lists under your account.', 344 | 'href': '/lists', 345 | 'method': 'GET', 346 | 'title': 'list', 347 | 'properties': { 348 | 'address': { 349 | 'type': 'string' 350 | }, 351 | 'limit': { 352 | 'type': 'number' 353 | }, 354 | 'skip': { 355 | 'type': 'number' 356 | } 357 | } 358 | }, 359 | { 360 | 'description': 'Returns a single mailing list by a given address.', 361 | 'href': '/lists/{address}', 362 | 'method': 'GET', 363 | 'title': 'info' 364 | }, 365 | { 366 | 'description': 'Creates a new mailing list.', 367 | 'href': '/lists', 368 | 'method': 'POST', 369 | 'title': 'create', 370 | 'properties': { 371 | 'address': { 372 | 'type': 'string' 373 | }, 374 | 'name': { 375 | 'type': 'string' 376 | }, 377 | 'description': { 378 | 'type': 'string' 379 | }, 380 | 'access_level': { 381 | 'type': 'string' 382 | } 383 | }, 384 | 'required': ['address'] 385 | }, 386 | { 387 | 'description': 'Update mailing list properties, such as address, description or name.', 388 | 'href': '/lists/{address}', 389 | 'method': 'PUT', 390 | 'title': 'update', 391 | 'properties': { 392 | 'address': { 393 | 'type': 'string' 394 | }, 395 | 'name': { 396 | 'type': 'string' 397 | }, 398 | 'description': { 399 | 'type': 'string' 400 | }, 401 | 'access_level': { 402 | 'type': 'string' 403 | } 404 | } 405 | }, 406 | { 407 | 'description': 'Deletes a mailing list.', 408 | 'href': '/lists/{address}', 409 | 'method': 'DELETE', 410 | 'title': 'delete' 411 | } 412 | ] 413 | }, 414 | 'members': { 415 | 'description': 'Programatically work with mailing lists members.', 416 | 'links': [{ 417 | 'description': 'Fetches the list of mailing list members.', 418 | 'href': '/lists/{address}/members', 419 | 'method': 'GET', 420 | 'title': 'list', 421 | 'properties': { 422 | 'subscribed': { 423 | 'type': 'boolean' 424 | }, 425 | 'limit': { 426 | 'type': 'number' 427 | }, 428 | 'skip': { 429 | 'type': 'number' 430 | } 431 | } 432 | }, 433 | { 434 | 'description': 'Paginate over list members in the given mailing list', 435 | 'href': '/lists/{address}/members/pages', 436 | 'method': 'GET', 437 | 'title': 'page', 438 | 'properties': { 439 | 'subscribed': { 440 | 'type': 'boolean' 441 | }, 442 | 'limit': { 443 | 'type': 'number' 444 | }, 445 | 'page': { 446 | 'type': 'string' 447 | }, 448 | 'address': { 449 | 'type': 'string' 450 | } 451 | } 452 | }, 453 | { 454 | 'description': 'Retrieves a mailing list member.', 455 | 'href': '/lists/{address}/members/{member_address}', 456 | 'method': 'GET', 457 | 'title': 'info' 458 | }, 459 | { 460 | 'description': 'Adds a member to the mailing list.', 461 | 'href': '/lists/{address}/members', 462 | 'method': 'POST', 463 | 'title': 'create', 464 | 'properties': { 465 | 'address': { 466 | 'type': 'string' 467 | }, 468 | 'name': { 469 | 'type': 'string' 470 | }, 471 | 'vars': { 472 | 'type': 'object' 473 | }, 474 | 'subscribed': { 475 | 'type': 'boolean' 476 | }, 477 | 'upsert': { 478 | 'type': 'string' 479 | } 480 | }, 481 | 'required': ['address'] 482 | }, 483 | { 484 | 'description': 'Adds multiple members, up to 1,000 per call, to a Mailing List.', 485 | 'href': '/lists/{address}/members.json', 486 | 'method': 'POST', 487 | 'title': 'add', 488 | 'properties': { 489 | 'members': { 490 | 'type': 'array' 491 | }, 492 | 'upsert': { 493 | 'type': 'boolean' 494 | } 495 | }, 496 | 'required': ['members'] 497 | }, 498 | { 499 | 'description': 'Updates a mailing list member with given properties.', 500 | 'href': '/lists/{address}/members/{member_address}', 501 | 'method': 'PUT', 502 | 'title': 'update', 503 | 'properties': { 504 | 'address': { 505 | 'type': 'string' 506 | }, 507 | 'name': { 508 | 'type': 'string' 509 | }, 510 | 'vars': { 511 | 'type': 'object' 512 | }, 513 | 'subscribed': { 514 | 'type': 'boolean' 515 | } 516 | } 517 | }, 518 | { 519 | 'description': 'Delete a mailing list member.', 520 | 'href': '/lists/{address}/members/{member_address}', 521 | 'method': 'DELETE', 522 | 'title': 'delete' 523 | } 524 | ] 525 | }, 526 | 'stats': { 527 | 'description': 'Various data and event statistics for you mailgun account. See http://documentation.mailgun.com/api-stats.html', 528 | 'links': [{ 529 | 'description': 'Returns a list of event stat items. Each record represents counts for one event per one day.', 530 | 'href': '/stats', 531 | 'method': 'GET', 532 | 'title': 'list', 533 | 'properties': { 534 | 'limit': { 535 | 'type': 'number' 536 | }, 537 | 'skip': { 538 | 'type': 'number' 539 | }, 540 | 'start-date': { 541 | 'type': 'string' 542 | } 543 | } 544 | }] 545 | }, 546 | 'tags': { 547 | 'description': 'Deletes all counters for particular tag and the tag itself. See http://documentation.mailgun.com/api-stats.html', 548 | 'links': [{ 549 | 'description': 'List all tags.', 550 | 'href': '/tags', 551 | 'method': 'GET', 552 | 'title': 'list' 553 | }, 554 | { 555 | 'description': 'Gets a specific tag.', 556 | 'href': '/tags/{tag}', 557 | 'method': 'GET', 558 | 'title': 'info' 559 | }, 560 | { 561 | 'description': 'Returns statistics for a given tag.', 562 | 'href': '/tags/{tag}/stats', 563 | 'method': 'GET', 564 | 'title': 'info', 565 | 'properties': { 566 | 'event': { 567 | 'type': 'array' 568 | }, 569 | 'start': { 570 | 'type': 'string' 571 | }, 572 | 'end': { 573 | 'type': 'string' 574 | }, 575 | 'resolution': { 576 | 'type': 'string' 577 | }, 578 | 'duration': { 579 | 'type': 'string' 580 | } 581 | }, 582 | 'required': ['event'] 583 | }, 584 | { 585 | 'description': 'Returns a list of countries of origin for a given domain for different event types.', 586 | 'href': '/tags/{tag}/stats/aggregates/countries', 587 | 'method': 'GET', 588 | 'title': 'list' 589 | }, 590 | { 591 | 'description': 'Returns a list of email providers for a given domain for different event types.', 592 | 'href': '/tags/{tag}/stats/aggregates/providers', 593 | 'method': 'GET', 594 | 'title': 'list' 595 | }, 596 | { 597 | 'description': 'Returns a list of devices for a given domain that have triggered event types.', 598 | 'href': '/tags/{tag}/stats/aggregates/devices', 599 | 'method': 'GET', 600 | 'title': 'list' 601 | }, 602 | { 603 | 'description': 'Deletes all counters for particular tag and the tag itself.', 604 | 'href': '/tags/{tag}', 605 | 'method': 'DELETE', 606 | 'title': 'delete' 607 | } 608 | ] 609 | }, 610 | 'events': { 611 | 'description': 'Query events that happen to your emails. See http://documentation.mailgun.com/api-events.html', 612 | 'links': [{ 613 | 'description': 'Queries event records.', 614 | 'href': '/events', 615 | 'method': 'GET', 616 | 'title': 'get', 617 | 'properties': { 618 | 'begin': { 619 | 'type': 'string' 620 | }, 621 | 'end': { 622 | 'type': 'string' 623 | }, 624 | 'ascending': { 625 | 'type': 'string' 626 | }, 627 | 'limit': { 628 | 'type': 'number' 629 | }, 630 | 'pretty': { 631 | 'type': 'string' 632 | } 633 | } 634 | }] 635 | }, 636 | 'tracking': { 637 | 'description': 'Programmatically get and modify domain tracking settings.', 638 | 'links': [{ 639 | 'description': 'Returns tracking settings for a domain.', 640 | 'href': '/domains/{domain}/tracking', 641 | 'method': 'GET', 642 | 'title': 'info' 643 | }, 644 | { 645 | 'description': 'Updates the open tracking settings for a domain.', 646 | 'href': '/domains/{domain}/tracking/open', 647 | 'method': 'PUT', 648 | 'title': 'update', 649 | 'properties': { 650 | 'active': { 651 | 'type': ['string', 'boolean'] 652 | } 653 | }, 654 | 'required': ['active'] 655 | }, 656 | { 657 | 'description': 'Updates the click tracking settings for a domain.', 658 | 'href': '/domains/{domain}/tracking/click', 659 | 'method': 'PUT', 660 | 'title': 'update', 661 | 'properties': { 662 | 'active': { 663 | 'type': ['string', 'boolean'] 664 | } 665 | }, 666 | 'required': ['active'] 667 | }, 668 | { 669 | 'description': 'Updates the unsubscribe tracking settings for a domain.', 670 | 'href': '/domains/{domain}/tracking/unsubscribe', 671 | 'method': 'PUT', 672 | 'title': 'update', 673 | 'properties': { 674 | 'active': { 675 | 'type': 'boolean' 676 | }, 677 | 'html_footer': { 678 | 'type': 'string' 679 | }, 680 | 'text_footer': { 681 | 'type': 'string' 682 | } 683 | }, 684 | 'required': ['active'] 685 | } 686 | ] 687 | } 688 | } 689 | } 690 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mailgun-js", 3 | "description": "Simple Node.js helper module for Mailgun API", 4 | "keywords": [ 5 | "email", 6 | "mailgun" 7 | ], 8 | "version": "0.23.0", 9 | "homepage": "https://github.com/bojand/mailgun-js", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/bojand/mailgun-js.git" 14 | }, 15 | "bugs": { 16 | "url": "http://github.com/bojand/mailgun-js/issues" 17 | }, 18 | "engines": { 19 | "node": ">=6.0.0" 20 | }, 21 | "main": "./lib/mailgun.js", 22 | "dependencies": { 23 | "async": "^2.6.1", 24 | "debug": "^4.1.0", 25 | "form-data": "^2.3.3", 26 | "inflection": "~1.12.0", 27 | "is-stream": "^1.1.0", 28 | "path-proxy": "~1.0.0", 29 | "promisify-call": "^2.0.2", 30 | "proxy-agent": "^3.1.1", 31 | "tsscmp": "^1.0.6" 32 | }, 33 | "author": { 34 | "name": "Bojan Djurkovic " 35 | }, 36 | "devDependencies": { 37 | "clone": "^2.1.2", 38 | "mocha": "~5.2.0", 39 | "nodemailer": "^4.6.8", 40 | "request": "^2.88.0", 41 | "sinon": "^7.1.0", 42 | "standard": "^12.0.0" 43 | }, 44 | "scripts": { 45 | "test": "npm run lint && npm run mocha", 46 | "mocha": "mocha --ui exports --slow 1500ms --timeout 10000ms --reporter spec", 47 | "lint": "standard lib/**/*.js && standard --env mocha test/**/*.js", 48 | "lint:fix": "standard --fix lib/**/*.js && standard --env mocha --fix test/**/*.js", 49 | "docs": "./bin/docs" 50 | }, 51 | "directories": { 52 | "test": "test" 53 | }, 54 | "standard": { 55 | "env": { 56 | "node": true, 57 | "mocha": true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/bounces.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const fixture = require('./data/fixture.json') 3 | const assert = require('assert') 4 | 5 | const mailgun = require('../')({ 6 | 'apiKey': auth.api_key, 7 | 'domain': auth.domain 8 | }) 9 | 10 | describe('Bounces', () => { 11 | beforeEach((done) => { 12 | setTimeout(done, 500) 13 | }) 14 | 15 | it('test bounces().create() missing address', (done) => { 16 | mailgun.bounces().create({}, (err) => { 17 | assert.ok(err) 18 | assert(/Missing parameter 'address'/.test(err.message)) 19 | done() 20 | }) 21 | }) 22 | 23 | it('test bounces().create() invalid address type', (done) => { 24 | mailgun.bounces().create({ 25 | 'address': 123 26 | }, (err) => { 27 | assert.ok(err) 28 | assert(/Invalid parameter type./.test(err.message)) 29 | done() 30 | }) 31 | }) 32 | 33 | it('test bounces().create()', (done) => { 34 | mailgun.bounces().create(fixture.bounce, (err, body) => { 35 | assert.ifError(err) 36 | assert.ok(body) 37 | done() 38 | }) 39 | }) 40 | 41 | it('test bounces().list()', (done) => { 42 | mailgun.bounces().list((err, body) => { 43 | assert.ifError(err) 44 | assert.ok(body.items) 45 | done() 46 | }) 47 | }) 48 | 49 | it('test bounces().info()', (done) => { 50 | mailgun.bounces(fixture.bounce.address).info((err, body) => { 51 | assert.ifError(err) 52 | assert.ok(body) 53 | assert.ok(body.address) 54 | done() 55 | }) 56 | }) 57 | 58 | it('test bounces().delete()', (done) => { 59 | mailgun.bounces(fixture.bounce.address).delete((err, body) => { 60 | assert.ifError(err) 61 | assert.ok(body) 62 | done() 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/complaints.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const fixture = require('./data/fixture.json') 3 | const assert = require('assert') 4 | 5 | const mailgun = require('../')({ 6 | 'apiKey': auth.api_key, 7 | 'domain': auth.domain 8 | }) 9 | 10 | describe('Complaints', () => { 11 | beforeEach((done) => { 12 | setTimeout(done, 500) 13 | }) 14 | 15 | it('test complaints().create() missing address', (done) => { 16 | mailgun.complaints().create({}, (err) => { 17 | assert.ok(err) 18 | assert(/Missing parameter 'address'/.test(err.message)) 19 | done() 20 | }) 21 | }) 22 | 23 | it('test complaints().create()', (done) => { 24 | mailgun.complaints().create({ 25 | 'address': fixture.complaints.address 26 | }, (err, body) => { 27 | assert.ifError(err) 28 | assert.ok(body.message) 29 | done() 30 | }) 31 | }) 32 | 33 | it('test complaints().list()', (done) => { 34 | mailgun.complaints().list((err, body) => { 35 | assert.ifError(err) 36 | assert.ok(body.items) 37 | done() 38 | }) 39 | }) 40 | 41 | it('test complaints().info()', (done) => { 42 | mailgun.complaints(fixture.complaints.address).info((err, body) => { 43 | assert.ifError(err) 44 | assert.ok(body) 45 | done() 46 | }) 47 | }) 48 | 49 | it('test complaints().delete()', (done) => { 50 | mailgun.complaints(fixture.complaints.address).delete((err, body) => { 51 | assert.ifError(err) 52 | assert.ok(body.message) 53 | done() 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/constructor.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const assert = require('assert') 3 | 4 | const mailgun = require('../')({ 5 | 'apiKey': auth.api_key, 6 | 'domain': auth.domain 7 | }) 8 | 9 | describe('Constructor', () => { 10 | beforeEach((done) => { 11 | setTimeout(done, 500) 12 | }) 13 | 14 | it('instance constructor', () => { 15 | const mg = new mailgun.Mailgun({ 16 | 'apiKey': auth.api_key, 17 | 'domain': auth.domain 18 | }) 19 | 20 | assert.ok(mg) 21 | assert.ok(mg instanceof mailgun.Mailgun) 22 | assert.ok(mg instanceof mg.Mailgun) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/data/fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": { 3 | "to": "mailgunjs+test1@gmail.com", 4 | "from": "mailgunjstest+recv1@gmail.com", 5 | "subject": "Test email subject", 6 | "text": "Test email text" 7 | }, 8 | "message_recipient_vars": { 9 | "to": [ 10 | "mailgunjs+test1@gmail.com", 11 | "mailgunjs+test2@gmail.com" 12 | ], 13 | "from": "mailgunjstest+send@gmail.com", 14 | "subject": "Hey, %recipient.first%", 15 | "text": "Test email text. Your ID: %recipient.id%", 16 | "recipient-variables": { 17 | "mailgunjs+test1@gmail.com": { 18 | "first": "Bob", 19 | "id": 1 20 | }, 21 | "mailgunjs+test2@gmail.com": { 22 | "first": "Alice", 23 | "id": 2 24 | } 25 | } 26 | }, 27 | "route": { 28 | "description": "Sample test route", 29 | "expression": "match_recipient('.*@sandbox77047.mailgun.org')", 30 | "action": "forward('http://myhost.com/messages/')" 31 | }, 32 | "mailingList": { 33 | "address": "devtest@sandbox77047.mailgun.org", 34 | "name": "MailgunDev", 35 | "description": "Mailgun developers list" 36 | }, 37 | "new_domain": { 38 | "name": "mgjsunits1234.mailgun.org" 39 | }, 40 | "existing_domain": { 41 | "name": "sandbox77047.mailgun.org" 42 | }, 43 | "credentials": { 44 | "login": "user01@sandbox77047.mailgun.org", 45 | "password": "12345" 46 | }, 47 | "unsubscribe": { 48 | "address": "me@test.mailgun.org", 49 | "tag": "*" 50 | }, 51 | "unsubscribe2": { 52 | "address": "me2@test.mailgun.org", 53 | "tag": ["some_tag"] 54 | }, 55 | "bounce": { 56 | "address": "me@test.mailgun.org" 57 | }, 58 | "campaign": { 59 | "name": "My test campaign", 60 | "id": "my_unique_campaign_id", 61 | "newName": "my_new_campaign_name" 62 | }, 63 | "complaints": { 64 | "address": "testcomplaint@faketest.com" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/data/mailgun_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailgun/mailgun-js-boland/0190873362bf0b79d2813d711f6492e0d351047e/test/data/mailgun_logo.png -------------------------------------------------------------------------------- /test/data/message.eml: -------------------------------------------------------------------------------- 1 | To: mm@samples.mailgun.org 2 | Subject: Test email subject 3 | Content-Type: multipart/alternative; 4 | boundary="----mailcomposer-?=_1-1410115176966" 5 | MIME-Version: 1.0 6 | 7 | ------mailcomposer-?=_1-1410115176966 8 | Content-Type: text/plain; charset=utf-8 9 | Content-Transfer-Encoding: quoted-printable 10 | 11 | Test email text 12 | To: mm@samples.mailgun.org 13 | To: mm@samples.mailgun.org 14 | Subject: Test email subject 15 | Subject: Test email subject 16 | Content-Type: multipart/alternative; 17 | boundary="----mailcomposer-?=_1-1410115176966" 18 | Content-Type: multipart/alternative; 19 | boundary="----mailcomposer-?=_2-1410115176970" 20 | MIME-Version: 1.0 21 | 22 | ------mailcomposer-?=_2-1410115176970 23 | Content-Type: text/html; charset=utf-8 24 | Content-Transfer-Encoding: quoted-printable 25 | 26 | Test email text 27 | ------mailcomposer-?=_2-1410115176970-- 28 | -------------------------------------------------------------------------------- /test/domains.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const fixture = require('./data/fixture.json') 3 | const assert = require('assert') 4 | 5 | const mailgun = require('../')({ 6 | 'apiKey': auth.api_key, 7 | 'domain': auth.domain 8 | }) 9 | 10 | describe('Domains', () => { 11 | beforeEach((done) => { 12 | setTimeout(done, 500) 13 | }) 14 | 15 | it('test domains().create() invalid missing address', (done) => { 16 | mailgun.domains().create({}, (err) => { 17 | assert.ok(err) 18 | assert(/Missing parameter 'name'/.test(err.message)) 19 | done() 20 | }) 21 | }) 22 | 23 | it.skip('test domains().create() ', () => { 24 | // mailgun.domains().create(fixture.new_domain, function (err, body) { 25 | // assert.ifError(err); 26 | // assert.ok(body.message); 27 | // assert(/Domain has been created/.test(body.message)); 28 | // assert.ok(body.domain); 29 | // assert.strictEqual(fixture.new_domain.name, body.domain.name); 30 | // assert.strictEqual(fixture.new_domain.smtp_password, body.domain.smtp_password); 31 | // done(); 32 | // }); 33 | }) 34 | 35 | it('test domains().list()', (done) => { 36 | mailgun.domains().list((err, body) => { 37 | assert.ifError(err) 38 | assert.ok(body.total_count) 39 | assert.ok(body.items) 40 | done() 41 | }) 42 | }) 43 | 44 | it('test domains().info()', (done) => { 45 | mailgun.domains(fixture.existing_domain.name).info((err, body) => { 46 | assert.ifError(err) 47 | assert.ok(body.domain) 48 | assert.strictEqual(fixture.existing_domain.name, body.domain.name) 49 | done() 50 | }) 51 | }) 52 | 53 | it('test domains().credentials().list()', (done) => { 54 | mailgun.domains(fixture.existing_domain.name).credentials().list((err, body) => { 55 | assert.ifError(err) 56 | assert.ok(body.total_count) 57 | assert.ok(body.items) 58 | done() 59 | }) 60 | }) 61 | 62 | it('test domains().credentials().create() missing login', (done) => { 63 | mailgun.domains(fixture.existing_domain.name).credentials().create({}, (err) => { 64 | assert.ok(err) 65 | assert(/Missing parameter 'login'/.test(err.message)) 66 | done() 67 | }) 68 | }) 69 | 70 | it('test domains().credentials().create() missing password', (done) => { 71 | mailgun.domains(fixture.existing_domain.name).credentials().create({ 72 | 'login': fixture.credentials.login 73 | }, (err) => { 74 | assert.ok(err) 75 | assert(/Missing parameter 'password'/.test(err.message)) 76 | done() 77 | }) 78 | }) 79 | 80 | it('test domains().credentials().create() invalid login type', (done) => { 81 | mailgun.domains(fixture.existing_domain.name).credentials().create({ 82 | 'login': 123, 83 | 'password': 'password' 84 | }, (err) => { 85 | assert.ok(err) 86 | assert(/Invalid parameter type./.test(err.message)) 87 | done() 88 | }) 89 | }) 90 | 91 | it('test domains().credentials().create() invalid password type', (done) => { 92 | mailgun.domains(fixture.existing_domain.name).credentials().create({ 93 | 'login': fixture.credentials.login, 94 | 'password': 123 95 | }, (err) => { 96 | assert.ok(err) 97 | assert(/Invalid parameter type./.test(err.message)) 98 | done() 99 | }) 100 | }) 101 | 102 | it('test domains().credentials().create()', (done) => { 103 | mailgun.domains(fixture.existing_domain.name).credentials().create({ 104 | 'login': fixture.credentials.login, 105 | 'password': fixture.credentials.password 106 | }, (err, body) => { 107 | assert.ifError(err) 108 | assert.ok(body.message) 109 | done() 110 | }) 111 | }) 112 | 113 | it('test domains().credentials().update() missing password', (done) => { 114 | mailgun.domains(fixture.existing_domain.name).credentials(fixture.credentials.login).update({}, (err) => { 115 | assert.ok(err) 116 | assert(/Missing parameter 'password'/.test(err.message)) 117 | done() 118 | }) 119 | }) 120 | 121 | it('test domains().credentials().update() invalid password type', (done) => { 122 | mailgun.domains(fixture.existing_domain.name).credentials(fixture.credentials.login).update({ 123 | 'password': 123 124 | }, (err) => { 125 | assert.ok(err) 126 | assert(/Invalid parameter type./.test(err.message)) 127 | done() 128 | }) 129 | }) 130 | 131 | it('test domains().credentials().update()', (done) => { 132 | mailgun.domains(fixture.existing_domain.name).credentials(fixture.credentials.login).update({ 133 | 'password': fixture.credentials.password 134 | }, (err, body) => { 135 | assert.ifError(err) 136 | assert.ok(body.message) 137 | done() 138 | }) 139 | }) 140 | 141 | it('test domains().credentials().delete()', (done) => { 142 | mailgun.domains(fixture.existing_domain.name).credentials(fixture.credentials.login).delete((err, body) => { 143 | assert.ifError(err) 144 | assert.ok(body.message) 145 | done() 146 | }) 147 | }) 148 | 149 | it.skip('test domains().delete()', () => { 150 | // var domain = fixture.new_domain.name; 151 | // mailgun.domains(domain).delete(function (err, body) { 152 | // assert.ifError(err); 153 | // assert.ok(body.message); 154 | // assert(/Domain has been deleted/.test(body.message)); 155 | // done(); 156 | // }); 157 | }) 158 | 159 | it('test domains().verify() that it is a function', () => { 160 | // we can't actally just call this endpoint 161 | const fn = mailgun.domains(fixture.existing_domain.name).verify 162 | 163 | assert.ok(typeof fn === 'function') 164 | }) 165 | 166 | it('server error should have status code', (done) => { 167 | mailgun.domains('domaintest@test123.com').info((err) => { 168 | assert.ok(err) 169 | assert.ok(err.statusCode) 170 | assert.strictEqual(404, err.statusCode) 171 | done() 172 | }) 173 | }) 174 | 175 | it('test domains().tracking().into()', (done) => { 176 | mailgun.domains(fixture.existing_domain.name).tracking().info((err, body) => { 177 | assert.ifError(err) 178 | assert.ok(body) 179 | assert.ok(body.tracking) 180 | done() 181 | }) 182 | }) 183 | 184 | it('test domains().tracking().open().update()', (done) => { 185 | mailgun.domains(fixture.existing_domain.name).tracking().open().update({ active: 'no' }, (err, body) => { 186 | assert.ifError(err) 187 | assert.ok(body) 188 | assert.ok(body.open) 189 | done() 190 | }) 191 | }) 192 | 193 | it('test domains().tracking().click().update()', (done) => { 194 | mailgun.domains(fixture.existing_domain.name).tracking().click().update({ active: false }, (err, body) => { 195 | assert.ifError(err) 196 | assert.ok(body) 197 | assert.ok(body.click) 198 | done() 199 | }) 200 | }) 201 | 202 | it('test domains().tracking().unsubscribe().update()', (done) => { 203 | const data = { 204 | active: false, 205 | html_footer: '
text
', 206 | text_footer: 'text' 207 | } 208 | 209 | mailgun.domains(fixture.existing_domain.name).tracking().unsubscribe().update(data, (err, body) => { 210 | assert.ifError(err) 211 | assert.ok(body) 212 | assert.ok(body.unsubscribe) 213 | done() 214 | }) 215 | }) 216 | }) 217 | -------------------------------------------------------------------------------- /test/events.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const assert = require('assert') 3 | 4 | const mailgun = require('../')({ 5 | 'apiKey': auth.api_key, 6 | 'domain': auth.domain 7 | }) 8 | 9 | describe('Events', () => { 10 | beforeEach((done) => { 11 | setTimeout(done, 500) 12 | }) 13 | 14 | it('test mailgun.events().get() simple recipient query', (done) => { 15 | const query = { 16 | 'begin': 'Fri, 3 May 2013 09:00:00 -0000', 17 | 'ascending': 'yes', 18 | 'limit': 25, 19 | 'pretty': 'yes', 20 | 'recipient': 'mm@samples.mailgun.org' 21 | } 22 | 23 | mailgun.events().get(query, (err, body) => { 24 | assert.ifError(err) 25 | assert.ok(body.items) 26 | done() 27 | }) 28 | }) 29 | 30 | it('test mailgun.events().get() simple event query', (done) => { 31 | const query = { 32 | 'event': 'rejected OR failed' 33 | } 34 | 35 | mailgun.events().get(query, (err, body) => { 36 | assert.ifError(err) 37 | assert.ok(body.items) 38 | done() 39 | }) 40 | }) 41 | 42 | it('test mailgun.events().get() without any params', (done) => { 43 | mailgun.events().get((err, body) => { 44 | assert.ifError(err) 45 | assert.ok(body.items) 46 | done() 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/genericrequests.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const assert = require('assert') 3 | 4 | const mailgun = require('../')({ 5 | 'apiKey': auth.api_key, 6 | 'domain': auth.domain 7 | }) 8 | 9 | describe('Generic requests functions', () => { 10 | beforeEach((done) => { 11 | setTimeout(done, 500) 12 | }) 13 | 14 | it('test mailgun.get()', (done) => { 15 | const path = `/${auth.domain}/stats` 16 | 17 | mailgun.get(path, { 18 | 'event': ['sent', 'delivered'] 19 | }, (err, body) => { 20 | assert.ifError(err) 21 | assert.ok(body.total_count) 22 | assert.ok(body.items) 23 | done() 24 | }) 25 | }) 26 | 27 | it('test mailgun.get() using promises', (done) => { 28 | const path = `/${auth.domain}/stats` 29 | 30 | mailgun.get(path, { 31 | 'event': ['sent', 'delivered'] 32 | }).then((body) => { 33 | assert.ok(body.total_count) 34 | assert.ok(body.items) 35 | done() 36 | }).catch((err) => { 37 | assert.ifError(err) 38 | done() 39 | }) 40 | }) 41 | 42 | it('test mailgun.get() using promises 2', (done) => { 43 | const mg = new mailgun.Mailgun({ 44 | 'apiKey': auth.public_api_key, 45 | 'domain': auth.domain 46 | }) 47 | 48 | mg.get('/address/validate', { 49 | 'address': 'emailtestmg@gmail.com' 50 | }).then(body => { 51 | assert.ok(body) 52 | done() 53 | }).catch(err => { 54 | assert.ifError(err) 55 | done() 56 | }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/mailinglists.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const fixture = require('./data/fixture.json') 3 | const assert = require('assert') 4 | 5 | const mailgun = require('../')({ 6 | 'apiKey': auth.api_key, 7 | 'domain': auth.domain 8 | }) 9 | 10 | describe('Mailing lists', () => { 11 | beforeEach((done) => { 12 | setTimeout(done, 500) 13 | }) 14 | 15 | it('test lists().create() invalid missing address', (done) => { 16 | mailgun.lists().create({}, (err) => { 17 | assert.ok(err) 18 | assert(/Missing parameter 'address'/.test(err.message)) 19 | done() 20 | }) 21 | }) 22 | 23 | it('test lists.create()', (done) => { 24 | mailgun.lists().create(fixture.mailingList, (err, body) => { 25 | assert.ifError(err) 26 | assert.ok(body.message) 27 | assert.ok(body.list) 28 | assert.strictEqual(fixture.mailingList.address, body.list.address) 29 | assert.strictEqual(fixture.mailingList.name, body.list.name) 30 | assert.strictEqual(fixture.mailingList.description, body.list.description) 31 | done() 32 | }) 33 | }) 34 | 35 | it('test lists.list()', (done) => { 36 | mailgun.lists().list((err, body) => { 37 | assert.ifError(err) 38 | assert.ok(body.total_count) 39 | assert.ok(body.items) 40 | done() 41 | }) 42 | }) 43 | 44 | it('test lists().info()', (done) => { 45 | mailgun.lists(fixture.mailingList.address).info((err, body) => { 46 | assert.ifError(err) 47 | assert.ok(body.list) 48 | assert.ok(body.list.address) 49 | assert.ok(body.list.name) 50 | done() 51 | }) 52 | }) 53 | 54 | it('test lists().update()', (done) => { 55 | const name = 'List Updated' 56 | const desc = 'My updated test mailing list' 57 | 58 | mailgun.lists(fixture.mailingList.address).update({ 59 | name, 60 | 'description': desc 61 | }, (err, body) => { 62 | assert.ifError(err) 63 | assert.ok(body.message) 64 | assert.strictEqual(fixture.mailingList.address, body.list.address) 65 | assert.strictEqual(name, body.list.name) 66 | assert.strictEqual(desc, body.list.description) 67 | done() 68 | }) 69 | }) 70 | 71 | it('test lists().members().create() no member address', (done) => { 72 | mailgun.lists(fixture.mailingList.address).members().create({}, (err) => { 73 | assert.ok(err) 74 | assert(/Missing parameter 'address'/.test(err.message)) 75 | done() 76 | }) 77 | }) 78 | 79 | it('test lists().members().create() invalid address type', (done) => { 80 | mailgun.lists(fixture.mailingList.address).members().create({ 81 | 'address': 1234 82 | }, (err) => { 83 | assert.ok(err) 84 | assert(/Invalid parameter type./.test(err.message)) 85 | done() 86 | }) 87 | }) 88 | 89 | it('test lists().members().create()', (done) => { 90 | const data = { 91 | 'subscribed': true, 92 | 'address': 'bob@gmail.com', 93 | 'name': 'Bob Bar', 94 | 'vars': { 95 | 'age': 26 96 | } 97 | } 98 | 99 | mailgun.lists(fixture.mailingList.address).members().create(data, (err, body) => { 100 | assert.ifError(err) 101 | assert.ok(body.message) 102 | assert.ok(body.member) 103 | assert.ok(body.member.address) 104 | assert.ok(body.member.name) 105 | assert.ok(body.member.vars) 106 | assert.strictEqual(data.subscribed, body.member.subscribed) 107 | assert.strictEqual(data.address, body.member.address) 108 | assert.strictEqual(data.name, body.member.name) 109 | assert.strictEqual(data.vars.age, body.member.vars.age) 110 | done() 111 | }) 112 | }) 113 | 114 | it('test lists().members().list()', (done) => { 115 | mailgun.lists(fixture.mailingList.address).members().list((err, body) => { 116 | assert.ifError(err) 117 | assert.ok(body.total_count >= 0) 118 | assert.ok(body.items) 119 | done() 120 | }) 121 | }) 122 | 123 | it('test lists().members().pages().page()', (done) => { 124 | mailgun.lists(fixture.mailingList.address).members().pages().page({ 125 | 'page': 'first' 126 | }, (err, body) => { 127 | assert.ifError(err) 128 | assert.ok(Array.isArray(body.items)) 129 | assert.ok(typeof body.paging === 'object') 130 | done() 131 | }) 132 | }) 133 | 134 | it('test lists.members().info()', (done) => { 135 | const data = { 136 | 'subscribed': true, 137 | 'address': 'bob@gmail.com', 138 | 'name': 'Bob Bar', 139 | 'vars': { 140 | 'age': 26 141 | } 142 | } 143 | 144 | mailgun.lists(fixture.mailingList.address).members('bob@gmail.com').info((err, body) => { 145 | assert.ifError(err) 146 | assert.ok(body.member) 147 | assert.ok(body.member.vars) 148 | assert.strictEqual(data.subscribed, body.member.subscribed) 149 | assert.strictEqual(data.address, body.member.address) 150 | assert.strictEqual(data.name, body.member.name) 151 | assert.strictEqual(data.vars.age, body.member.vars.age) 152 | done() 153 | }) 154 | }) 155 | 156 | it('test lists().members().update()', (done) => { 157 | const data = { 158 | 'subscribed': false, 159 | 'address': 'foo@gmail.com', 160 | 'name': 'Foo Bar', 161 | 'vars': { 162 | 'age': 36 163 | } 164 | } 165 | 166 | mailgun.lists(fixture.mailingList.address).members('bob@gmail.com').update(data, (err, body) => { 167 | assert.ifError(err) 168 | assert.ok(body.message) 169 | assert.ok(body.member) 170 | assert.ok(body.member.vars) 171 | assert.strictEqual(data.subscribed, body.member.subscribed) 172 | assert.strictEqual(data.address, body.member.address) 173 | assert.strictEqual(data.name, body.member.name) 174 | assert.strictEqual(data.vars.age, body.member.vars.age) 175 | done() 176 | }) 177 | }) 178 | 179 | it('test lists().members().delete()', (done) => { 180 | const address = 'foo@gmail.com' 181 | 182 | mailgun.lists(fixture.mailingList.address).members(address).delete((err, body) => { 183 | assert.ifError(err) 184 | assert.ok(body.message) 185 | done() 186 | }) 187 | }) 188 | 189 | it('test lists().members().add() without upsert', (done) => { 190 | const members = [{ 191 | 'address': 'Alice ', 192 | 'vars': { 193 | 'age': 26 194 | } 195 | }, 196 | { 197 | 'name': 'Bob', 198 | 'address': 'bob@example.com', 199 | 'vars': { 200 | 'age': 34 201 | } 202 | } 203 | ] 204 | 205 | mailgun.lists(fixture.mailingList.address).members().add({ 206 | members 207 | }, (err, body) => { 208 | assert.ifError(err) 209 | assert.ok(body.list) 210 | assert.ok(body.list.members_count >= 0) 211 | done() 212 | }) 213 | }) 214 | 215 | it('test lists().members().add()', (done) => { 216 | const members = [{ 217 | 'address': 'Alice ', 218 | 'vars': { 219 | 'age': 26 220 | } 221 | }, 222 | { 223 | 'name': 'Bob', 224 | 'address': 'bob@example.com', 225 | 'vars': { 226 | 'age': 34 227 | } 228 | } 229 | ] 230 | 231 | mailgun.lists(fixture.mailingList.address).members().add({ 232 | members, 233 | 'upsert': true 234 | }, (err, body) => { 235 | assert.ifError(err) 236 | assert.ok(body.list) 237 | assert.ok(body.list.members_count >= 0) 238 | done() 239 | }) 240 | }) 241 | 242 | it('test lists().delete()', (done) => { 243 | mailgun.lists(fixture.mailingList.address).delete((err, body) => { 244 | assert.ifError(err) 245 | assert.ok(body) 246 | assert.ok(body.message) 247 | done() 248 | }) 249 | }) 250 | }) 251 | -------------------------------------------------------------------------------- /test/messages.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const fixture = require('./data/fixture.json') 3 | const clone = require('clone') 4 | const assert = require('assert') 5 | const fs = require('fs') 6 | const path = require('path') 7 | const request = require('request') 8 | 9 | const mailgun = require('../')({ 10 | 'apiKey': auth.api_key, 11 | 'domain': auth.domain 12 | }) 13 | 14 | describe('Messages', () => { 15 | beforeEach((done) => { 16 | setTimeout(done, 500) 17 | }) 18 | 19 | it('test messages.send() invalid "from"', (done) => { 20 | mailgun.messages().send({ 21 | 'to': fixture.message.to 22 | }, (err) => { 23 | assert.ok(err) 24 | assert(/Missing parameter 'from'/.test(err.message)) 25 | done() 26 | }) 27 | }) 28 | 29 | it('test messages().send()', (done) => { 30 | const msg = clone(fixture.message) 31 | 32 | mailgun.messages().send(msg, (err, body) => { 33 | assert.ifError(err) 34 | assert.ok(body.id) 35 | assert.ok(body.message) 36 | assert(/Queued. Thank you./.test(body.message)) 37 | done() 38 | }) 39 | }) 40 | 41 | it('test messages().send() with recipient vars', (done) => { 42 | const msg = clone(fixture.message_recipient_vars) 43 | 44 | mailgun.messages().send(msg, (err, body) => { 45 | assert.ifError(err) 46 | assert.ok(body.id) 47 | assert.ok(body.message) 48 | assert(/Queued. Thank you./.test(body.message)) 49 | done() 50 | }) 51 | }) 52 | 53 | it('test messages().send() with invalid attachment should go ok', (done) => { 54 | const msg = clone(fixture.message) 55 | 56 | msg.attachment = 123 57 | 58 | mailgun.messages().send(msg, (err, body) => { 59 | assert.ifError(err) 60 | assert.ok(body.id) 61 | assert.ok(body.message) 62 | assert(/Queued. Thank you./.test(body.message)) 63 | done() 64 | }) 65 | }) 66 | 67 | it('test messages().send() with attachment using filename', (done) => { 68 | const msg = clone(fixture.message) 69 | const filename = path.join(__dirname, '/data/mailgun_logo.png') 70 | 71 | msg.attachment = filename 72 | 73 | mailgun.messages().send(msg, (err, body) => { 74 | assert.ifError(err) 75 | assert.ok(body.id) 76 | assert.ok(body.message) 77 | assert(/Queued. Thank you./.test(body.message)) 78 | done() 79 | }) 80 | }) 81 | 82 | it('test messages().send() with attachment using filename with important flag', (done) => { 83 | const msg = clone(fixture.message) 84 | 85 | msg.important = true 86 | const filename = path.join(__dirname, '/data/mailgun_logo.png') 87 | 88 | msg.attachment = filename 89 | 90 | mailgun.messages().send(msg, (err, body) => { 91 | assert.ifError(err) 92 | assert.ok(body.id) 93 | assert.ok(body.message) 94 | assert(/Queued. Thank you./.test(body.message)) 95 | done() 96 | }) 97 | }) 98 | 99 | it('test messages().send() with attachment using filename and undefined v: var', (done) => { 100 | const msg = clone(fixture.message) 101 | const filename = path.join(__dirname, '/data/mailgun_logo.png') 102 | 103 | msg.attachment = filename 104 | 105 | msg['v:someFoo'] = undefined 106 | 107 | mailgun.messages().send(msg, (err, body) => { 108 | assert.ifError(err) 109 | assert.ok(body.id) 110 | assert.ok(body.message) 111 | assert(/Queued. Thank you./.test(body.message)) 112 | done() 113 | }) 114 | }) 115 | 116 | it('test messages().send() with attachment using file buffer', (done) => { 117 | const msg = clone(fixture.message) 118 | 119 | const filename = path.join(__dirname, '/data/mailgun_logo.png') 120 | const file = fs.readFileSync(filename) 121 | 122 | msg.attachment = file 123 | 124 | mailgun.messages().send(msg, (err, body) => { 125 | assert.ifError(err) 126 | assert.ok(body.id) 127 | assert.ok(body.message) 128 | assert(/Queued. Thank you./.test(body.message)) 129 | done() 130 | }) 131 | }) 132 | 133 | it('test messages().send() with attachment using Attachment object (Buffer)', (done) => { 134 | const msg = clone(fixture.message) 135 | 136 | const filename = '/data/mailgun_logo.png' 137 | const filepath = path.join(__dirname, filename) 138 | const file = fs.readFileSync(filepath) 139 | 140 | msg.attachment = new mailgun.Attachment({ 141 | 'data': file, 142 | filename 143 | }) 144 | 145 | mailgun.messages().send(msg, (err, body) => { 146 | assert.ifError(err) 147 | assert.ok(body.id) 148 | assert.ok(body.message) 149 | assert(/Queued. Thank you./.test(body.message)) 150 | done() 151 | }) 152 | }) 153 | 154 | it('test messages().send() with attachment using Attachment object (file)', (done) => { 155 | const msg = clone(fixture.message) 156 | 157 | const filename = '/data/mailgun_logo.png' 158 | const filepath = path.join(__dirname, filename) 159 | 160 | msg.attachment = new mailgun.Attachment({ 161 | 'data': filepath, 162 | 'filename': 'my_custom_name.png' 163 | }) 164 | 165 | mailgun.messages().send(msg, (err, body) => { 166 | assert.ifError(err) 167 | assert.ok(body.id) 168 | assert.ok(body.message) 169 | assert(/Queued. Thank you./.test(body.message)) 170 | done() 171 | }) 172 | }) 173 | 174 | it('test messages().send() with multiple attachments', (done) => { 175 | const msg = clone(fixture.message) 176 | const filename = path.join(__dirname, '/data/fixture.json') 177 | const filename2 = path.join(__dirname, '/data/mailgun_logo.png') 178 | const file = fs.readFileSync(filename2) 179 | 180 | msg.attachment = [filename, file] 181 | 182 | mailgun.messages().send(msg, (err, body) => { 183 | assert.ifError(err) 184 | assert.ok(body.id) 185 | assert.ok(body.message) 186 | assert(/Queued. Thank you./.test(body.message)) 187 | done() 188 | }) 189 | }) 190 | 191 | it('test messages().send() with invalid inline attachment should go ok', (done) => { 192 | const msg = clone(fixture.message) 193 | 194 | msg.inline = 123 195 | 196 | mailgun.messages().send(msg, (err, body) => { 197 | assert.ifError(err) 198 | assert.ok(body.id) 199 | assert.ok(body.message) 200 | assert(/Queued. Thank you./.test(body.message)) 201 | done() 202 | }) 203 | }) 204 | 205 | it('test messages().send() with inline attachment using filename', (done) => { 206 | const msg = clone(fixture.message) 207 | const filename = path.join(__dirname, '/data/mailgun_logo.png') 208 | 209 | msg.inline = filename 210 | 211 | mailgun.messages().send(msg, (err, body) => { 212 | assert.ifError(err) 213 | assert.ok(body.id) 214 | assert.ok(body.message) 215 | assert(/Queued. Thank you./.test(body.message)) 216 | done() 217 | }) 218 | }) 219 | 220 | it('test messages().send() with inline attachment using file buffer', (done) => { 221 | const msg = clone(fixture.message) 222 | 223 | const filename = path.join(__dirname, '/data/mailgun_logo.png') 224 | const file = fs.readFileSync(filename) 225 | 226 | msg.inline = file 227 | 228 | mailgun.messages().send(msg, (err, body) => { 229 | assert.ifError(err) 230 | assert.ok(body.id) 231 | assert.ok(body.message) 232 | assert(/Queued. Thank you./.test(body.message)) 233 | done() 234 | }) 235 | }) 236 | 237 | it('test messages().send() with inline attachment using Attachment object (Buffer)', (done) => { 238 | const msg = clone(fixture.message) 239 | 240 | const filename = '/data/mailgun_logo.png' 241 | const filepath = path.join(__dirname, filename) 242 | const file = fs.readFileSync(filepath) 243 | 244 | msg.inline = new mailgun.Attachment({ 245 | 'data': file, 246 | filename 247 | }) 248 | 249 | mailgun.messages().send(msg, (err, body) => { 250 | assert.ifError(err) 251 | assert.ok(body.id) 252 | assert.ok(body.message) 253 | assert(/Queued. Thank you./.test(body.message)) 254 | done() 255 | }) 256 | }) 257 | 258 | it('test messages().send() with inline attachment using Attachment object (file)', (done) => { 259 | const msg = clone(fixture.message) 260 | 261 | const filename = '/data/mailgun_logo.png' 262 | const filepath = path.join(__dirname, filename) 263 | 264 | msg.inline = new mailgun.Attachment({ 265 | 'data': filepath, 266 | 'filename': 'my_custom_name.png' 267 | }) 268 | 269 | mailgun.messages().send(msg, (err, body) => { 270 | assert.ifError(err) 271 | assert.ok(body.id) 272 | assert.ok(body.message) 273 | assert(/Queued. Thank you./.test(body.message)) 274 | done() 275 | }) 276 | }) 277 | 278 | it('test messages().send() with multiple inline attachments', (done) => { 279 | const msg = clone(fixture.message) 280 | const filename = path.join(__dirname, '/data/fixture.json') 281 | const filename2 = path.join(__dirname, '/data/mailgun_logo.png') 282 | const file = fs.readFileSync(filename2) 283 | 284 | msg.inline = [filename, file] 285 | 286 | mailgun.messages().send(msg, (err, body) => { 287 | assert.ifError(err) 288 | assert.ok(body.id) 289 | assert.ok(body.message) 290 | assert(/Queued. Thank you./.test(body.message)) 291 | done() 292 | }) 293 | }) 294 | 295 | it('test messages().send() with multiple inline and normal attachments', (done) => { 296 | const msg = clone(fixture.message) 297 | const filename = path.join(__dirname, '/data/fixture.json') 298 | const filename2 = path.join(__dirname, '/data/mailgun_logo.png') 299 | 300 | msg.attachment = filename 301 | msg.inline = filename2 302 | 303 | mailgun.messages().send(msg, (err, body) => { 304 | assert.ifError(err) 305 | assert.ok(body.id) 306 | assert.ok(body.message) 307 | assert(/Queued. Thank you./.test(body.message)) 308 | done() 309 | }) 310 | }) 311 | 312 | it('test messages().send() with multiple tags', (done) => { 313 | const msg = clone(fixture.message) 314 | 315 | msg['o:tag'] = ['tag1', 'tag2', 'tag3'] 316 | 317 | mailgun.messages().send(msg, (err, body) => { 318 | assert.ifError(err) 319 | assert.ok(body.id) 320 | assert.ok(body.message) 321 | assert(/Queued. Thank you./.test(body.message)) 322 | done() 323 | }) 324 | }) 325 | 326 | it('test messages().send() with multiple tags and normal attachment', (done) => { 327 | const msg = clone(fixture.message) 328 | const filename = path.join(__dirname, '/data/fixture.json') 329 | 330 | msg.attachment = filename 331 | 332 | msg['o:tag'] = ['tag1', 'tag2', 'tag3'] 333 | 334 | mailgun.messages().send(msg, (err, body) => { 335 | assert.ifError(err) 336 | assert.ok(body.id) 337 | assert.ok(body.message) 338 | assert(/Queued. Thank you./.test(body.message)) 339 | done() 340 | }) 341 | }) 342 | 343 | it('test messages().send() with attachment using stream', (done) => { 344 | const msg = clone(fixture.message) 345 | 346 | const file = request('https://www.google.ca/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png') 347 | 348 | msg.attachment = file 349 | 350 | mailgun.messages().send(msg, (err, body) => { 351 | assert.ifError(err) 352 | assert.ok(body.id) 353 | assert.ok(body.message) 354 | assert(/Queued. Thank you./.test(body.message)) 355 | done() 356 | }) 357 | }) 358 | 359 | it('test messages().send() with attachment using Attachment object (stream)', (done) => { 360 | const msg = clone(fixture.message) 361 | 362 | const filename = '/data/mailgun_logo.png' 363 | const filepath = path.join(__dirname, filename) 364 | const fileStream = fs.createReadStream(filepath) 365 | const fileStat = fs.statSync(filepath) 366 | 367 | msg.attachment = new mailgun.Attachment({ 368 | 'data': fileStream, 369 | 'filename': 'my_custom_name.png', 370 | 'knownLength': fileStat.size, 371 | 'contentType': 'image/png' 372 | }) 373 | 374 | mailgun.messages().send(msg, (err, body) => { 375 | assert.ifError(err) 376 | assert.ok(body.id) 377 | assert.ok(body.message) 378 | assert(/Queued. Thank you./.test(body.message)) 379 | done() 380 | }) 381 | }) 382 | 383 | it('test messages().send() with attachment using Attachment object (stream) and only filename', (done) => { 384 | const msg = clone(fixture.message) 385 | 386 | const file = request('https://www.google.ca/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png') 387 | 388 | msg.attachment = new mailgun.Attachment({ 389 | 'data': file, 390 | 'filename': 'my_custom_name.png' 391 | }) 392 | 393 | mailgun.messages().send(msg, (err, body) => { 394 | assert.ifError(err) 395 | assert.ok(body.id) 396 | assert.ok(body.message) 397 | assert(/Queued. Thank you./.test(body.message)) 398 | done() 399 | }) 400 | }) 401 | 402 | it('test messages().send() with custom data', (done) => { 403 | const msg = clone(fixture.message) 404 | 405 | msg['v:whatever'] = 123 406 | msg['v:test'] = true 407 | 408 | mailgun.messages().send(msg, (err, body) => { 409 | assert.ifError(err) 410 | assert.ok(body.id) 411 | assert.ok(body.message) 412 | assert(/Queued. Thank you./.test(body.message)) 413 | done() 414 | }) 415 | }) 416 | 417 | it('test messages().send() with custom data as object', (done) => { 418 | const msg = clone(fixture.message) 419 | 420 | msg['v:my-custom-data'] = { 421 | 'whatever': 123, 422 | 'test': true 423 | } 424 | 425 | mailgun.messages().send(msg, (err, body) => { 426 | assert.ifError(err) 427 | assert.ok(body.id) 428 | assert.ok(body.message) 429 | assert(/Queued. Thank you./.test(body.message)) 430 | done() 431 | }) 432 | }) 433 | 434 | describe('Retry', () => { 435 | it('messages().send() should work with retry option', (done) => { 436 | const mg = new mailgun.Mailgun({ 437 | 'apiKey': auth.api_key, 438 | 'domain': auth.domain, 439 | 'retry': 3 440 | }) 441 | 442 | const msg = clone(fixture.message) 443 | 444 | mg.messages().send(msg, (err, body) => { 445 | assert.ifError(err) 446 | assert.ok(body.id) 447 | assert.ok(body.message) 448 | assert(/Queued. Thank you./.test(body.message)) 449 | done() 450 | }) 451 | }) 452 | 453 | it('messages().send() should retry when request fails', (done) => { 454 | process.env.DEBUG_MAILGUN_FORCE_RETRY = true 455 | const mg = new mailgun.Mailgun({ 456 | 'apiKey': auth.api_key, 457 | 'domain': auth.domain, 458 | 'retry': 3 459 | }) 460 | 461 | const msg = clone(fixture.message) 462 | 463 | mg.messages().send(msg, (err, body) => { 464 | assert.ifError(err) 465 | assert.ok(body.id) 466 | assert.ok(body.message) 467 | assert(/Queued. Thank you./.test(body.message)) 468 | done() 469 | }) 470 | }) 471 | 472 | it('messages().send() should work with retry option as an object', (done) => { 473 | const mg = new mailgun.Mailgun({ 474 | 'apiKey': auth.api_key, 475 | 'domain': auth.domain, 476 | 'retry': { 477 | 'times': 3, 478 | 'interval': 100 479 | } 480 | }) 481 | 482 | const msg = clone(fixture.message) 483 | 484 | mg.messages().send(msg, (err, body) => { 485 | assert.ifError(err) 486 | assert.ok(body.id) 487 | assert.ok(body.message) 488 | assert(/Queued. Thank you./.test(body.message)) 489 | done() 490 | }) 491 | }) 492 | 493 | it('messages().send() should retry when request fails with a delay of 100 ms between attempts', (done) => { 494 | process.env.DEBUG_MAILGUN_FORCE_RETRY = true 495 | const mg = new mailgun.Mailgun({ 496 | 'apiKey': auth.api_key, 497 | 'domain': auth.domain, 498 | 'retry': { 499 | 'times': 3, 500 | 'interval': 100 501 | } 502 | }) 503 | 504 | const msg = clone(fixture.message) 505 | 506 | mg.messages().send(msg, (err, body) => { 507 | assert.ifError(err) 508 | assert.ok(body.id) 509 | assert.ok(body.message) 510 | assert(/Queued. Thank you./.test(body.message)) 511 | done() 512 | }) 513 | }) 514 | }) 515 | }) 516 | -------------------------------------------------------------------------------- /test/routes.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const fixture = require('./data/fixture.json') 3 | const assert = require('assert') 4 | 5 | const mailgun = require('../')({ 6 | 'apiKey': auth.api_key, 7 | 'domain': auth.domain 8 | }) 9 | 10 | let routeId = -1 11 | 12 | describe('Routes', () => { 13 | beforeEach((done) => { 14 | setTimeout(done, 500) 15 | }) 16 | 17 | it('test routes().create() invalid missing expression', (done) => { 18 | mailgun.routes().create({}, (err) => { 19 | assert.ok(err) 20 | assert(/Missing parameter 'expression'/.test(err.message)) 21 | done() 22 | }) 23 | }) 24 | 25 | it('test routes().create()', (done) => { 26 | mailgun.routes().create(fixture.route, (err, body) => { 27 | assert.ifError(err) 28 | assert.ok(body) 29 | assert.ok(body.route) 30 | routeId = body.route.id 31 | done() 32 | }) 33 | }) 34 | 35 | it('test routes().list()', (done) => { 36 | mailgun.routes().list((err, body) => { 37 | assert.ifError(err) 38 | assert.ok(body.total_count >= 0) 39 | assert.ok(body.items) 40 | done() 41 | }) 42 | }) 43 | 44 | it('test routes().info()', (done) => { 45 | mailgun.routes(routeId).info((err, body) => { 46 | assert.ifError(err) 47 | assert.ok(body.route) 48 | assert.strictEqual(routeId, body.route.id) 49 | done() 50 | }) 51 | }) 52 | 53 | it('test routes().update() with one action argument', (done) => { 54 | mailgun.routes(routeId).update({ 55 | 'description': 'new route update', 56 | 'expression': 'match_recipient(".*@samples.mailgun.org")', 57 | 'action': 'forward("http://myhost.com/messages/")' 58 | }, (err, body) => { 59 | assert.ifError(err) 60 | assert.ok(body.message) 61 | assert.strictEqual(routeId, body.id) 62 | done() 63 | }) 64 | }) 65 | 66 | it('test routes().update() with two action arguments', (done) => { 67 | mailgun.routes(routeId).update({ 68 | 'description': 'test new route update', 69 | 'expression': 'match_recipient(".*@samples.mailgun.org")', 70 | 'action': ['forward("http://myhost.com/messages/")', 'stop()'] 71 | }, (err, body) => { 72 | assert.ifError(err) 73 | assert.ok(body.message) 74 | assert.strictEqual(routeId, body.id) 75 | done() 76 | }) 77 | }) 78 | 79 | it('test routes().delete()', (done) => { 80 | mailgun.routes(routeId).delete((err, body) => { 81 | assert.ifError(err) 82 | assert.ok(body.message) 83 | done() 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /test/sendmime.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const fixture = require('./data/fixture.json') 3 | const assert = require('assert') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const MailComposer = require('nodemailer/lib/mail-composer') 7 | 8 | const mailgun = require('../')({ 9 | 'apiKey': auth.api_key, 10 | 'domain': auth.domain 11 | }) 12 | 13 | describe('Send MIME', () => { 14 | beforeEach((done) => { 15 | setTimeout(done, 500) 16 | }) 17 | 18 | it('test sendMime()', (done) => { 19 | const mailOptions = { 20 | 'from': fixture.message.from, 21 | 'to': fixture.message.to, 22 | 'subject': fixture.message.subject, 23 | 'body': fixture.message.text, 24 | 'html': `${fixture.message.text}` 25 | } 26 | 27 | const mail = new MailComposer(mailOptions) 28 | 29 | mail.compile().build((err, message) => { 30 | assert.ifError(err) 31 | 32 | const data = { 33 | 'to': fixture.message.to, 34 | 'message': message.toString('ascii') 35 | } 36 | 37 | mailgun.messages().sendMime(data, (err, body) => { 38 | assert.ifError(err) 39 | assert.ok(body.id) 40 | assert.ok(body.message) 41 | assert(/Queued. Thank you./.test(body.message)) 42 | done() 43 | }) 44 | }) 45 | }) 46 | 47 | it('test sendMime() with to field as array', (done) => { 48 | const mailOptions = { 49 | 'from': fixture.message.from, 50 | 'to': fixture.message.to, 51 | 'subject': fixture.message.subject, 52 | 'body': fixture.message.text, 53 | 'html': `${fixture.message.text}` 54 | } 55 | 56 | const mail = new MailComposer(mailOptions) 57 | 58 | mail.compile().build((err, message) => { 59 | assert.ifError(err) 60 | 61 | const data = { 62 | 'to': fixture.message_recipient_vars.to, 63 | 'message': message.toString('ascii') 64 | } 65 | 66 | mailgun.messages().sendMime(data, (err, body) => { 67 | assert.ifError(err) 68 | assert.ok(body.id) 69 | assert.ok(body.message) 70 | assert(/Queued. Thank you./.test(body.message)) 71 | done() 72 | }) 73 | }) 74 | }) 75 | 76 | it('test sendMime() with file path', (done) => { 77 | const filePath = path.join(__dirname, '/data/message.eml') 78 | 79 | const data = { 80 | 'to': fixture.message.to, 81 | 'message': filePath 82 | } 83 | 84 | mailgun.messages().sendMime(data, (err, body) => { 85 | assert.ifError(err) 86 | assert.ok(body.id) 87 | assert.ok(body.message) 88 | assert(/Queued. Thank you./.test(body.message)) 89 | done() 90 | }) 91 | }) 92 | 93 | it('test sendMime() with file stream', (done) => { 94 | const filePath = path.join(__dirname, '/data/message.eml') 95 | 96 | const data = { 97 | 'to': fixture.message.to, 98 | 'message': fs.createReadStream(filePath) 99 | } 100 | 101 | mailgun.messages().sendMime(data, (err, body) => { 102 | assert.ifError(err) 103 | assert.ok(body.id) 104 | assert.ok(body.message) 105 | assert(/Queued. Thank you./.test(body.message)) 106 | done() 107 | }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /test/stats.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const assert = require('assert') 3 | 4 | const mailgun = require('../')({ 5 | 'apiKey': auth.api_key, 6 | 'domain': auth.domain 7 | }) 8 | 9 | describe('Stats', () => { 10 | beforeEach((done) => { 11 | setTimeout(done, 500) 12 | }) 13 | 14 | it('test mailgun.stats().list()', (done) => { 15 | mailgun.stats().list((err, body) => { 16 | assert.ifError(err) 17 | assert.ok(body.total_count) 18 | assert.ok(body.items) 19 | done() 20 | }) 21 | }) 22 | 23 | it('test mailgun.stats().list() with one argument', (done) => { 24 | mailgun.stats().list({ 25 | 'event': 'delivered' 26 | }, (err, body) => { 27 | assert.ifError(err) 28 | assert.ok(body.total_count) 29 | assert.ok(body.items) 30 | done() 31 | }) 32 | }) 33 | 34 | it('test mailgun.stats().list() with arguments', (done) => { 35 | mailgun.stats().list({ 36 | 'event': ['sent', 'delivered'] 37 | }, (err, body) => { 38 | assert.ifError(err) 39 | assert.ok(body.total_count) 40 | assert.ok(body.items) 41 | done() 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/tags.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const assert = require('assert') 3 | 4 | const mailgun = require('../')({ 5 | 'apiKey': auth.api_key, 6 | 'domain': auth.domain 7 | }) 8 | 9 | describe('Tags', () => { 10 | beforeEach((done) => { 11 | setTimeout(done, 500) 12 | }) 13 | 14 | it('test mailgun.tags().list()', (done) => { 15 | mailgun.tags().list((err, body) => { 16 | assert.ifError(err) 17 | assert.ok(body) 18 | done() 19 | }) 20 | }) 21 | 22 | it('test mailgun.tags().info()', (done) => { 23 | mailgun.tags('tag1').info((err, body) => { 24 | assert.ifError(err) 25 | assert.ok(body) 26 | done() 27 | }) 28 | }) 29 | 30 | it('test mailgun.tags().stats().info()', (done) => { 31 | mailgun.tags('tag1').stats().info({ 32 | event: ['accepted', 'delivered', 'opened', 'clicked'] 33 | }, (err, body) => { 34 | assert.ifError(err) 35 | assert.ok(body) 36 | done() 37 | }) 38 | }) 39 | 40 | it('test mailgun.tags().aggregates().countries().list()', (done) => { 41 | mailgun.tags('tag1').stats().aggregates().countries().list((err, body) => { 42 | assert.ifError(err) 43 | assert.ok(body) 44 | assert.ok(body.countries) 45 | done() 46 | }) 47 | }) 48 | 49 | it('test mailgun.tags().aggregates().providers().list()', (done) => { 50 | mailgun.tags('tag1').stats().aggregates().providers().list((err, body) => { 51 | assert.ifError(err) 52 | assert.ok(body) 53 | assert.ok(body.providers) 54 | done() 55 | }) 56 | }) 57 | 58 | it('test mailgun.tags().aggregates().devices().list()', (done) => { 59 | mailgun.tags('tag1').stats().aggregates().devices().list((err, body) => { 60 | assert.ifError(err) 61 | assert.ok(body) 62 | assert.ok(body.devices) 63 | done() 64 | }) 65 | }) 66 | 67 | it('test mailgun.tags().delete()', (done) => { 68 | mailgun.tags('tag1').delete((err, body) => { 69 | assert.ifError(err) 70 | assert.ok(body.message) 71 | done() 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /test/testmode.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const fixture = require('./data/fixture.json') 3 | const clone = require('clone') 4 | const assert = require('assert') 5 | const path = require('path') 6 | const debug = require('debug') 7 | const sinon = require('sinon') 8 | 9 | let mailgun 10 | let log, sandbox 11 | 12 | describe('test mode', () => { 13 | describe('default operation', () => { 14 | before(() => { 15 | mailgun = require('../')({ 16 | apiKey: auth.api_key, 17 | domain: auth.domain, 18 | testMode: true 19 | }) 20 | }) 21 | 22 | beforeEach(done => { 23 | setTimeout(done, 100) 24 | }) 25 | 26 | it('test messages().send()', done => { 27 | const msg = clone(fixture.message) 28 | 29 | mailgun.messages().send(msg, (err, body) => { 30 | assert.strictEqual(err, undefined) 31 | assert.strictEqual(body, undefined) 32 | 33 | done() 34 | }) 35 | }) 36 | 37 | it('test messages().send() with attachment using filename', done => { 38 | const msg = clone(fixture.message) 39 | const filename = path.join(__dirname, '/data/mailgun_logo.png') 40 | 41 | msg.attachment = filename 42 | 43 | mailgun.messages().send(msg, (err, body) => { 44 | assert.strictEqual(err, undefined) 45 | assert.strictEqual(body, undefined) 46 | 47 | done() 48 | }) 49 | }) 50 | }) 51 | 52 | describe('default operation with DEBUG', () => { 53 | before(() => { 54 | mailgun = require('../')({ 55 | apiKey: auth.api_key, 56 | domain: auth.domain, 57 | testMode: true 58 | }) 59 | 60 | debug.enable('mailgun-js') 61 | }) 62 | 63 | after(() => { 64 | debug.disable() 65 | }) 66 | 67 | beforeEach(done => { 68 | setTimeout(done, 100) 69 | }) 70 | 71 | it('test messages().send()', done => { 72 | const msg = clone(fixture.message) 73 | 74 | mailgun.messages().send(msg, (err, body) => { 75 | assert.strictEqual(err, undefined) 76 | assert.strictEqual(body, undefined) 77 | 78 | done() 79 | }) 80 | }) 81 | }) 82 | 83 | describe('custom logger', () => { 84 | before(() => { 85 | mailgun = require('../')({ 86 | apiKey: auth.api_key, 87 | domain: auth.domain, 88 | testMode: true, 89 | testModeLogger: (httpOptions, payload, form) => { 90 | const { method, path } = httpOptions 91 | const hasPayload = !!payload 92 | const hasForm = !!form 93 | 94 | console.log(`%s %s payload: %s form: %s`, method, path, hasPayload, hasForm) 95 | } 96 | }) 97 | }) 98 | 99 | beforeEach(done => { 100 | sandbox = sinon.createSandbox() 101 | log = sandbox.spy(console, 'log') 102 | 103 | setTimeout(done, 100) 104 | }) 105 | 106 | afterEach(() => { 107 | sandbox.restore() 108 | }) 109 | 110 | it('test messages().send()', done => { 111 | const msg = clone(fixture.message) 112 | 113 | mailgun.messages().send(msg, (err, body) => { 114 | assert.strictEqual(err, undefined) 115 | assert.strictEqual(body, undefined) 116 | 117 | assert.ok(log.calledOnce) 118 | assert.ok( 119 | log.calledWith( 120 | `%s %s payload: %s form: %s`, 121 | 'POST', 122 | '/v3/sandbox77047.mailgun.org/messages', 123 | true, 124 | false 125 | ) 126 | ) 127 | 128 | done() 129 | }) 130 | }) 131 | 132 | it('test messages().send() with attachment using filename', done => { 133 | const msg = clone(fixture.message) 134 | const filename = path.join(__dirname, '/data/mailgun_logo.png') 135 | 136 | msg.attachment = filename 137 | 138 | mailgun.messages().send(msg, (err, body) => { 139 | assert.strictEqual(err, undefined) 140 | assert.strictEqual(body, undefined) 141 | 142 | assert.ok(log.calledOnce) 143 | assert.ok( 144 | log.calledWith( 145 | `%s %s payload: %s form: %s`, 146 | 'POST', 147 | '/v3/sandbox77047.mailgun.org/messages', 148 | false, 149 | true 150 | ) 151 | ) 152 | 153 | done() 154 | }) 155 | }) 156 | }) 157 | }) 158 | -------------------------------------------------------------------------------- /test/unsubscribes.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const fixture = require('./data/fixture.json') 3 | const assert = require('assert') 4 | 5 | const mailgun = require('../')({ 6 | 'apiKey': auth.api_key, 7 | 'domain': auth.domain 8 | }) 9 | 10 | describe('Unsubscribes', () => { 11 | beforeEach((done) => { 12 | setTimeout(done, 500) 13 | }) 14 | 15 | it('test unsubscribes().create()', (done) => { 16 | mailgun.unsubscribes().create(fixture.unsubscribe, (err, body) => { 17 | assert.ifError(err) 18 | assert.ok(body) 19 | done() 20 | }) 21 | }) 22 | 23 | it('test unsubscribes().create() multi', (done) => { 24 | mailgun.unsubscribes().create([fixture.unsubscribe, fixture.unsubscribe2], (err, body) => { 25 | assert.ifError(err) 26 | assert.ok(body) 27 | done() 28 | }) 29 | }) 30 | 31 | it('test unsubscribes().list()', (done) => { 32 | mailgun.unsubscribes().list((err, body) => { 33 | assert.ifError(err) 34 | assert.ok(body.items) 35 | done() 36 | }) 37 | }) 38 | 39 | it('test unsubscribes().info()', (done) => { 40 | mailgun.unsubscribes(fixture.unsubscribe.address).info((err, body) => { 41 | assert.ifError(err) 42 | assert.ok(body) 43 | done() 44 | }) 45 | }) 46 | 47 | it('test unsubscribes().delete()', (done) => { 48 | mailgun.unsubscribes(fixture.unsubscribe.address).delete((err, body) => { 49 | assert.ifError(err) 50 | assert.ok(body) 51 | done() 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/validation.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('./data/auth.json') 2 | const assert = require('assert') 3 | 4 | const mailgun = require('../')({ 5 | 'apiKey': auth.api_key, 6 | 'domain': auth.domain 7 | }) 8 | 9 | describe('Email Validation', () => { 10 | beforeEach((done) => { 11 | setTimeout(done, 500) 12 | }) 13 | 14 | it('test mailgun.validate() should validate one email address', (done) => { 15 | const mg = new mailgun.Mailgun({ 16 | 'apiKey': auth.api_key, 17 | 'publicApiKey': auth.public_api_key, 18 | 'domain': auth.domain 19 | }) 20 | 21 | mg.validate('raboof@gmail.com', (err, body) => { 22 | assert.ifError(err) 23 | assert.ok(body) 24 | done() 25 | }) 26 | }) 27 | 28 | it('test mailgun.validate() should validate one email address using private option', (done) => { 29 | const mg = new mailgun.Mailgun({ 30 | 'apiKey': auth.api_key, 31 | 'publicApiKey': auth.public_api_key, 32 | 'domain': auth.domain 33 | }) 34 | 35 | mg.validate('raboof@gmail.com', true, (err, body) => { 36 | assert.ifError(err) 37 | assert.ok(body) 38 | done() 39 | }) 40 | }) 41 | 42 | it('test mailgun.parse() should parse list of email addresses', (done) => { 43 | const mg = new mailgun.Mailgun({ 44 | 'apiKey': auth.api_key, 45 | 'publicApiKey': auth.public_api_key, 46 | 'domain': auth.domain 47 | }) 48 | 49 | mg.parse(['raboof@gmail.com'], (err, body) => { 50 | assert.ifError(err) 51 | assert.ok(body) 52 | done() 53 | }) 54 | }) 55 | 56 | it('test mailgun.parse() should parse list of email addresses with an option', (done) => { 57 | const mg = new mailgun.Mailgun({ 58 | 'apiKey': auth.api_key, 59 | 'publicApiKey': auth.public_api_key, 60 | 'domain': auth.domain 61 | }) 62 | 63 | mg.parse(['raboof@gmail.com'], { 64 | 'syntax_only': false 65 | }, (err, body) => { 66 | assert.ifError(err) 67 | assert.ok(body) 68 | done() 69 | }) 70 | }) 71 | 72 | it('test mailgun.parse() should private parse list of email addresses with an option', (done) => { 73 | const mg = new mailgun.Mailgun({ 74 | 'apiKey': auth.api_key, 75 | 'publicApiKey': auth.public_api_key, 76 | 'domain': auth.domain 77 | }) 78 | 79 | mg.parse(['raboof@gmail.com'], true, { 80 | 'syntax_only': false 81 | }, (err, body) => { 82 | assert.ifError(err) 83 | assert.ok(body) 84 | done() 85 | }) 86 | }) 87 | 88 | it('test mailgun.parse() should parse list of email addresses using private option', (done) => { 89 | const mg = new mailgun.Mailgun({ 90 | 'apiKey': auth.api_key, 91 | 'publicApiKey': auth.public_api_key, 92 | 'domain': auth.domain 93 | }) 94 | 95 | mg.parse(['raboof@gmail.com'], true, (err, body) => { 96 | assert.ifError(err) 97 | assert.ok(body) 98 | done() 99 | }) 100 | }) 101 | }) 102 | --------------------------------------------------------------------------------