Compare commits

..

1 commit

Author SHA1 Message Date
Erick Ruiz de Chavez
2de6589222
Started working on a PHP refactor for this service 2018-04-04 15:35:36 -04:00
16 changed files with 355 additions and 2892 deletions

View file

@ -1,2 +0,0 @@
node_modules
vendor

5
.gitignore vendored
View file

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

3
.jshintrc Normal file
View file

@ -0,0 +1,3 @@
{
"esversion": 6
}

View file

@ -1,5 +0,0 @@
FROM node:14-alpine
WORKDIR /app
COPY . .
RUN npm ci
ENTRYPOINT [ "node", "index.js" ]

View file

@ -6,65 +6,64 @@ A basic node server for sending email forms.
```html ```html
<form method="post" action="http://example.com/contact@example.com"> <form method="post" action="http://example.com/contact@example.com">
<input type="hidden" name="_subject" value="This is a test form" /> <input type="hidden" name="_subject" value="This is a test form" />
<input type="email" name="_from" /> <input type="email" name="_from" />
<input type="text" name="first_name" /> <input type="text" name="first_name" />
<input type="text" name="last_name" /> <input type="text" name="last_name" />
<textarea name="comments"></textarea> <textarea name="comments"></textarea>
<input type="submit" name="Submit" /> <input type="submit" name="Submit" />
</form> </form>
``` ```
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: 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:
1. Setup (and maintenance) should be minimal. 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. 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. Should allow me to send attachments. 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. 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 ## Requirements
- Node.js v6.0.0 or greater. - Node.js v6.0.0 or greater.
- A Mailgun account. - A Mailgun account.
## Installation ## Installation
Clone this repo on your server or download the zip file. Clone this repo on your server or download the zip file.
Once you have the code, run `npm install --production` to download and install all the project dependencies required to run this project. Once you have the code, run `npn install --production` to download and install all the project dependencies required to run this project.
Next step, configure your server. Open `config/config.json` which should look like this: Next step, configure your server. Open `config/config.yml` which should look like this:
```json ```yml
{ emails:
"emails": [], mailgun:
"mailgun": { url:
"url": "", key:
"key": ""
}
}
``` ```
And update the values to match your preferences: And update the values to match your preferences:
- **emails** is an array of email addresses. These emails are the **ONLY** emails this server will be allowed to send emails to (authorized emails) - **emails** is an array of email addresses. This emails are the **ONLY** emails this server will be allowed to send emails to (authorized emails)
- **mailgun.url** your Mailgun API URL - **mailgun.url** your Mailgun API URL
- **mailgun.key** you Mailgun Domain API Key - **mailgun.key** you Mailgun Domain API Key
An example of a `config.json` file: An example of a `config.yml` file:
```json ```yml
{ emails:
"emails": ["sales@example.com", "contact@example.com", "support@example.com"], - sales@example.com
"mailgun": { - contact@example.com
"url": "https://api.mailgun.net/v3/example.com/messages", - support@example.com
"key": "key-l0r3m1p5umd0l0r5174m37c0n53c737u" mailgun:
} url: https://api.mailgun.net/v3/example.com/messages
} key: key-l0r3m1p5umd0l0r5174m37c0n53c737u
``` ```
**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`. **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
## Server Usage ## Server Usage
@ -74,15 +73,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. 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 ### Fields
- **\_from**: _Required_. This is usually the email address of the user submitting the email form. It will be used as the Form field. - **_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. - **_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. - **_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. - **_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. - **_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 payload will be shown. Useful when testing the form with a REST client. - **_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.
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:" 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:"

6
composer.json Normal file
View file

@ -0,0 +1,6 @@
{
"require": {
"rmccue/requests": "^1.7",
"xamin/handlebars.php": "^0.10.4"
}
}

109
composer.lock generated Normal file
View file

@ -0,0 +1,109 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "a54bd6daf1501e5092742fd20e496fec",
"packages": [
{
"name": "rmccue/requests",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/rmccue/Requests.git",
"reference": "87932f52ffad70504d93f04f15690cf16a089546"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rmccue/Requests/zipball/87932f52ffad70504d93f04f15690cf16a089546",
"reference": "87932f52ffad70504d93f04f15690cf16a089546",
"shasum": ""
},
"require": {
"php": ">=5.2"
},
"require-dev": {
"requests/test-server": "dev-master"
},
"type": "library",
"autoload": {
"psr-0": {
"Requests": "library/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"ISC"
],
"authors": [
{
"name": "Ryan McCue",
"homepage": "http://ryanmccue.info"
}
],
"description": "A HTTP library written in PHP, for human beings.",
"homepage": "http://github.com/rmccue/Requests",
"keywords": [
"curl",
"fsockopen",
"http",
"idna",
"ipv6",
"iri",
"sockets"
],
"time": "2016-10-13T00:11:37+00:00"
},
{
"name": "xamin/handlebars.php",
"version": "v0.10.4",
"source": {
"type": "git",
"url": "https://github.com/XaminProject/handlebars.php.git",
"reference": "b85cee07eae96db0e1eec224ca90f5ce1e4d857a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/XaminProject/handlebars.php/zipball/b85cee07eae96db0e1eec224ca90f5ce1e4d857a",
"reference": "b85cee07eae96db0e1eec224ca90f5ce1e4d857a",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "~4.4",
"squizlabs/php_codesniffer": "~1.5"
},
"type": "library",
"autoload": {
"psr-0": {
"Handlebars": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "fzerorubigd",
"email": "fzerorubigd@gmail.com"
},
{
"name": "Behrooz Shabani (everplays)",
"email": "everplays@gmail.com"
}
],
"description": "Handlebars processor for php",
"homepage": "https://github.com/XaminProject/handlebars.php",
"time": "2016-12-12T13:51:02+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

BIN
composer.phar Executable file

Binary file not shown.

View file

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

4
config/config.yml Normal file
View file

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

View file

@ -1,13 +0,0 @@
services:
node:
build:
context: .
restart: always
ports:
- 8081:8081
networks:
- palmiers
networks:
palmiers:
external: true

View file

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

199
index.js
View file

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

46
index.php Normal file
View file

@ -0,0 +1,46 @@
<?php
require_once './vendor/autoload.php';
// phpinfo();
// $response = Requests::get('https://api.github.com/events');
// var_dump(json_decode($response->body));
// var_dump($_GET);
// var_dump($_POST);
$method = $_SERVER['REQUEST_METHOD'];
// if ($method == 'GET') {
// echo 'contact running';
// exit;
// }
const FORM_FIELDS = [
'_from',
'_subject',
'_to',
'_attachment',
];
const PRIVATE_FIELDS = [
'_fake',
'_info',
'_next',
];
$formData = [
'from' => $_POST['_from'],
'subject' => $_POST['_subject'],
'to' => $_POST['_to'],
'attachment' => $_POST['_attachment'],
];
$fields = [
'fake' => $_POST['_fake'],
'info' => $_POST['_info'],
'next' => $_POST['_next'],
];
var_dump($formData, $fields);
echo 'DONE';

2654
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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