PageRenderTime 49ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/email/emailDigest.js

https://github.com/dbhurley/scrollback
JavaScript | 505 lines | 419 code | 17 blank | 69 comment | 81 complexity | 77264d7646284d293d8491880060991f MD5 | raw file
  1. var config = require('../config.js');
  2. var log = require("../lib/logger.js");
  3. var send = require('./sendEmail.js');
  4. var fs=require("fs"),jade = require("jade");
  5. var redis = require('../lib/redisProxy.js').select(config.redisDB.email);
  6. var core;
  7. var internalSession = Object.keys(config.whitelists)[0];
  8. var emailConfig = config.email, digestJade;
  9. var waitingTime1 = emailConfig.mentionEmailTimeout || 3 * 60 * 60 * 1000; //mention email timeout
  10. var waitingTime2 = emailConfig.regularEmailTimeout || 12 * 60 * 60 * 1000;//regular email timeout
  11. var timeout = 30 * 1000;//for debuging only
  12. var debug = emailConfig.debug;
  13. module.exports.init = function (coreObj) {
  14. core = coreObj;
  15. init();
  16. };
  17. module.exports.initMailSending = initMailSending;
  18. module.exports.trySendingToUsers = trySendingToUsers;
  19. module.exports.sendPeriodicMails = sendPeriodicMails;
  20. /**
  21. * Read digest,jade
  22. * And setInterval
  23. */
  24. function init() {
  25. fs.readFile(__dirname + "/views/digest.jade", "utf8", function(err, data) {
  26. if(err) throw err;
  27. digestJade = jade.compile(data, {basedir: __dirname + "/views/" });
  28. //send mails in next hour
  29. var x = new Date().getUTCMinutes();
  30. var sub = 90;
  31. if (x < 30) {
  32. sub = 30;
  33. }
  34. log("Init email will send email after ", (sub - x)* 60000, " ms");
  35. setTimeout(function(){
  36. sendPeriodicMails();
  37. setInterval(sendPeriodicMails, 60*60*1000);//TODO move these numbers to myConfig
  38. }, (sub-x)*60000);
  39. setTimeout(function(){
  40. trySendingToUsers();
  41. setInterval(trySendingToUsers, 60*60*1000);
  42. }, (60-x)*60000);
  43. });
  44. }
  45. /**
  46. *Try sending mail to waiting users.
  47. *Reads email:toSend from redis.
  48. */
  49. function trySendingToUsers() {
  50. redis.smembers("email:toSend", function(err,usernames) {
  51. if(!err && usernames) {
  52. if (emailConfig.debug) log("checking for mentions...", usernames);
  53. usernames.forEach(function(username) {
  54. initMailSending(username);
  55. });
  56. }
  57. });
  58. }
  59. /**
  60. *Init of mail sending to username
  61. *conditions that can call the function
  62. *1 - after 24 hours(12 AM in user's timezone)
  63. *2 - On nick mention
  64. *3 - Periodic check for mention timeout.
  65. *@param {string} username username
  66. */
  67. function initMailSending(username) {
  68. log("init mail sending for user " + username);
  69. redis.get("email:" + username + ":lastsent", function(err, lastSent) {
  70. log("data returned form redis", lastSent + " , " , err);
  71. if (err) return;
  72. redis.get("email:" + username + ":isMentioned", function(err, data) {
  73. var ct = new Date().getTime();
  74. var interval = waitingTime2 ;
  75. if (data) interval = waitingTime1;
  76. if (emailConfig.debug) {
  77. log("username " + username + " is mentioned ", data);
  78. interval = timeout/2;
  79. if (data) interval = timeout/8;
  80. log("interval " , interval);
  81. }
  82. if (!lastSent ) lastSent = ct - interval;
  83. log("time left for user " , (parseInt(lastSent, 10) + interval - ct));
  84. if (parseInt(lastSent, 10) + interval <= ct) {
  85. //get rooms that user is following...
  86. log("getting rooms that user is following....");
  87. core.emit("getRooms", {hasMember: username, session: internalSession}, function(err, following) {
  88. log("results:", following);
  89. if(err || !following) {
  90. log("error in getting members information" , err);
  91. return;
  92. }
  93. if(!following.results || !following.results.length) {
  94. log("username ", username ," is not following any rooms ");
  95. return;
  96. }
  97. var rooms = [];
  98. following.results.forEach(function(r) {
  99. rooms.push(r.id);
  100. });
  101. prepareEmailObject(username, rooms, lastSent, function(err, email) {
  102. if (!err) sendMail(email);
  103. });
  104. });
  105. redis.srem("email:toSend", username);
  106. }else {
  107. log("can not send email to user ", username, " now" );
  108. redis.sadd("email:toSend", username);
  109. }
  110. });
  111. });
  112. }
  113. /**
  114. *send mail to user read data from redis and create mail object
  115. *email: {
  116. * username: {string}, //username
  117. heading : {string},
  118. count: {number} ,//total count of labels
  119. emailId: {string},
  120. rooms: [
  121. id: {string}, //room name
  122. totalCount: {number},//total count of labels
  123. labels: [
  124. {
  125. label: {string},
  126. count: {number},
  127. interesting: [
  128. messages objects
  129. ]
  130. },
  131. ....
  132. ],
  133. ...
  134. ],
  135. }
  136. *@param {string} username
  137. *@param {string} rooms all rooms that user is following
  138. *@param {function} callback(err, emailObject).
  139. */
  140. function prepareEmailObject(username ,rooms, lastSent, callback) {
  141. if (emailConfig.debug) log("send mail to user ", username , rooms);
  142. var email = {};
  143. email.username = username;
  144. email.rooms = [];
  145. var ct = 0;
  146. var vq = 0;
  147. rooms.forEach(function(room) {
  148. var roomsObj = [];
  149. var qc = 0;
  150. var m = "email:mentions:" + room + ":" + username ;
  151. qc++;
  152. redis.smembers(m, function(err, mentions) {
  153. if (!err) {
  154. mentions.sort(function(a, b) {
  155. a = JSON.parse(a);
  156. b = JSON.parse(b);
  157. return a.time - b.time;
  158. });
  159. log("mentions returned from redis ", room ,mentions, lastSent);
  160. var l = "email:label:" + room + ":labels";
  161. redis.zrangebyscore(l, lastSent, "+inf", function(err,labels) {
  162. if(emailConfig.debug) log("labels returned from redis" , labels);
  163. roomsObj.labels = [];
  164. roomsObj.totalCount = labels.length;
  165. if (!err) {
  166. var isLabel = false;
  167. labels.forEach(function(label) {
  168. isLabel = true;
  169. var lc = "email:label:" + room + ":" + label + ":count";
  170. qc++;
  171. redis.get(lc, function(err,count) {
  172. if (err) {
  173. callback(err);
  174. }else {
  175. var ll = {
  176. label: label ,
  177. count : parseInt(count, 10)
  178. };
  179. roomsObj.labels.push(ll);
  180. }
  181. done(roomsObj, mentions);
  182. });
  183. });
  184. if (!isLabel) {
  185. qc++;//if no label never call done() for this room;
  186. isNoLabel();
  187. ct++;
  188. }
  189. }
  190. else {
  191. callback(err);
  192. }
  193. done();//members
  194. });
  195. }
  196. else {
  197. callback(err);
  198. }
  199. });
  200. function isNoLabel() {
  201. if (++vq >= rooms.length) {
  202. callback(new Error("NO_DATA"));
  203. }
  204. }
  205. function done( roomObj, mentions) {
  206. log("room done......" , room , qc);
  207. if(--qc > 0 ) return;
  208. sortLabels(room ,roomObj,mentions,function(err,rr) {
  209. if (err) callback(err);
  210. else {
  211. email.rooms.push(rr);
  212. ct++;
  213. if (ct >= rooms.length) {
  214. deleteMentions(username, rooms);
  215. log("email object creation complete" , JSON.stringify(email));
  216. callback(null, email);
  217. }
  218. }
  219. });
  220. }
  221. });
  222. }
  223. /**
  224. *create email.rooms element
  225. *filter out labels and generate labels array for current room
  226. *Add label.interesting messages.
  227. */
  228. function sortLabels(room, roomObj, mentions,callback) {
  229. var maxLabels = 5;
  230. log("sort labels");
  231. var r = {};
  232. var ct = 0;
  233. r.id = room;
  234. r.totalCount = roomObj.totalCount;
  235. r.labels = [];
  236. roomObj.labels.forEach(function(label) {
  237. label.interesting = [];
  238. mentions.forEach(function(m) {//TODO use new schema
  239. m = JSON.parse(m);
  240. var id = m.threads[0].id;
  241. if(id === label.label) {
  242. label.interesting.push(m);
  243. label.title = m.threads[0].title;
  244. }
  245. });
  246. ct++;
  247. redis.get ("email:label:" + room + ":" + label.label + ":title", function(err, title) {
  248. if (!err && title) {
  249. label.title = title;
  250. } else label.title = "Title";
  251. var pos = r.labels.length;
  252. for (var i = 0;i < r.labels.length;i++ ) {
  253. if (r.labels[i].interesting.length < label.interesting.length ) {
  254. pos = i;
  255. break;
  256. }
  257. else if(r.labels[i].interesting.length === label.interesting.length) {
  258. if (r.labels[i].count < label.count) {
  259. pos = i;
  260. break;
  261. }
  262. }
  263. }
  264. var rm = -1;
  265. if (r.labels.length >= maxLabels) {
  266. rm = r.labels.length;
  267. }
  268. r.labels.splice(pos,0,label);
  269. r.labels.sort(function(l1,l2){
  270. return l2.count - l1.count;
  271. });
  272. if (rm != -1) {
  273. r.labels.splice(rm,1);
  274. }
  275. done();
  276. });
  277. });
  278. function done() {
  279. if (--ct > 0) {
  280. return;
  281. }
  282. r.labels.sort(function(l1, l2) {
  283. return l2.count - l1.count;
  284. });
  285. var nn = 0;
  286. r.labels.forEach(function(label) {
  287. nn++;
  288. redis.lrange("email:label:" + room + ":" + label.label + ":tail", 0, -1, function(err, lastMsgs) {
  289. if (lastMsgs ) {
  290. lastMsgs.reverse();
  291. lastMsgs.forEach(function(lastMsg) {
  292. var isP = true;
  293. var msg = JSON.parse(lastMsg);
  294. label.interesting.forEach(function(m) {
  295. if(m.id === msg.id) {
  296. isP = false;
  297. }
  298. });
  299. if (isP) {
  300. label.interesting.push(msg);
  301. }
  302. });
  303. }
  304. complete();
  305. });
  306. });
  307. log("room Obj " , JSON.stringify(r));
  308. function complete() {
  309. if (--nn > 0) {
  310. return;
  311. }
  312. r.labels.forEach(function(label) {
  313. label.interesting.sort(function(m1,m2){
  314. return m1.time - m2.time;
  315. });
  316. });
  317. callback(null, r);
  318. }
  319. }
  320. }
  321. /**
  322. *delete all mentions of user on rooms from redis
  323. *@param {string} username.
  324. * @param rooms
  325. */
  326. function deleteMentions(username , rooms) {
  327. rooms.forEach(function(room) {
  328. var m = "email:mentions:" + room + ":" + username ;
  329. redis.multi(function(multi) {
  330. multi.del(m);
  331. m = "email:" + username + ":isMentioned";
  332. multi.del(m);
  333. multi.exec(function(replies) {
  334. log("mentions deleted" , replies);
  335. });
  336. });
  337. });
  338. }
  339. /**
  340. *Read data from email Object render HTML from email object using /views/digest.jade
  341. *and then send mail to email.emailId
  342. *@param {object} Email Object
  343. */
  344. function sendMail(email) {
  345. core.emit("getUsers", {ref: email.username, session: internalSession}, function(err, data) {
  346. if (err || !data.results) return;
  347. var user = data.results;
  348. log("getting email id", data);
  349. var mailAccount;
  350. if (user && user[0] && user[0].identities) {
  351. mailAccount = user[0].identities;
  352. mailAccount.forEach(function(e) {
  353. if (e.indexOf("mailto:") === 0) {
  354. email.emailId = e.substring(7);
  355. var html;
  356. try {
  357. email.heading = getHeading(email);
  358. log("email object" + JSON.stringify(email));
  359. html = digestJade(email);
  360. }catch(err) {
  361. log("Error while rendering email: ", err);
  362. //TODO send mail to developer..
  363. return;
  364. }
  365. log(email , "sending email to user " , html );
  366. send(emailConfig.from, email.emailId, email.heading, html);
  367. redis.set("email:" + email.username + ":lastsent", new Date().getTime());
  368. var interval = 2*24*60*60*1000;//TODO move this variable inside myConfig
  369. if (emailConfig.debug) {
  370. interval = timeout*2;
  371. }
  372. email.rooms.forEach(function(room) {
  373. redis.zremrangebyscore("email:label:" + room.id + ":labels", 0,
  374. new Date().getTime() - interval , function(err, data) {
  375. log("deleted old labels from that room " , err ,data);
  376. });//ZREMRANGEBYSCORE email:scrollback:labels -1 1389265655284
  377. });
  378. }
  379. });
  380. }
  381. });
  382. }
  383. /**
  384. *Generate Heading from email Object
  385. *@param {object} email Object
  386. */
  387. function getHeading(email) {
  388. var heading = "";
  389. var bestLabel ;
  390. var bestMention = {};
  391. var labelCount = 0;
  392. var more = 0;
  393. email.rooms.forEach(function(room) {
  394. labelCount += room.totalCount;
  395. more += room.labels.length;
  396. room.labels.forEach(function(label) {
  397. if (!bestLabel) {
  398. bestLabel = {};
  399. bestLabel.title = formatText(label.title);
  400. bestLabel.room = room.id;
  401. bestLabel.count = label.count;
  402. }
  403. else if(bestLabel.count < label.count){
  404. bestLabel.title = formatText(label.title);
  405. bestLabel.room = room.id;
  406. bestLabel.count = label.count;
  407. }
  408. log("best label", bestLabel);
  409. label.interesting.forEach(function(m) {
  410. if (!bestMention.mentions && m.mentions && m.mentions.indexOf(email.username) != -1) {
  411. bestMention = m;
  412. }
  413. else if(m.mentions && m.mentions.indexOf(email.username) != -1 && bestMention.text.length < m.text.length) {
  414. bestMention = m;
  415. }
  416. });
  417. });
  418. });
  419. email.count = labelCount;
  420. if (bestMention.mentions) {//if mentioned
  421. heading += "[" + bestMention.from.replace(/guest-/g, "") + "] " + bestMention.text + " - on " + bestMention.to;
  422. }
  423. else {
  424. var tail = (more > 1 ? " +" + (more - 1) + " more": "");
  425. heading += "[" + bestLabel.room.substring(0,1).toUpperCase() + bestLabel.room.substring(1) + "] " +
  426. bestLabel.title + tail;
  427. }
  428. email.formatText = formatText;
  429. return heading;
  430. }
  431. var formatText = function(text) {
  432. var s = text.replace(/-/g,' ');
  433. s = s.trim();
  434. s = s.substring(0,1).toUpperCase() + s.substring(1);
  435. return s;
  436. };
  437. /**
  438. *Send mails to users based on current time.
  439. *@param {object} Map of room data.
  440. */
  441. function sendPeriodicMails(){
  442. var x = new Date().getUTCHours();
  443. var t;
  444. var start1 = x >= 12 ? (24 - x)*60 : -x*60;
  445. var end1 = start1 + 59;
  446. var start2 = -100*60;//big values
  447. var end2 = -200*60;
  448. if (emailConfig.debug) {
  449. start1=0;//for testing....
  450. end1=10000000;//for testing...
  451. }
  452. if (x >= 9 && x < 12) {
  453. start2 = 24*60 + start1;//(+12 +14 +13)
  454. end2 = start2 + 59;//+13
  455. }
  456. if (x == 12) {
  457. start2 = -12*60;
  458. end2 = start2 + 59;
  459. }
  460. log("current time hour:",x+","+start1+","+start2);
  461. function processResults(err, data) {
  462. log("err", err, " data: ", data );
  463. if (err || !data.results) return;
  464. var users = data.results;
  465. users.forEach(function(user) {
  466. log("trying for user", user);
  467. if (user.params && (!user.params.email || user.params.email.frequency !== "never")) {//TODO write a query based on freq
  468. initMailSending(user.id);
  469. }
  470. });
  471. }
  472. if (start1 > end1) {
  473. t = start1;
  474. start1 = end1;
  475. end1 = t;
  476. }
  477. if (start2 > end2) {
  478. t = start2;
  479. start2 = end2;
  480. end2 = t;
  481. }
  482. core.emit("getUsers", {timezone: {gte: start1, lte: end1}, session: internalSession}, function(err, data) {
  483. processResults(err, data);
  484. });
  485. core.emit("getUsers", {timezone: {gte: start2, lte: end2}, session: internalSession}, function(err, data) {
  486. processResults(err, data);
  487. });
  488. }