Merge pull request #1 from eruizdechavez/update-dependencies

Update dependencies, refactor as needed
This commit is contained in:
Erick Ruiz de Chavez 2019-05-10 18:46:32 -04:00 committed by GitHub
commit 18605d862e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 2872 additions and 191 deletions

5
.gitignore vendored
View file

@ -1,4 +1,5 @@
node_modules
development.yml
production.yml
local.json
development.json
production.json
vendor

View file

@ -6,64 +6,65 @@ A basic node server for sending email forms.
```html
<form method="post" action="http://example.com/contact@example.com">
<input type="hidden" name="_subject" value="This is a test form" />
<input type="email" name="_from" />
<input type="text" name="first_name" />
<input type="text" name="last_name" />
<textarea name="comments"></textarea>
<input type="submit" name="Submit" />
<input type="hidden" name="_subject" value="This is a test form" />
<input type="email" name="_from" />
<input type="text" name="first_name" />
<input type="text" name="last_name" />
<textarea name="comments"></textarea>
<input type="submit" name="Submit" />
</form>
```
This project is, in some way, a clone of a great free service: [Formspree.io](https://formspree.io/). Although Formspree is great, it might **not** be the best option for everyone; 2 key factors drived me to create this clone:
This project is, in some way, a clone of an excellent free service: [Formspree.io](https://formspree.io/). Although Formspree is outstanding, it might **not** be the best option for everyone; 2 key factors drove me to create this clone:
1. Setup (and maintenance) should be minimal.
Formspree is opensource and it's code is on GitHub, but the setup process is way more than a trivial task, not to mention the requirements. Requirement 1 not met.
1. Setup (and maintenance) should be minimal.
Formspree is opensource, and its code is on GitHub, but the setup process is way more than a trivial task, not to mention the requirements. Requirement 1 not met.
1. Should allow me to send attachments.
Formspree does not allow you to send email forms with attachments (at least not at the time when this written). Requirement 2 not met.
1. Should allow me to send attachments.
Formspree does not allow you to send email forms with attachments (at least not at the time when this written). Requirement 2 not met.
## Requirements
- Node.js v6.0.0 or greater.
- A Mailgun account.
## Installation
## Installation
Clone this repo on your server or download the zip file.
Once you have the code, run `npn install --production` to download and install all the project dependencies required to run this project.
Once you have the code, run `npm install --production` to download and install all the project dependencies required to run this project.
Next step, configure your server. Open `config/config.yml` which should look like this:
Next step, configure your server. Open `config/config.json` which should look like this:
```yml
emails:
mailgun:
url:
key:
```json
{
"emails": [],
"mailgun": {
"url": "",
"key": ""
}
}
```
And update the values to match your preferences:
- **emails** is an array of email addresses. This emails are the **ONLY** emails this server will be allowed to send emails to (authorized emails)
- **emails** is an array of email addresses. These emails are the **ONLY** emails this server will be allowed to send emails to (authorized emails)
- **mailgun.url** your Mailgun API URL
- **mailgun.key** you Mailgun Domain API Key
An example of a `config.yml` file:
An example of a `config.json` file:
```yml
emails:
- sales@example.com
- contact@example.com
- support@example.com
mailgun:
url: https://api.mailgun.net/v3/example.com/messages
key: key-l0r3m1p5umd0l0r5174m37c0n53c737u
```json
{
"emails": ["sales@example.com", "contact@example.com", "support@example.com"],
"mailgun": {
"url": "https://api.mailgun.net/v3/example.com/messages",
"key": "key-l0r3m1p5umd0l0r5174m37c0n53c737u"
}
}
```
**Contact** uses [indecent.js][indecent] for loading its YAML configuration files, this allows you to override the above settings based on the value of `NODE_ENV`. You can read more about that in the [module documentation][indecent].
[indecent]: https://github.com/eruizdechavez/indecent.js
**Contact** uses `NODE_ENV` environment variable for loading its JSON configuration files, this allows you to override the above settings with other JSON files, for example, `config/production.json`.
## Server Usage
@ -73,15 +74,15 @@ To run the server, just run `node index.js`. You can use other node runners to k
Once your server is up and running, all you need to do is create an HTML form and point it to your server.
Your form's action should point to your **contact** server using a valid email address (defined in the YAML file). If the email address is not in the whitelist the email will not be sent.
Your form's action should point to your **contact** server using a valid email address (defined in the YAML file). If the email address is not in the whitelist, the email will not be sent.
### Fields
- **_from**: *Required*. This is usually the email address of the user submitting the email form. It will be used as the Form field.
- **_subject**: *Optional*. The email subject.
- **_info**: *Optional*. This text will be included in the email body before the values. Useful for providing some context in the email body.
- **_attachment**: *Optional*. A file can be attached to the email form using this for the name of an `<input type="file"/>`. When sending attachments do not forget to include `enctype="multipart/form-data"` in your form tag.
- **_next**: *Optional*. A URL to redirect the user once the form was submited succesfully.
- **_fake**: *For testing*. If `true`, the email will not be sent and instead a JSON paylod will be shown. Useful when testing the form with a REST client.
- **\_from**: _Required_. This is usually the email address of the user submitting the email form. It will be used as the Form field.
- **\_subject**: _Optional_. The email subject.
- **\_info**: _Optional_. This text will be included in the email body before the values. Useful for providing some context in the email body.
- **\_attachment**: _Optional_. A file can be attached to the email form using this for the name of an `<input type="file"/>`. When sending attachments do not forget to include `enctype="multipart/form-data"` in your form tag.
- **\_next**: _Optional_. A URL to redirect the user once the form was submitted successfully.
- **\_fake**: _For testing_. If `true`, the email will not be sent and instead a JSON payload will be shown. Useful when testing the form with a REST client.
All other form fields will be send by title casing the name. For example `<input type="text" name="first_name" />` will be displayed in the email as "First Name:"
All other form fields will be sent by title casing the name. For example `<input type="text" name="first_name" />` will be displayed in the email as "First Name:"

7
config/config.json Normal file
View file

@ -0,0 +1,7 @@
{
"emails": [],
"mailgun": {
"url": "",
"key": ""
}
}

View file

@ -1,4 +0,0 @@
emails:
mailgun:
url:
key:

View file

@ -1,31 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style media="screen">
th {
text-align: right;
font-weight: bold;
}
</style>
</head>
<body>
<p>{{info}}</p>
<table>
<tbody>
{{#data}}
<tr>
<th>
{{key}}:
</th>
<td>
{{value}}
</td>
</tr>
{{/data}}
</tbody>
</table>
<p>&nbsp;</p>
<p>&nbsp;</p>
</body>
<head>
<meta charset="utf-8">
<style media="screen">
th {
text-align: right;
font-weight: bold;
}
</style>
</head>
<body>
<p>{{info}}</p>
<table>
<tbody>
{{#data}}
<tr>
<th>
{{key}}:
</th>
<td>
{{value}}
</td>
</tr>
{{/data}}
</tbody>
</table>
<p>&nbsp;</p>
<p>&nbsp;</p>
</body>
</html>

199
index.js
View file

@ -3,27 +3,38 @@ const restify = require('restify');
const Handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
const config = require('indecent');
const corsMiddleware = require('restify-cors-middleware');
const cors = corsMiddleware({
preflightMaxAge: 5,
origins: ['*'],
});
const { chain, get, indexOf, merge, set, startCase } = require('lodash');
const knownEmails = get(config, 'emails');
let defaults;
try {
defaults = JSON.parse(fs.readFileSync(path.join(__dirname, 'config', 'config.json'), { encoding: 'utf8' }));
} catch (err) {
console.log(err.message);
defaults = {};
}
let environment;
try {
environment = JSON.parse(
fs.readFileSync(path.join(__dirname, 'config', `${process.env.NODE_ENV}.json`), { encoding: 'utf8' })
);
} catch (err) {
console.log(err.message);
environment = {};
}
const config = merge(defaults, environment);
const knownEmails = get(config, 'emails');
const MAILGUN_URL = get(config, 'mailgun.url');
const MAILGUN_API_KEY = get(config, 'mailgun.key');
const FORM_FIELDS = [
'_from',
'_subject',
'_to',
'_attachment'
];
const PRIVATE_FIELDS = [
'_fake',
'_info',
'_next'
];
const FORM_FIELDS = ['_from', '_subject', '_to', '_attachment'];
const PRIVATE_FIELDS = ['_fake', '_info', '_next'];
const htmlSource = fs.readFileSync(path.join(__dirname, 'html_template.hbs'), { encoding: 'utf8' });
const textSource = fs.readFileSync(path.join(__dirname, 'text_template.hbs'), { encoding: 'utf8' });
@ -31,105 +42,113 @@ const htmlTemplate = Handlebars.compile(htmlSource);
const textTemplate = Handlebars.compile(textSource);
var server = restify.createServer();
server.use(restify.CORS());
server.use(restify.bodyParser());
server.pre(cors.preflight);
server.use(cors.actual);
server.use(
restify.plugins.bodyParser({
mapParams: true,
})
);
server.get('/status', (req, res, next) => {
res.send('contact running');
res.send('contact running');
return next();
});
server.post('/:_to', (req, res, next) => {
const { formData, fields } = parseRequest(req);
const { formData, fields } = parseRequest(req);
if (indexOf(knownEmails, get(formData, 'to')) < 0) {
return next(new Error('Unknown email address'));
}
if (indexOf(knownEmails, get(formData, 'to')) < 0) {
return next(new Error('Unknown email address'));
}
const params = {
formData,
auth: {
user: 'api',
pass: MAILGUN_API_KEY
},
url: MAILGUN_URL,
method: 'post'
};
const params = {
formData,
auth: {
user: 'api',
pass: MAILGUN_API_KEY,
},
url: MAILGUN_URL,
method: 'post',
};
sendMail(fields, params).then(response => {
const redirect = get(fields, 'next');
if (redirect) {
res.redirect(redirect, next);
} else {
res.json(response);
}
sendMail(fields, params)
.then(response => {
const redirect = get(fields, 'next');
if (redirect) {
res.redirect(redirect, next);
} else {
res.json(response);
}
return next();
}).catch(error => {
return next(error);
});
return next();
})
.catch(error => {
return next(error);
});
});
function parseRequest (req) {
const body = get(req, 'params');
function parseRequest(req) {
const body = get(req, 'params');
const formData = chain(body)
.pick(FORM_FIELDS)
.mapKeys((value, key) => key.replace('_', ''))
.value();
const formData = chain(body)
.pick(FORM_FIELDS)
.mapKeys((value, key) => key.replace('_', ''))
.value();
const fields = chain(body)
.pick(PRIVATE_FIELDS)
.mapKeys((value, key) => key.replace('_', ''))
.value();
const fields = chain(body)
.pick(PRIVATE_FIELDS)
.mapKeys((value, key) => key.replace('_', ''))
.value();
const data = chain(body)
.omit(PRIVATE_FIELDS)
.omit(FORM_FIELDS)
.mapKeys((value, key) => startCase(key))
.map((value, key) => {
return { key, value };
})
.value();
const data = chain(body)
.omit(PRIVATE_FIELDS)
.omit(FORM_FIELDS)
.mapKeys((value, key) => startCase(key))
.map((value, key) => {
return { key, value };
})
.value();
set(fields, 'data', data);
set(formData, 'html', htmlTemplate(fields));
set(formData, 'text', textTemplate(fields));
set(fields, 'data', data);
set(formData, 'html', htmlTemplate(fields));
set(formData, 'text', textTemplate(fields));
const attachment = get(req, 'files._attachment');
if (attachment) {
const filePath = get(attachment, 'path');
const value = get(fields, 'fake') ? filePath : fs.createReadStream(filePath);
const attachment = get(req, 'files._attachment');
if (attachment) {
const filePath = get(attachment, 'path');
const value = get(fields, 'fake') ? filePath : fs.createReadStream(filePath);
set(formData, 'attachment', {
value,
options: {
filename: get(attachment, 'name'),
contentType: get(attachment, 'type')
}
});
}
set(formData, 'attachment', {
value,
options: {
filename: get(attachment, 'name'),
contentType: get(attachment, 'type'),
},
});
}
return { data, fields, formData };
return { data, fields, formData };
}
function sendMail (fields, params) {
const fake = get(fields, 'fake');
function sendMail(fields, params) {
const fake = get(fields, 'fake');
if (fake) {
return Promise.resolve(merge({ message: 'fake response' }, params));
}
if (fake) {
return Promise.resolve(merge({ message: 'fake response' }, params));
}
return new Promise((resolve, reject) => {
request(params, (error, msg, response) => {
if (error) {
return reject(error);
}
return new Promise((resolve, reject) => {
request(params, (error, msg, response) => {
if (error) {
return reject(error);
}
return resolve(JSON.parse(response));
});
});
return resolve(JSON.parse(response));
});
});
}
server.listen(8081, () => {
console.log(`${server.name} listening at ${server.url}`);
console.log(`${server.name} listening at ${server.url}`);
});

2654
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,29 +1,29 @@
{
"name": "contact",
"version": "1.0.1",
"description": "A basic clone of formspree.io for internal use",
"main": "index.js",
"homepage": "https://github.com/eruizdechavez/contact",
"bugs": "https://github.com/eruizdechavez/contact/issues",
"license": "MIT",
"author": {
"name": "Erick Ruiz de Chavez",
"email": "eruizdechavez@fastmail.com",
"url": "https://github.com/eruizdechavez"
},
"repository": "eruizdechavez/contact",
"engines": {
"node": ">=6.0.0"
},
"private": true,
"dependencies": {
"handlebars": "4.0.5",
"indecent": "1.0.1",
"lodash": "4.13.1",
"request": "2.72.0",
"restify": "4.1.1"
},
"devDependencies": {
"semistandard": "*"
}
"name": "contact",
"version": "1.0.1",
"description": "A basic clone of formspree.io for internal use",
"main": "index.js",
"homepage": "https://github.com/eruizdechavez/contact",
"bugs": "https://github.com/eruizdechavez/contact/issues",
"license": "MIT",
"author": {
"name": "Erick Ruiz de Chavez",
"email": "eruizdechavez@fastmail.com",
"url": "https://github.com/eruizdechavez"
},
"repository": "eruizdechavez/contact",
"engines": {
"node": ">=6.0.0"
},
"private": true,
"dependencies": {
"handlebars": "4.1.2",
"lodash": "4.17.11",
"request": "2.88.0",
"restify": "8.3.2",
"restify-cors-middleware": "1.1.1"
},
"devDependencies": {
"semistandard": "*"
}
}