summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xapp.js2
-rw-r--r--config/domain-example.js3
-rwxr-xr-xpackage-lock.json20
-rwxr-xr-xroutes.js370
-rw-r--r--views/emails/addeventattendee.handlebars7
-rw-r--r--views/emails/addeventcomment.handlebars7
-rw-r--r--views/emails/createevent.handlebars19
-rw-r--r--views/emails/createeventgroup.handlebars27
-rw-r--r--views/emails/deleteevent.handlebars4
-rw-r--r--views/emails/editevent.handlebars8
-rw-r--r--views/emails/removeeventattendee.handlebars4
-rw-r--r--views/emails/unattendevent.handlebars8
-rw-r--r--views/layouts/email.handlebars126
13 files changed, 426 insertions, 179 deletions
diff --git a/app.js b/app.js
index ab4eb56..c6e0647 100755
--- a/app.js
+++ b/app.js
@@ -19,6 +19,7 @@ const app = express();
hbsInstance = hbs.create({
defaultLayout: 'main',
partialsDir: ['views/partials/'],
+ layoutsDir: 'views/layouts/',
helpers: {
plural: function(number, text) {
var singular = number === 1;
@@ -39,6 +40,7 @@ hbsInstance = hbs.create({
});
app.engine('handlebars', hbsInstance.engine);
app.set('view engine', 'handlebars');
+app.set('hbsInstance', hbsInstance);
// Static files //
diff --git a/config/domain-example.js b/config/domain-example.js
index 2f22fe7..3b77197 100644
--- a/config/domain-example.js
+++ b/config/domain-example.js
@@ -2,5 +2,6 @@ module.exports = {
// Your domain goes here. If there is a port it should be 'domain:port', but otherwise just 'domain'
'domain' : 'localhost:3000' ,
'port': '3000',
- 'email': 'contact@example.com'
+ 'email': 'contact@example.com',
+ 'sitename': 'gathio'
};
diff --git a/package-lock.json b/package-lock.json
index e9b5b66..10d0d73 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -1535,9 +1535,9 @@
}
},
"commander": {
- "version": "2.20.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
- "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"optional": true
},
"component-emitter": {
@@ -2732,9 +2732,9 @@
}
},
"handlebars": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
- "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
+ "version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
+ "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
"requires": {
"neo-async": "^2.6.0",
"optimist": "^0.6.1",
@@ -5276,12 +5276,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"uglify-js": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
- "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
+ "version": "3.7.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.3.tgz",
+ "integrity": "sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg==",
"optional": true,
"requires": {
- "commander": "~2.20.0",
+ "commander": "~2.20.3",
"source-map": "~0.6.1"
},
"dependencies": {
diff --git a/routes.js b/routes.js
index 2475904..34dc1f8 100755
--- a/routes.js
+++ b/routes.js
@@ -26,6 +26,7 @@ const request = require('request');
const domain = require('./config/domain.js').domain;
const contactEmail = require('./config/domain.js').email;
+const siteName = require('./config/domain.js').sitename
var sanitizeHtml = require('sanitize-html');
// Extra marked renderer (used to render plaintext event description for page metadata)
@@ -610,13 +611,23 @@ router.get('/:eventID', (req, res) => {
}
}
}
- let eventAttendees = event.attendees.sort((a,b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
- let spotsRemaining, noMoreSpots;
- if (event.maxAttendees) {
- spotsRemaining = event.maxAttendees - eventAttendees.length;
- if (spotsRemaining <= 0) {
- noMoreSpots = true;
- }
+ let eventAttendees = event.attendees.sort((a,b) => (a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0))
+ .map(el => {
+ if (!el.id) {
+ el.id = el._id;
+ }
+ return el;
+ })
+ .filter((obj, pos, arr) => {
+ return arr.map(mapObj => mapObj.id).indexOf(obj.id) === pos;
+ });
+
+ let spotsRemaining, noMoreSpots;
+ if (event.maxAttendees) {
+ spotsRemaining = event.maxAttendees - eventAttendees.length;
+ if (spotsRemaining <= 0) {
+ noMoreSpots = true;
+ }
}
let metadata = {
title: event.name,
@@ -887,30 +898,28 @@ router.post('/newevent', async (req, res) => {
addToLog("createEvent", "success", "Event " + eventID + "created");
// Send email with edit link
if (sendEmails) {
- const msg = {
- to: req.body.creatorEmail,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-00330b8278ab463e9f88c16566487d97',
- dynamic_template_data: {
- subject: 'gathio: ' + req.body.eventName,
- eventID: eventID,
- editToken: editToken
- },
- };
- sgMail.send(msg).catch(e => {
- console.error(e.toString());
- res.status(500).end();
- });
+ req.app.get('hbsInstance').renderView('./views/emails/createevent.handlebars', {eventID, editToken, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) {
+ const msg = {
+ to: req.body.creatorEmail,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: ${req.body.eventName}`,
+ html,
+ };
+ sgMail.send(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
res.writeHead(302, {
'Location': '/' + eventID + '?e=' + editToken
});
res.end();
})
- .catch((err) => { res.send('Database error, please try again :( - ' + err); addToLog("createEvent", "error", "Attempt to create event failed with error: " + err);});
+ .catch((err) => { res.status(500).send('Database error, please try again :( - ' + err); addToLog("createEvent", "error", "Attempt to create event failed with error: " + err);});
});
router.post('/importevent', (req, res) => {
@@ -957,23 +966,21 @@ router.post('/importevent', (req, res) => {
addToLog("createEvent", "success", "Event " + eventID + " created");
// Send email with edit link
if (sendEmails) {
- const msg = {
- to: creatorEmail,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-00330b8278ab463e9f88c16566487d97',
- dynamic_template_data: {
- subject: 'gathio: ' + req.body.eventName,
- eventID: eventID,
- editToken: editToken
- },
- };
- sgMail.send(msg).catch(e => {
- console.error(e.toString());
- res.status(500).end();
- });
+ req.app.get('hbsInstance').renderView('./views/emails/createevent.handlebars', {eventID, editToken, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) {
+ const msg = {
+ to: req.body.creatorEmail,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: ${req.body.eventName}`,
+ html,
+ };
+ sgMail.send(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
res.writeHead(302, {
'Location': '/' + eventID + '?e=' + editToken
@@ -1019,23 +1026,21 @@ router.post('/neweventgroup', (req, res) => {
addToLog("createEventGroup", "success", "Event group " + eventGroupID + " created");
// Send email with edit link
if (sendEmails) {
- const msg = {
- to: req.body.creatorEmail,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-4c5ddcb34ac44ec5b2313c6da4e405f3',
- dynamic_template_data: {
- subject: 'gathio: ' + req.body.eventGroupName,
- eventGroupID: eventGroupID,
- editToken: editToken
- },
- };
- sgMail.send(msg).catch(e => {
- console.error(e.toString());
- res.status(500).end();
- });
+ req.app.get('hbsInstance').renderView('./views/emails/createeventgroup.handlebars', {eventGroupID, editToken, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) {
+ const msg = {
+ to: req.body.creatorEmail,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: ${req.body.eventGroupName}`,
+ html,
+ };
+ sgMail.send(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
res.writeHead(302, {
'Location': '/group/' + eventGroupID + '?e=' + editToken
@@ -1168,21 +1173,21 @@ router.post('/editevent/:eventID/:editToken', (req, res) => {
attendeeEmails = ids;
if (!error && attendeeEmails !== ""){
console.log("Sending emails to: " + attendeeEmails);
- const msg = {
- to: attendeeEmails,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-e21f3ca49d82476b94ddd8892c72a162',
- dynamic_template_data: {
- subject: 'gathio: Event edited',
- actionType: 'edited',
- eventExists: true,
- eventID: req.params.eventID
- }
- }
- sgMail.sendMultiple(msg);
+ req.app.get('hbsInstance').renderView('./views/emails/editevent.handlebars', {diffText, eventID: req.params.eventID, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) {
+ const msg = {
+ to: attendeeEmails,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: ${event.name} was just edited`,
+ html,
+ };
+ sgMail.sendMultiple(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
else {
console.log("Nothing to send!");
@@ -1294,6 +1299,7 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => {
if (event.editToken === submittedEditToken) {
// Token matches
+ let eventImage;
if (event.image){
eventImage = event.image;
}
@@ -1304,21 +1310,21 @@ router.post('/deleteevent/:eventID/:editToken', (req, res) => {
attendeeEmails = ids;
if (!error){
console.log("Sending emails to: " + attendeeEmails);
- const msg = {
- to: attendeeEmails,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-e21f3ca49d82476b94ddd8892c72a162',
- dynamic_template_data: {
- subject: 'gathio: Event "' + event.name + '" deleted',
- actionType: 'deleted',
- eventExists: false,
- eventID: req.params.eventID
- }
- }
- sgMail.sendMultiple(msg);
+ req.app.get('hbsInstance').renderView('./views/emails/deleteevent.handlebars', {siteName, domain, eventName: event.name, cache: true, layout: 'email.handlebars'}, function(err, html) {
+ const msg = {
+ to: attendeeEmails,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: ${event.name} was deleted`,
+ html,
+ };
+ sgMail.sendMultiple(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
else {
console.log("Nothing to send!");
@@ -1432,22 +1438,23 @@ router.post('/attendevent/:eventID', (req, res) => {
addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID);
if (sendEmails) {
if (req.body.attendeeEmail){
- const msg = {
- to: req.body.attendeeEmail,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-977612474bba49c48b58e269f04f927c',
- dynamic_template_data: {
- subject: 'gathio: ' + event.name,
- eventID: req.params.eventID
- },
- };
- sgMail.send(msg);
+ req.app.get('hbsInstance').renderView('./views/emails/addeventattendee.handlebars', {eventID: req.params.eventID, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) {
+ const msg = {
+ to: req.body.attendeeEmail,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: You're RSVPed to ${event.name}`,
+ html,
+ };
+ sgMail.send(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
}
-
res.writeHead(302, {
'Location': '/' + req.params.eventID
});
@@ -1464,22 +1471,23 @@ router.post('/unattendevent/:eventID', (req, res) => {
)
.then(response => {
console.log(response)
- addToLog("removeEventAttendee", "success", "Attendee removed from event " + req.params.eventID);
+ addToLog("unattendEvent", "success", "Attendee removed self from event " + req.params.eventID);
if (sendEmails) {
if (req.body.attendeeEmail){
- const msg = {
- to: req.body.attendeeEmail,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-56c97755d6394c23be212fef934b0f1f',
- dynamic_template_data: {
- subject: 'gathio: You have been removed from an event',
- eventID: req.params.eventID
- },
- };
- sgMail.send(msg);
+ req.app.get('hbsInstance').renderView('./views/emails/unattendevent.handlebars', {eventID: req.params.eventID, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { const msg = {
+ to: req.body.attendeeEmail,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: You have been removed from an event`,
+ html,
+ };
+ sgMail.send(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
}
res.writeHead(302, {
@@ -1501,20 +1509,22 @@ router.post('/removeattendee/:eventID/:attendeeID', (req, res) => {
console.log(response)
addToLog("removeEventAttendee", "success", "Attendee removed by admin from event " + req.params.eventID);
if (sendEmails) {
+ // currently this is never called because we don't have the email address
if (req.body.attendeeEmail){
- const msg = {
- to: req.body.attendeeEmail,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-f8ee9e1e2c8a48e3a329d1630d0d371f',
- dynamic_template_data: {
- subject: 'gathio: You have been removed from an event',
- eventID: req.params.eventID
- },
- };
- sgMail.send(msg);
+ req.app.get('hbsInstance').renderView('./views/emails/removeeventattendee.handlebars', {eventName: req.params.eventName, siteName, domain, cache: true, layout: 'email.handlebars'}, function(err, html) { const msg = {
+ to: req.body.attendeeEmail,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: You have been removed from an event`,
+ html,
+ };
+ sgMail.send(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
}
res.writeHead(302, {
@@ -1549,20 +1559,21 @@ router.post('/post/comment/:eventID', (req, res) => {
attendeeEmails = ids;
if (!error){
console.log("Sending emails to: " + attendeeEmails);
- const msg = {
- to: attendeeEmails,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-756d078561e047aba307155f02b6686d',
- dynamic_template_data: {
- subject: 'gathio: New comment in ' + event.name,
- commentAuthor: req.body.commentAuthor,
- eventID: req.params.eventID
- }
- }
- sgMail.sendMultiple(msg);
+ req.app.get('hbsInstance').renderView('./views/emails/addeventcomment.handlebars', {siteName, domain, eventID: req.params.eventID, commentAuthor: req.body.commentAuthor, cache: true, layout: 'email.handlebars'}, function(err, html) {
+ const msg = {
+ to: attendeeEmails,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: New comment in ${event.name}`,
+ html,
+ };
+ sgMail.sendMultiple(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
else {
console.log("Nothing to send!");
@@ -1601,20 +1612,21 @@ router.post('/post/reply/:eventID/:commentID', (req, res) => {
attendeeEmails = ids;
if (!error){
console.log("Sending emails to: " + attendeeEmails);
- const msg = {
- to: attendeeEmails,
- from: {
- name: 'Gathio',
- email: contactEmail,
- },
- templateId: 'd-756d078561e047aba307155f02b6686d',
- dynamic_template_data: {
- subject: 'gathio: New comment in ' + event.name,
- commentAuthor: req.body.commentAuthor,
- eventID: req.params.eventID
- }
- }
- sgMail.sendMultiple(msg);
+ req.app.get('hbsInstance').renderView('./views/emails/addeventcomment.handlebars', {siteName, domain, eventID: req.params.eventID, commentAuthor: req.body.replyAuthor, cache: true, layout: 'email.handlebars'}, function(err, html) {
+ const msg = {
+ to: attendeeEmails,
+ from: {
+ name: siteName,
+ email: contactEmail,
+ },
+ subject: `${siteName}: New comment in ${event.name}`,
+ html,
+ };
+ sgMail.sendMultiple(msg).catch(e => {
+ console.error(e.toString());
+ res.status(500).end();
+ });
+ });
}
else {
console.log("Nothing to send!");
@@ -1673,13 +1685,23 @@ router.post('/activitypub/inbox', (req, res) => {
}, {});
// get the actor
+ // TODO if this is a Delete for an Actor this won't work
request({
url: signature_header.keyId,
headers: {
'Accept': 'application/activity+json',
'Content-Type': 'application/activity+json'
}}, function (error, response, actor) {
- publicKey = JSON.parse(actor).publicKey.publicKeyPem;
+ let publicKey = '';
+
+ try {
+ if (JSON.parse(actor).publicKey) {
+ publicKey = JSON.parse(actor).publicKey.publicKeyPem;
+ }
+ }
+ catch(err) {
+ return res.status(500).send('Actor could not be parsed' + err);
+ }
let comparison_string = signature_header.headers.split(' ').map(header => {
if (header === '(request-target)') {
@@ -1694,10 +1716,14 @@ router.post('/activitypub/inbox', (req, res) => {
verifier.update(comparison_string, 'ascii')
const publicKeyBuf = new Buffer(publicKey, 'ascii')
const signatureBuf = new Buffer(signature_header.signature, 'base64')
- const result = verifier.verify(publicKeyBuf, signatureBuf)
- console.log('VALIDATE RESULT:', result)
+ try {
+ const result = verifier.verify(publicKeyBuf, signatureBuf)
+ }
+ catch(err) {
+ return res.status(401).send('Signature could not be verified: ' + err);
+ }
if (!result) {
- res.status(401).send('Signature could not be verified.');
+ return res.status(401).send('Signature could not be verified.');
}
else {
processInbox(req, res);
@@ -1863,9 +1889,13 @@ function processInbox(req, res) {
.then(() => {
addToLog("addEventAttendee", "success", "Attendee added to event " + req.params.eventID);
console.log('added attendee', attendeeName)
- res.send(200);
+ return res.sendStatus(200);
})
- .catch((err) => { res.send('Database error, please try again :('); addToLog("addEventAttendee", "error", "Attempt to add attendee to event " + req.params.eventID + " failed with error: " + err); });
+ .catch((err) => { addToLog("addEventAttendee", "error", "Attempt to add attendee to event " + req.params.eventID + " failed with error: " + err); return res.status(500).send('Database error, please try again :('); });
+ }
+ else {
+ // it's a duplicate and this person is already rsvped so just say OK
+ return res.status(200).send("Attendee is already registered.");
}
});
}
@@ -1874,6 +1904,7 @@ function processInbox(req, res) {
});
}
if (req.body && req.body.type === 'Delete') {
+ // TODO: only do this if it's a delete for a Note
// figure out if we have a matching comment by id
const deleteObjectId = req.body.object.id;
// find all events with comments from the author
@@ -1920,7 +1951,7 @@ function processInbox(req, res) {
if (req.body && req.body.type === 'Create' && req.body.object && req.body.object.type === 'Note' && req.body.object.to) {
console.log('create note!!')
// figure out what this is in reply to -- it should be addressed specifically to us
- let {name, attributedTo, inReplyTo, to, cc} = req.body.object;
+ let {attributedTo, inReplyTo, to, cc} = req.body.object;
// normalize cc into an array
if (typeof cc === 'string') {
cc = [cc];
@@ -1983,8 +2014,11 @@ function processInbox(req, res) {
});
} // end ourevent
} // end public message
- // if it's not a public message, let them know that we only support public messages right now
- else {
+ // if it's not a public message, AND it's not a vote let them know that we only support public messages right now
+ else if (req.body.object.name !== 'Yes') {
+ if (!cc) {
+ cc = [];
+ }
// figure out which event(s) of ours it was addressing
ourEvents = cc.concat(to).filter(el => el.includes(`https://${domain}/`))
.map(el => el.replace(`https://${domain}/`,''));
@@ -2007,7 +2041,7 @@ function processInbox(req, res) {
"content": `<span class=\"h-card\"><a href="${req.body.actor}" class="u-url mention">@<span>${name}</span></a></span> Sorry, this service only supports posting public messages to the event page. Try contacting the event organizer directly if you need to have a private conversation.`,
"tag":[{"type":"Mention","href":req.body.actor,"name":name}]
}
- res.send(200);
+ res.sendStatus(200);
sendDirectMessage(jsonObject, req.body.actor, eventID);
}
);
diff --git a/views/emails/addeventattendee.handlebars b/views/emails/addeventattendee.handlebars
new file mode 100644
index 0000000..f49c790
--- /dev/null
+++ b/views/emails/addeventattendee.handlebars
@@ -0,0 +1,7 @@
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You just marked yourself as attending an event on {{siteName}}. Thank you! We'll send you another email if there are any updates to the event. Your email will be automatically removed from the database once the event finishes.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Follow this link to open the event page any time: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
+<hr/>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.</p>
diff --git a/views/emails/addeventcomment.handlebars b/views/emails/addeventcomment.handlebars
new file mode 100644
index 0000000..8ab7ec1
--- /dev/null
+++ b/views/emails/addeventcomment.handlebars
@@ -0,0 +1,7 @@
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>{{commentAuthor}}</strong> has just posted a comment on an event you're attending on {{siteName}}.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click here to see the comment: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
+<hr/>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.</p>
diff --git a/views/emails/createevent.handlebars b/views/emails/createevent.handlebars
new file mode 100644
index 0000000..030ee58
--- /dev/null
+++ b/views/emails/createevent.handlebars
@@ -0,0 +1,19 @@
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Your event has been created!</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Use this link to share it with people: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click this button to edit your event. <strong>DO NOT SHARE THIS</strong>, as anyone with this link can edit your event.</p>
+
+<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
+ <tbody>
+ <tr>
+ <td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;">
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
+ <tbody>
+ <tr>
+ <td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #28a745; border-radius: 5px; text-align: center;"> <a href="https://{{domain}}/{{eventID}}?e={{editToken}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #28a745; border: solid 1px #28a745; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #28a745;">Edit Your Event</a> </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+</table>
diff --git a/views/emails/createeventgroup.handlebars b/views/emails/createeventgroup.handlebars
new file mode 100644
index 0000000..3f03345
--- /dev/null
+++ b/views/emails/createeventgroup.handlebars
@@ -0,0 +1,27 @@
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You just created a new event group on {{siteName}}! Thanks a bunch - we're delighted to have you.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You can edit your event group by clicking the button below, or just following this link: <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}">https://{{domain}}/{{eventGroupID}}?e={{editToken}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">To add events to this group (whether brand new events or ones you've already made), click the 'This event is part of an event group' checkbox. You will need to copy the following two codes into the dark grey box which opens:</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Event group ID</strong>: {{eventGroupID}}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Event group secret editing code</strong>: {{editToken}}</p>
+<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
+ <tbody>
+ <tr>
+ <td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;">
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
+ <tbody>
+ <tr>
+ <td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #28a745; border-radius: 5px; text-align: center;"> <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #28a745; border: solid 1px #28a745; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #28a745;">Edit event group</a> </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+</table>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">To let others know about your event group, send them this link: <a href="https://{{domain}}/group/{{eventGroupID}}?e={{editToken}}">https://{{domain}}/{{eventGroupID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">And that's it - have a great day!</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
+<hr/>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't make an event group on {{siteName}}, someone may have accidentally typed your email instead of theirs when they were making an event. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes, and if you're still worried, just click on the edit link above and delete that event group, which removes your email from the system as well.</p>
diff --git a/views/emails/deleteevent.handlebars b/views/emails/deleteevent.handlebars
new file mode 100644
index 0000000..5a3670c
--- /dev/null
+++ b/views/emails/deleteevent.handlebars
@@ -0,0 +1,4 @@
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">The {{eventName}} event you're attending on {{siteName}} was just deleted by its creator.</p>
+<hr/>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - that event, and your email, is deleted from the system now.</p>
diff --git a/views/emails/editevent.handlebars b/views/emails/editevent.handlebars
new file mode 100644
index 0000000..ddb9885
--- /dev/null
+++ b/views/emails/editevent.handlebars
@@ -0,0 +1,8 @@
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">An event you're attending on {{siteName}} has just been edited.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{{diffText}}}</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click here to see the event: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
+<hr/>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - there isn't anything you need to do. Your email will be removed from the system when the event finishes.</p>
diff --git a/views/emails/removeeventattendee.handlebars b/views/emails/removeeventattendee.handlebars
new file mode 100644
index 0000000..66ca858
--- /dev/null
+++ b/views/emails/removeeventattendee.handlebars
@@ -0,0 +1,4 @@
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You have been removed from the event {{eventName}} on {{siteName}} by the organizer of the event.</p>
+<hr/>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs. Don't worry - you won't receive any more of these emails for this event, and your email has been removed from the database.</p>
diff --git a/views/emails/unattendevent.handlebars b/views/emails/unattendevent.handlebars
new file mode 100644
index 0000000..62dac8a
--- /dev/null
+++ b/views/emails/unattendevent.handlebars
@@ -0,0 +1,8 @@
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You just removed yourself from an event on {{siteName}}. You will no longer receive update emails for this event.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mean to do this, someone else who knows your email removed you from the event.</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Follow this link to open the event page any time: <a href="https://{{domain}}/{{eventID}}">https://{{domain}}/{{eventID}}</a></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Love,</p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{siteName}}</p>
+<hr/>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;"><strong>Hold up - I have no idea what this email is about!</strong></p>
+<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you didn't mark yourself as attending an event on {{siteName}}, someone may have accidentally typed your email instead of theirs, then removed it. Don't worry - you won't receive any more emails linked to this event.</p>
diff --git a/views/layouts/email.handlebars b/views/layouts/email.handlebars
new file mode 100644
index 0000000..6158ddb
--- /dev/null
+++ b/views/layouts/email.handlebars
@@ -0,0 +1,126 @@
+<!doctype html>
+<html>
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>{{ subject }}</title>
+ <style>
+ /* -------------------------------------
+ INLINED WITH htmlemail.io/inline
+ ------------------------------------- */
+ /* -------------------------------------
+ RESPONSIVE AND MOBILE FRIENDLY STYLES
+ ------------------------------------- */
+ @media only screen and (max-width: 620px) {
+ table[class=body] h1 {
+ font-size: 28px !important;
+ margin-bottom: 10px !important;
+ }
+ table[class=body] p,
+ table[class=body] ul,
+ table[class=body] ol,
+ table[class=body] td,
+ table[class=body] span,
+ table[class=body] a {
+ font-size: 16px !important;
+ }
+ table[class=body] .wrapper,
+ table[class=body] .article {
+ padding: 10px !important;
+ }
+ table[class=body] .content {
+ padding: 0 !important;
+ }
+ table[class=body] .container {
+ padding: 0 !important;
+ width: 100% !important;
+ }
+ table[class=body] .main {
+ border-left-width: 0 !important;
+ border-radius: 0 !important;
+ border-right-width: 0 !important;
+ }
+ table[class=body] .btn table {
+ width: 100% !important;
+ }
+ table[class=body] .btn a {
+ width: 100% !important;
+ }
+ table[class=body] .img-responsive {
+ height: auto !important;
+ max-width: 100% !important;
+ width: auto !important;
+ }
+ }
+ /* -------------------------------------
+ PRESERVE THESE STYLES IN THE HEAD
+ ------------------------------------- */
+ @media all {
+ .ExternalClass {
+ width: 100%;
+ }
+ .ExternalClass,
+ .ExternalClass p,
+ .ExternalClass span,
+ .ExternalClass font,
+ .ExternalClass td,
+ .ExternalClass div {
+ line-height: 100%;
+ }
+ .apple-link a {
+ color: inherit !important;
+ font-family: inherit !important;
+ font-size: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+ text-decoration: none !important;
+ }
+ #MessageViewBody a {
+ color: inherit;
+ text-decoration: none;
+ font-size: inherit;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: inherit;
+ }
+ .btn-primary table td:hover {
+ background-color: #34495e !important;
+ }
+ .btn-primary a:hover {
+ background-color: #34495e !important;
+ border-color: #34495e !important;
+ }
+ }
+ </style>
+</head>
+<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
+ <table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
+ <tr>
+ <td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
+ <td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
+ <div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
+ <table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
+
+ <!-- START MAIN CONTENT AREA -->
+ <tr>
+ <td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
+ <table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
+ <tr>
+ <td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
+ {{{ body }}}
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <!-- END MAIN CONTENT AREA -->
+ </table>
+ <!-- END CENTERED WHITE AREA -->
+ </div>
+ </td>
+ <td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
+ </tr>
+ </table>
+</body>
+</html>