diff --git a/ep.json b/ep.json index 2f7fe5a..8f69f6c 100644 --- a/ep.json +++ b/ep.json @@ -8,7 +8,8 @@ "eejsBlock_scripts": "ep_email_notifications/client", "eejsBlock_mySettings": "ep_email_notifications/client:eejsBlock_mySettings", "eejsBlock_styles": "ep_email_notifications/client:eejsBlock_styles", - "clientVars": "ep_email_notifications/client:clientVars" + "clientVars": "ep_email_notifications/client:clientVars", + "expressCreateServer" : "ep_email_notifications/index:registerRoute" }, "client_hooks": { "postAceInit":"ep_email_notifications/static/js/ep_email:postAceInit", diff --git a/handleMessage.js b/handleMessage.js index 34b6c71..0bfd1e7 100644 --- a/handleMessage.js +++ b/handleMessage.js @@ -1,9 +1,18 @@ var db = require('../../src/node/db/DB').db, API = require('../../src/node/db/API.js'), async = require('../../src/node_modules/async'), + email = require('emailjs'), +randomString = require('../../src/static/js/pad_utils').randomString; settings = require('../../src/node/utils/Settings'); var pluginSettings = settings.ep_email_notifications; +var fromName = pluginSettings.fromName || "Etherpad"; +var fromEmail = pluginSettings.fromEmail || "pad@etherpad.org"; +var urlToPads = pluginSettings.urlToPads || "http://beta.etherpad.org/p/"; +var emailServer = pluginSettings.emailServer || {host:"127.0.0.1"}; + +// Connect to the email server -- This might not be the ideal place to connect but it stops us having lots of connections +var server = email.server.connect(emailServer); // When a new message comes in from the client - FML this is ugly exports.handleMessage = function(hook_name, context, callback){ @@ -138,20 +147,16 @@ exports.handleMessage = function(hook_name, context, callback){ */ exports.subscriptionEmail = function (context, email, emailFound, userInfo, padId, callback) { var validatesAsEmail = exports.checkEmailValidation(email); + var subscribeId = randomString(25); if(emailFound == false && validatesAsEmail){ // Subscription -> Go for it console.debug ("Subscription: Wrote to the database and sent client a positive response ",context.message.data.userInfo.email); - exports.setAuthorEmail( - userInfo.userId, - userInfo, - callback - ); - exports.setAuthorEmailRegistered( userInfo, userInfo.userId, + subscribeId, padId ); @@ -164,6 +169,20 @@ exports.subscriptionEmail = function (context, email, emailFound, userInfo, padI } } }); + + // Send mail to user with the link for validation + server.send( + { + text: "Please click on this link in order to validate your subscription to the pad " + padId + "\n" + urlToPads+padId + "/subscribe=" + subscribeId, + from: fromName+ "<"+fromEmail+">", + to: userInfo.email, + subject: "Email subscription confirmation for pad "+padId + }, + function(err, message) { + console.log(err || message); + } + ); + } else if (!validatesAsEmail) { // Subscription -> failed coz mail malformed.. y'know in general fuck em! console.warn("Dropped email subscription due to malformed email address"); @@ -198,18 +217,16 @@ exports.subscriptionEmail = function (context, email, emailFound, userInfo, padI * UnsUbscription process */ exports.unsubscriptionEmail = function (context, emailFound, userInfo, padId) { + var unsubscribeId = randomString(25); + if(emailFound == true) { // Unsubscription -> Go for it console.debug ("Unsubscription: Remove from the database and sent client a positive response ",context.message.data.userInfo.email); - exports.unsetAuthorEmail( - userInfo.userId, - userInfo - ); - exports.unsetAuthorEmailRegistered( userInfo, userInfo.userId, + unsubscribeId, padId ); @@ -222,6 +239,19 @@ exports.unsubscriptionEmail = function (context, emailFound, userInfo, padId) { } } }); + + // Send mail to user with the link for validation + server.send( + { + text: "Please click on this link in order to validate your unsubscription to the pad " + padId + "\n" + urlToPads+padId + "/unsubscribe=" + unsubscribeId, + from: fromName+ "<"+fromEmail+">", + to: userInfo.email, + subject: "Email unsubscription confirmation for pad "+padId + }, + function(err, message) { + console.log(err || message); + } + ); } else { // Unsubscription -> Send failed as email not found console.debug ("Unsubscription: Send client a negative response ",context.message.data.userInfo.email); @@ -297,53 +327,53 @@ exports.checkEmailValidation = function (email) { * Database manipulation */ -// Updates the database with the email record -exports.setAuthorEmail = function (author, userInfo, callback){ - db.setSub("globalAuthor:" + author, ["email"], userInfo.email, callback); -} - -// Write email and padId to the database -exports.setAuthorEmailRegistered = function(userInfo, authorId, padId){ +// Write email, options, authorId and pendingId to the database +exports.setAuthorEmailRegistered = function(userInfo, authorId, subscribeId, padId){ var timestamp = new Date().getTime(); var registered = { authorId: authorId, onStart: userInfo.email_onStart, onEnd: userInfo.email_onEnd, + subscribeId: subscribeId, timestamp: timestamp }; console.debug("registered", registered, " to ", padId); + // Here we have to basically hack a new value into the database, this isn't clean or polite. db.get("emailSubscription:" + padId, function(err, value){ // get the current value - if(!value){value = {};} // if an emailSubscription doesnt exist yet for this padId don't panic - value[userInfo.email] = registered; // add the registered values to the object + if(!value){ + // if an emailSubscription doesnt exist yet for this padId don't panic + value = {"pending":{}}; + } else if (!value['pending']) { + // if the pending section doesn't exist yet for this padId, we create it + value['pending'] = {}; + } + + // add the registered values to the pending section of the object + value['pending'][userInfo.email] = registered; + console.warn("written to database"); db.set("emailSubscription:" + padId, value); // stick it in the database }); } -// Updates the database by removing the email record for that AuthorId -exports.unsetAuthorEmail = function (author, userInfo){ - db.get("globalAuthor:" + author, function(err, value){ // get the current value - - if (value['email'] == userInfo.email) { - // Remove the email option from the datas - delete value['email']; - - // Write the modified datas back in the Db - db.set("globalAuthor:" + author, value); - } - }); -} - -// Remove email, options and padId from the database -exports.unsetAuthorEmailRegistered = function(userInfo, authorId, padId){ +// Write email, authorId and pendingId to the database +exports.unsetAuthorEmailRegistered = function(userInfo, authorId, unsubscribeId, padId){ + var timestamp = new Date().getTime(); + var registered = { + authorId: authorId, + unsubscribeId: unsubscribeId, + timestamp: timestamp + }; console.debug("unregistered", userInfo.email, " to ", padId); db.get("emailSubscription:" + padId, function(err, value){ // get the current value + // if the pending section doesn't exist yet for this padId, we create it (this shouldn't happen) + if (!value['pending']) {value['pending'] = {};} - // remove the registered options from the object - delete value[userInfo.email]; + // add the registered values to the pending section of the object + value['pending'][userInfo.email] = registered; // Write the modified datas back in the Db console.warn("written to database"); diff --git a/index.js b/index.js new file mode 100644 index 0000000..a7e22ed --- /dev/null +++ b/index.js @@ -0,0 +1,287 @@ +var db = require('ep_etherpad-lite/node/db/DB').db, + async = require('../../src/node_modules/async'), + settings = require('../../src/node/utils/Settings'); + +// Remove cache for this procedure +db['dbSettings'].cache = 0; + +exports.registerRoute = function (hook_name, args, callback) { + // Catching (un)subscribe addresses + args.app.get('/p/*/(un){0,1}subscribe=*', function(req, res) { + var fullURL = req.protocol + "://" + req.get('host') + req.url; + var path=req.url.split("/"); + var padId=path[2]; + var param = path[3].split("="); + var action = param[0]; + var actionId = param[1]; + var padURL = req.protocol + "://" + req.get('host') + "/p/" +padId; + var resultDb = {}; + + async.series( + [ + function(cb) { + // Is the (un)subscription valid (exists & not older than 24h) + db.get("emailSubscription:"+padId, function(err, userIds){ + + var foundInDb = false; + var timeDiffGood = false; + var email = "your email"; + + if(userIds && userIds['pending']){ + async.forEach(Object.keys(userIds['pending']), function(user){ + var userInfo = userIds['pending'][user]; + + // If we have Id int the Db, then we are good ot really unsubscribe the user + if(userInfo[action + 'Id'] == actionId){ + console.debug("emailSubscription:", user, "found in DB:", userInfo); + + foundInDb = true; + email = user; + + // Checking if the demand is not older than 24h + var timeDiff = new Date().getTime() - userInfo.timestamp; + timeDiffGood = timeDiff < 1000 * 60 * 60 * 24; + + if(action == 'subscribe' && timeDiffGood == true) { + // Subscription process + setAuthorEmail( + userInfo, + user + ); + + setAuthorEmailRegistered( + userIds, + userInfo, + user, + padId + ); + } else if (action == 'unsubscribe' && timeDiffGood == true) { + // Unsubscription process + unsetAuthorEmail( + userInfo, + user + ); + + unsetAuthorEmailRegistered( + userIds, + user, + padId + ); + } + } + }, + + function(err, msg){ + // There should be something in here! + console.error("Error in emailSubscription async in first function", err, " -> ", msg); + }); // end async for each + } + + resultDb = { + "foundInDb": foundInDb, + "timeDiffGood": timeDiffGood, + "email": email + } + + cb(null, 1); + }); + }, + + function(cb) { + // Create and send the output message + sendContent(res, args, action, padId, padURL, resultDb); + + cb(null, 2); + }, + + function(cb) { + // Take a moment to clean all obsolete pending data + cleanPendingData(padId); + + cb(null, 3); + } + ], + function(err, results){ + console.error("Callback async.series: Err -> ", err, " / results -> ", results); + } + ); + }); + + callback(); // Am I even called? +} + +/** + * Database manipulation + */ + +// Updates the database with the email record +setAuthorEmail = function (userInfo, email){ + db.setSub("globalAuthor:" + userInfo.authorId, ["email"], email); +} + +// Write email and padId to the database +setAuthorEmailRegistered = function(userIds, userInfo, email, padId){ + console.debug("setAuthorEmailRegistered: Initial userIds:", userIds); + var timestamp = new Date().getTime(); + var registered = { + authorId: userInfo.authorId, + onStart: userInfo.onStart, + onEnd: userInfo.onEnd, + timestamp: timestamp + }; + + // add the registered values to the object + userIds[email] = registered; + + // remove the pending data + delete userIds['pending'][email]; + + // Write the modified datas back in the Db + console.warn("written to database"); + db.set("emailSubscription:" + padId, userIds); // stick it in the database + + console.debug("setAuthorEmailRegistered: Modified userIds:", userIds); +} + +// Updates the database by removing the email record for that AuthorId +unsetAuthorEmail = function (userInfo, email){ + db.get("globalAuthor:" + userInfo.authorId, function(err, value){ // get the current value + if (value['email'] == email) { + // Remove the email option from the datas + delete value['email']; + + // Write the modified datas back in the Db + db.set("globalAuthor:" + userInfo.authorId, value); + } + }); +} + +// Remove email, options and padId from the database +unsetAuthorEmailRegistered = function(userIds, email, padId){ + console.debug("unsetAuthorEmailRegistered: initial userIds:", userIds); + // remove the registered options from the object + delete userIds[email]; + + // remove the pending data + delete userIds['pending'][email]; + + // Write the modified datas back in the Db + console.warn("written to database"); + db.set("emailSubscription:" + padId, userIds); + + console.debug("unsetAuthorEmailRegistered: modified userIds:", userIds); +} + +/** + * We take a moment to remove too old pending (un)subscription + */ +cleanPendingData = function (padId) { + var modifiedData, areDataModified = false; + + db.get("emailSubscription:" + padId, function(err, userIds){ // get the current value + console.debug("cleanPendingData: Initial userIds:", userIds); + modifiedData = userIds; + if(userIds && userIds['pending']){ + async.forEach(Object.keys(userIds['pending']), function(user){ + var timeDiff = new Date().getTime() - userIds['pending'][user].timestamp; + var timeDiffGood = timeDiff < 1000 * 60 * 60 * 24; + + if(timeDiffGood == false) { + delete modifiedData['pending'][user]; + + areDataModified = true; + } + }); + } + + if (areDataModified == true) { + // Write the modified datas back in the Db + db.set("emailSubscription:" + padId, modifiedData); + } + + console.debug("cleanPendingData: Modified userIds:", modifiedData, " / areDataModified:", areDataModified); + }); +} + +/** + * Create html output with the status of the process + */ +function sendContent(res, args, action, padId, padURL, resultDb) { + console.debug("starting sendContent: args ->", action, " / ", padId, " / ", padURL, " / ", resultDb); + + if (action == 'subscribe') { + var actionMsg = "Subscribing '" + resultDb.email + "' to pad " + padId; + } else { + var actionMsg = "Unsubscribing '" + resultDb.email + "' from pad " + padId; + } + var msgCause, resultMsg, classResult; + + if (resultDb.foundInDb == true && resultDb.timeDiffGood == true) { + // Pending data were found un Db and updated -> good + resultMsg = "Success"; + classResult = "good"; + } else if (resultDb.foundInDb == true) { + // Pending data were found but older than a day -> fail + msgCause = "You have max 24h to click the link in your confirmation email."; + resultMsg = "Too late!"; + resultMsg += '