Skip to content

Commit

Permalink
Merge pull request #1 from pdroll/switch-to-mailgun-dot-js
Browse files Browse the repository at this point in the history
Move to mailgun.js, complete test suite, modernize code base
  • Loading branch information
pdroll authored Jan 15, 2021
2 parents f3e7fb7 + 0e48b63 commit 8a8afd1
Show file tree
Hide file tree
Showing 17 changed files with 1,754 additions and 840 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present
- run: npm run lint
- run: npm test
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Easily send mail, even HTML emails, from the command line using the Mailgun API.
npm install mailgun-send-cli -g
```

Or if you're using [Yarn](https://yarnpkg.com/):
Note, this uses [mailgun.js](https://github.com/mailgun/mailgun-js) under the hood, which requires node.js >= 12.x. If you are using an older version of node, consider using an older version of this package. Version 0.2.0 requires node >=8, while version 0.1.4 requires node >=4.3.2.

```
yarn global add mailgun-send-cli
npm install mailgun-send-cli@0.2.0 -g
```

## Usage
Expand All @@ -20,7 +20,7 @@ yarn global add mailgun-send-cli
mailgun-send [options]
```

You will be prompted to enter your Mailgun [API Key](https://mailgun.com/app/account/security) and [domain](https://mailgun.com/app/domains) on your first use. These values will be used on every subsequent call until the `--reset` flag is used to reset them.
You will be prompted to enter your Mailgun [API Key](https://app.mailgun.com/app/account/security/api_keys) and [domain](https://app.mailgun.com/app/sending/domains/) on your first use. These values will be used on every subsequent call until the `--reset` flag is used to reset them.

### Examples

Expand Down Expand Up @@ -71,6 +71,9 @@ Output usage information
#### `-V, --version`
Output the version number

#### `-v, --verbose`
Output more detailed information, such as message id


## Troubleshooting

Expand Down
27 changes: 27 additions & 0 deletions __mocks__/keytar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
let data = {};

const getPassword = jest.fn(async (name, key) => (
(data[name] && data[name][key]) ? data[name][key] : null));

const setPassword = jest.fn(async (name, key, value) => {
data[name] = data[name] || {};
data[name][key] = value;
});

const deletePassword = jest.fn(async (name, key) => {
if (!getPassword(name, key)) {
return false;
}

delete data[name][key];

return true;
});

const clearMockData = () => {
data = {};
};

module.exports = {
getPassword, setPassword, deletePassword, clearMockData,
};
35 changes: 0 additions & 35 deletions __mocks__/mailgun-js.js

This file was deleted.

35 changes: 35 additions & 0 deletions __mocks__/mailgun.js.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const faker = require('faker');

function mailgunClientMock({ key, username }) {
this.apiKey = key;
this.username = username;

this.messages = {
create: jest.fn(async () => {
if (this.apiKey === 'FAIL_UNAUTHORIZED') {
const e = new Error();
e.status = 401;
throw e;
}

if (this.apiKey === 'FAIL_NETWORK') {
const e = new Error();
e.type = 'EUNAVAILABLE';
throw e;
}

if (this.apiKey === 'FAIL_OTHER') {
throw new Error('Failed');
}

return {
id: faker.random.uuid(),
message: 'Queued. Thank you.',
};
}),
};

return this;
}

module.exports = { client: mailgunClientMock };
25 changes: 25 additions & 0 deletions bin/__snapshots__/mailgun-send.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`help option prints help text 1`] = `
"Usage: mailgun-send [options]
You will be prompted to enter your Mailgun API Key [https://app.mailgun.com/app/account/security/api_keys] and Domain [https://app.mailgun.com/app/sending/domains/] on your first use.
Options:
-V, --version output the version number
-s, --subject <value> Subject of Email
-t, --to <value> Email address of recipient of email
-f, --from <value> Email address of email sender
-r, --reply <value> ReplyTo email address. Optional
-c, --cc <value> Email address to CC. Optional
-b, --bcc <value> Email address to BCC. Optional
-T, --text <value> Text to send as body of email. Must specify this or
--htmlpath.
-H, --htmlpath <value> Path to HTML file to send as email. Must specify this
or --text.
-R, --reset Reset Mailgun API key and Domain. You will be
prompted to enter these again.
-v, --verbose Output more detailed information, such as message id
-h, --help display help for command
"
`;
82 changes: 18 additions & 64 deletions bin/mailgun-send.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,29 @@
#!/usr/bin/env node
const runCli = require('../lib/runCli.js');

const { Command } = require('commander');
const keytar = require('keytar');
const readlineSync = require('readline-sync');
const MailgunSend = require('../lib/MailgunSend');
const packageJson = require('../package.json');
const { argv } = process;

const program = new Command();
// Suppress commander error logging,
// since we're handling them ourselves
global.console.error = () => {};

/**
* Configure Program options and parse arguments
*/
program
.version(packageJson.version)
.usage('[options]')
.description('You will be prompted to enter your Mailgun API Key [https://mailgun.com/app/account/security] and Domain [https://mailgun.com/app/domains] on your first use.')
.option('-s, --subject <value>', 'Subject of Email')
.option('-t, --to <value>', 'Email address of recipient of email')
.option('-f, --from <value>', 'Email address of email sender')
.option('-r, --reply <value>', 'ReplyTo email address. Optional')
.option('-c, --cc <value>', 'Email address to CC. Optional')
.option('-b, --bcc <value>', 'Email address to BCC. Optional')
.option('-T, --text <value>', 'Text to send as body of email. Must specify this or --htmlpath.')
.option('-H, --htmlpath <value>', 'Path to HTML file to send as email. Must specify this or --text.')
.option('-R, --reset', 'Reset Mailgun API key and Domain. You will be prompted to enter these again.')
.parse(process.argv);

(async () => {
/**
* Get/Set Mailgun creds from keychain.
* Prompt user for them if they are not found.
*/
if (program.reset) {
await keytar.deletePassword(packageJson.name, 'apiKey');
await keytar.deletePassword(packageJson.name, 'domain');
const exitOverride = (e) => {
// Don't treat help or version commands as errors
if (['commander.helpDisplayed', 'commander.version'].includes(e.code)) {
process.exit(0);
}

let apiKey = await keytar.getPassword(packageJson.name, 'apiKey');
let domain = await keytar.getPassword(packageJson.name, 'domain');
throw new Error(e.message);
};

if (!domain) {
domain = readlineSync.question('Mailgun Domain (e.g. mg.example.com): ');
await keytar.setPassword(packageJson.name, 'domain', domain);
}

if (!apiKey) {
apiKey = readlineSync.question('Mailgun API (e.g. key-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX): ', {
hideEchoBack: true,
});
await keytar.setPassword(packageJson.name, 'apiKey', apiKey);
}

/**
* Attempt to send email
*/
const mg = new MailgunSend({ apiKey, domain });

mg.send({
subject: program.subject,
to: program.to,
from: program.from,
reply: program.reply,
cc: program.cc,
bcc: program.bcc,
text: program.text,
htmlpath: program.htmlpath,
}).then((msg) => {
(async () => {
try {
const msg = await runCli({ argv, exitOverride });
console.log(`\n✅ Success!\n\t${msg}`);
}).catch((e) => {
} catch (e) {
// Remove extraneous 'Error:' if present
const errMsg = `${e}`.replace('Error:', '');
const errMsg = `${e.message || e}`.replace(/error:/i, '');
console.log(`\n🚨 Error:${errMsg}\n`);
});
process.exit(1);
}
})();
57 changes: 57 additions & 0 deletions bin/mailgun-send.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const path = require('path');
const { exec } = require('child_process');
const { version } = require('../package.json');

const cli = (args) => new Promise((resolve) => {
exec(`node ${path.resolve('bin/mailgun-send.js')} ${args.join(' ')}`,
{ cwd: '.' },
(error, stdout, stderr) => {
resolve({
code: error && error.code ? error.code : 0,
error,
stdout,
stderr,
});
});
});

describe('help option', () => {
it('returns a zero code', async () => {
const result = await cli(['-h']);

expect(result.code).toBe(0);
});

it('prints help text', async () => {
const result = await cli(['-h']);

expect(result.stdout).toMatchSnapshot();
});
});

describe('version option', () => {
it('returns a zero code', async () => {
const result = await cli(['-V']);

expect(result.code).toBe(0);
});

it('prints the version from the package.json file', async () => {
const result = await cli(['-V']);

expect(result.stdout).toMatch(version);
});
});

describe('error handling', () => {
it('returns error code of 1', async () => {
const result = await cli(['-t']);
expect(result.code).toBe(1);
});

it('prints a friendly error message for commander errors', async () => {
const result = await cli(['-W']);
expect(result.stdout).toMatch(/🚨 {2}Error:/);
expect(result.stdout).toMatch(/unknown option/i);
});
});
Loading

0 comments on commit 8a8afd1

Please sign in to comment.