From 1460bf21289c4eda05feadc19aa9f7af3b7cc25f Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 22 Jun 2015 16:52:01 -0300 Subject: [PATCH 1/7] add language & unit to preferences Signed-off-by: Ivan Socolsky --- lib/model/preferences.js | 4 ++++ lib/server.js | 36 ++++++++++++++++++++++++---- test/integration/server.js | 48 ++++++++++++++++++++++++++++---------- 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/lib/model/preferences.js b/lib/model/preferences.js index ab2fb92..353c858 100644 --- a/lib/model/preferences.js +++ b/lib/model/preferences.js @@ -13,6 +13,8 @@ Preferences.create = function(opts) { x.walletId = opts.walletId; x.copayerId = opts.copayerId; x.email = opts.email; + x.language = opts.language; + x.unit = opts.unit; return x; }; @@ -23,6 +25,8 @@ Preferences.fromObj = function(obj) { x.walletId = obj.walletId; x.copayerId = obj.copayerId; x.email = obj.email; + x.language = obj.language; + x.unit = obj.unit; return x; }; diff --git a/lib/server.js b/lib/server.js index 5c137c5..7706b11 100644 --- a/lib/server.js +++ b/lib/server.js @@ -454,16 +454,42 @@ WalletService.prototype.joinWallet = function(opts, cb) { * Save copayer preferences for the current wallet/copayer pair. * @param {Object} opts * @param {string} opts.email - Email address for notifications. + * @param {string} opts.language - Language used for notifications. + * @param {string} opts.unit - Bitcoin unit used to format amounts in notifications. */ WalletService.prototype.savePreferences = function(opts, cb) { var self = this; opts = opts || {}; - if (opts.email) { - if (!EmailValidator.validate(opts.email)) { - return cb(new ClientError('Invalid email address')); - } + var preferences = [{ + name: 'email', + isValid: function(value) { + return EmailValidator.validate(value); + }, + }, { + name: 'language', + isValid: function(value) { + return _.isString(value) && value.length == 2; + }, + }, { + name: 'unit', + isValid: function(value) { + return _.isString(value) && _.contains(['btc', 'bit'], value.toLowerCase()); + }, + }]; + + try { + _.each(preferences, function(preference) { + var value = opts[preference.name]; + if (!value) return; + if (!preference.isValid(value)) { + throw 'Invalid ' + preference.name; + return false; + } + }); + } catch (ex) { + return cb(new ClientError(ex)); } self._runLocked(cb, function(cb) { @@ -471,6 +497,8 @@ WalletService.prototype.savePreferences = function(opts, cb) { walletId: self.walletId, copayerId: self.copayerId, email: opts.email, + language: opts.language, + unit: opts.unit, }); self.storage.storePreferences(preferences, function(err) { return cb(err); diff --git a/test/integration/server.js b/test/integration/server.js index 3b93e23..ed1d21d 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1099,13 +1099,17 @@ describe('Wallet service', function() { it('should save & retrieve preferences', function(done) { server.savePreferences({ - email: 'dummy@dummy.com' + email: 'dummy@dummy.com', + language: 'es', + unit: 'bit', }, function(err) { should.not.exist(err); server.getPreferences({}, function(err, preferences) { should.not.exist(err); should.exist(preferences); preferences.email.should.equal('dummy@dummy.com'); + preferences.language.should.equal('es'); + preferences.unit.should.equal('bit'); done(); }); }); @@ -1125,20 +1129,40 @@ describe('Wallet service', function() { }); }); it.skip('should save preferences only for requesting wallet', function(done) {}); - it('should validate email address', function(done) { - server.savePreferences({ - email: ' ' - }, function(err) { - should.exist(err); - err.message.should.contain('email'); - server.savePreferences({ + it('should validate entries', function(done) { + var invalid = [{ + preferences: { + email: ' ', + }, + expected: 'email' + }, { + preferences: { email: 'dummy@' + _.repeat('domain', 50), - }, function(err) { + }, + expected: 'email' + }, { + preferences: { + language: 'xxxxx', + }, + expected: 'language' + }, { + preferences: { + language: 123, + }, + expected: 'language' + }, { + preferences: { + unit: 'xxxxx', + }, + expected: 'unit' + }, ]; + async.each(invalid, function(item, next) { + server.savePreferences(item.preferences, function(err) { should.exist(err); - err.message.should.contain('email'); - done(); + err.message.should.contain(item.expected); + next(); }); - }); + }, done); }); }); From 6609814840230485d1365f5c0d833cbfd4018667 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 26 Jun 2015 11:15:47 -0300 Subject: [PATCH 2/7] test email preferences --- lib/model/email.js | 2 ++ test/integration/server.js | 44 +++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/model/email.js b/lib/model/email.js index 63140bb..f6ebca6 100644 --- a/lib/model/email.js +++ b/lib/model/email.js @@ -25,6 +25,7 @@ Email.create = function(opts) { x.attempts = 0; x.lastAttemptOn = null; x.notificationId = opts.notificationId; + x.language = opts.language || 'en'; return x; }; @@ -43,6 +44,7 @@ Email.fromObj = function(obj) { x.attempts = obj.attempts; x.lastAttemptOn = obj.lastAttemptOn; x.notificationId = obj.notificationId; + x.language = obj.language; return x; }; diff --git a/test/integration/server.js b/test/integration/server.js index ed1d21d..327f4c7 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -308,7 +308,7 @@ describe('Wallet service', function() { WalletService.shutDown(done); }); - describe('Email notifications', function() { + describe.only('Email notifications', function() { var server, wallet, mailerStub, emailService; beforeEach(function(done) { @@ -543,6 +543,48 @@ describe('Wallet service', function() { }); }); + it('should build each email using preferences of the copayers', function(done) { + // Set same email address for copayer1 and copayer2 + server.savePreferences({ + language: 'es', + unit: 'btc', + }, function(err) { + server.createAddress({}, function(err, address) { + should.not.exist(err); + + // Simulate incoming tx notification + server._notify('NewIncomingTx', { + txid: '999', + address: address, + amount: 12300000, + }, function(err) { + setTimeout(function() { + var calls = mailerStub.sendMail.getCalls(); + calls.length.should.equal(3); + var emails = _.map(calls, function(c) { + return c.args[0]; + }); + var spanish = _.find(emails, { + to: 'copayer1@domain.com' + }); + spanish.from.should.equal('bws@dummy.net'); + spanish.subject.should.contain('Nuevo pago recibido'); + spanish.text.should.contain(wallet.name); + spanish.text.should.contain('0.123000 btc'); + var english = _.find(emails, { + to: 'copayer2@domain.com' + }); + english.from.should.equal('bws@dummy.net'); + english.subject.should.contain('New payment received'); + english.text.should.contain(wallet.name); + english.text.should.contain('123,000 bit'); + done(); + }, 100); + }); + }); + }); + }); + it('should support multiple emailservice instances running concurrently', function(done) { var emailService2 = new EmailService(); emailService2.start({ From 044f511726fd648c32b5202a9fe5a077e9c3bb00 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 26 Jun 2015 15:47:27 -0300 Subject: [PATCH 3/7] select template based on language --- config.js | 2 + lib/emailservice.js | 91 +++++++++++++++---- lib/templates/{ => en}/new_copayer.plain | 0 lib/templates/{ => en}/new_incoming_tx.plain | 0 lib/templates/{ => en}/new_outgoing_tx.plain | 0 lib/templates/{ => en}/new_tx_proposal.plain | 0 .../{ => en}/txp_finally_rejected.plain | 0 lib/templates/{ => en}/wallet_complete.plain | 0 lib/templates/es/new_copayer.plain | 2 + lib/templates/es/new_incoming_tx.plain | 2 + lib/templates/es/new_outgoing_tx.plain | 2 + lib/templates/es/new_tx_proposal.plain | 2 + lib/templates/es/txp_finally_rejected.plain | 2 + lib/templates/es/wallet_complete.plain | 2 + test/integration/server.js | 5 +- 15 files changed, 89 insertions(+), 21 deletions(-) rename lib/templates/{ => en}/new_copayer.plain (100%) rename lib/templates/{ => en}/new_incoming_tx.plain (100%) rename lib/templates/{ => en}/new_outgoing_tx.plain (100%) rename lib/templates/{ => en}/new_tx_proposal.plain (100%) rename lib/templates/{ => en}/txp_finally_rejected.plain (100%) rename lib/templates/{ => en}/wallet_complete.plain (100%) create mode 100644 lib/templates/es/new_copayer.plain create mode 100644 lib/templates/es/new_incoming_tx.plain create mode 100644 lib/templates/es/new_outgoing_tx.plain create mode 100644 lib/templates/es/new_tx_proposal.plain create mode 100644 lib/templates/es/txp_finally_rejected.plain create mode 100644 lib/templates/es/wallet_complete.plain diff --git a/config.js b/config.js index c2f4da9..f493af8 100644 --- a/config.js +++ b/config.js @@ -52,6 +52,8 @@ var config = { ignoreTLS: true, subjectPrefix: '[Wallet Service]', from: 'wallet-service@bitcore.io', + templatePath: './lib/templates', + defaultLanguage: 'en', }, }; module.exports = config; diff --git a/lib/emailservice.js b/lib/emailservice.js index 50ebf6d..a977352 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -7,6 +7,7 @@ var Mustache = require('mustache'); var log = require('npmlog'); log.debug = log.verbose; var fs = require('fs'); +var path = require('path'); var nodemailer = require('nodemailer'); var WalletUtils = require('bitcore-wallet-utils'); @@ -49,10 +50,32 @@ function EmailService() {}; EmailService.prototype.start = function(opts, cb) { opts = opts || {}; + function _readDirectories(basePath, cb) { + fs.readdir(basePath, function(err, files) { + if (err) return cb(err); + async.filter(files, function(file, next) { + fs.stat(path.join(basePath, file), function(err, stats) { + return next(!err && stats.isDirectory()); + }); + }, function(dirs) { + return cb(null, dirs); + }); + }); + }; + var self = this; + self.defaultLanguage = opts.defaultLanguage || 'en'; + self.templatePath = path.normalize((opts.templatePath || (__dirname + '/templates')) + '/'); + async.parallel([ + function(done) { + _readDirectories(self.templatePath, function(err, res) { + self.availableLanguages = res; + done(err); + }); + }, function(done) { if (opts.storage) { self.storage = opts.storage; @@ -87,10 +110,13 @@ EmailService.prototype.start = function(opts, cb) { // TODO: cache for X minutes -EmailService.prototype._readTemplate = function(filename, cb) { - fs.readFile(__dirname + '/templates/' + filename + '.plain', 'utf8', function(err, template) { +EmailService.prototype._readTemplate = function(filename, language, cb) { + var self = this; + + var fullFilename = path.join(self.templatePath, language, filename + '.plain'); + fs.readFile(fullFilename, 'utf8', function(err, template) { if (err) { - log.error('Could not read template file ' + filename, err); + log.error('Could not read template file ' + fullFilename, err); return cb(err); } var lines = template.split('\n'); @@ -126,9 +152,18 @@ EmailService.prototype._getRecipientsList = function(notification, emailType, cb usedEmails[p.email] = true; if (notification.creatorId == p.copayerId && !emailType.notifyDoer) return; + if (!_.contains(self.availableLanguages, p.language)) { + if (p.language) { + log.warn('Language for email "' + p.language + '" not available.'); + } + p.language = self.defaultLanguage; + } + return { copayerId: p.copayerId, - emailAddress: p.email + emailAddress: p.email, + language: p.language, + unit: p.unit || 'btc', }; })); @@ -189,6 +224,36 @@ EmailService.prototype._send = function(email, cb) { }); }; + +EmailService.prototype._readAndApplyTemplates = function(notification, emailType, recipientsList, cb) { + var self = this; + + async.waterfall([ + + function(next) { + self._getDataForTemplate(notification, next); + }, + function(data, next) { + var languages = _.uniq(_.pluck(recipientsList, 'language')); + async.map(languages, function(lang, next) { + async.waterfall([ + + function(next) { + self._readTemplate(emailType.filename, lang, next); + }, + function(template, next) { + self._applyTemplate(template, data, next); + }, + ], function(err, res) { + next(err, [lang, res]); + }); + }, function(err, res) { + return next(err, _.zipObject(res)); + }); + }, + ], cb); +}; + EmailService.prototype.sendEmail = function(notification, cb) { var self = this; @@ -211,23 +276,11 @@ EmailService.prototype.sendEmail = function(notification, cb) { async.waterfall([ function(next) { - async.parallel([ - - function(next) { - self._readTemplate(emailType.filename, next); - }, - function(next) { - self._getDataForTemplate(notification, next); - }, - ], function(err, res) { - next(err, res[0], res[1]); - }); - }, - function(template, data, next) { - self._applyTemplate(template, data, next); + self._readAndApplyTemplates(notification, emailType, recipientsList, next); }, - function(content, next) { + function(contents, next) { async.map(recipientsList, function(recipient, next) { + var content = contents[recipient.language]; var email = Model.Email.create({ walletId: notification.walletId, copayerId: recipient.copayerId, diff --git a/lib/templates/new_copayer.plain b/lib/templates/en/new_copayer.plain similarity index 100% rename from lib/templates/new_copayer.plain rename to lib/templates/en/new_copayer.plain diff --git a/lib/templates/new_incoming_tx.plain b/lib/templates/en/new_incoming_tx.plain similarity index 100% rename from lib/templates/new_incoming_tx.plain rename to lib/templates/en/new_incoming_tx.plain diff --git a/lib/templates/new_outgoing_tx.plain b/lib/templates/en/new_outgoing_tx.plain similarity index 100% rename from lib/templates/new_outgoing_tx.plain rename to lib/templates/en/new_outgoing_tx.plain diff --git a/lib/templates/new_tx_proposal.plain b/lib/templates/en/new_tx_proposal.plain similarity index 100% rename from lib/templates/new_tx_proposal.plain rename to lib/templates/en/new_tx_proposal.plain diff --git a/lib/templates/txp_finally_rejected.plain b/lib/templates/en/txp_finally_rejected.plain similarity index 100% rename from lib/templates/txp_finally_rejected.plain rename to lib/templates/en/txp_finally_rejected.plain diff --git a/lib/templates/wallet_complete.plain b/lib/templates/en/wallet_complete.plain similarity index 100% rename from lib/templates/wallet_complete.plain rename to lib/templates/en/wallet_complete.plain diff --git a/lib/templates/es/new_copayer.plain b/lib/templates/es/new_copayer.plain new file mode 100644 index 0000000..81ca74c --- /dev/null +++ b/lib/templates/es/new_copayer.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Nuevo copayer +Un nuevo copayer ha ingresado a su monedero {{walletName}}. diff --git a/lib/templates/es/new_incoming_tx.plain b/lib/templates/es/new_incoming_tx.plain new file mode 100644 index 0000000..3164273 --- /dev/null +++ b/lib/templates/es/new_incoming_tx.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Nuevo pago recibido +Un pago de {{amount}} fue recibido en su monedero {{walletName}}. diff --git a/lib/templates/es/new_outgoing_tx.plain b/lib/templates/es/new_outgoing_tx.plain new file mode 100644 index 0000000..ebcb6e2 --- /dev/null +++ b/lib/templates/es/new_outgoing_tx.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Pago enviado +Un pago de {{amount}} ha sido enviado de su monedero {{walletName}}. diff --git a/lib/templates/es/new_tx_proposal.plain b/lib/templates/es/new_tx_proposal.plain new file mode 100644 index 0000000..3c5cd0e --- /dev/null +++ b/lib/templates/es/new_tx_proposal.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Nueva propuesta de pago +Una nueva propuesta de pago ha sido creada en su monedero {{walletName}} por {{copayerName}}. diff --git a/lib/templates/es/txp_finally_rejected.plain b/lib/templates/es/txp_finally_rejected.plain new file mode 100644 index 0000000..0eecd49 --- /dev/null +++ b/lib/templates/es/txp_finally_rejected.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Propuesta de pago rechazada +Una propuesta de pago en su monedero {{walletName}} ha sido rechazada por {{rejectorsNames}}. diff --git a/lib/templates/es/wallet_complete.plain b/lib/templates/es/wallet_complete.plain new file mode 100644 index 0000000..bb2ada6 --- /dev/null +++ b/lib/templates/es/wallet_complete.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Monedero completo +Su monedero {{walletName}} está completo. diff --git a/test/integration/server.js b/test/integration/server.js index 327f4c7..6cf5040 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -308,7 +308,7 @@ describe('Wallet service', function() { WalletService.shutDown(done); }); - describe.only('Email notifications', function() { + describe('Email notifications', function() { var server, wallet, mailerStub, emailService; beforeEach(function(done) { @@ -543,9 +543,10 @@ describe('Wallet service', function() { }); }); - it('should build each email using preferences of the copayers', function(done) { + it.only('should build each email using preferences of the copayers', function(done) { // Set same email address for copayer1 and copayer2 server.savePreferences({ + email: 'copayer1@domain.com', language: 'es', unit: 'btc', }, function(err) { From 2d5fdaca1bff62bd6c3f8b7e14e4c6f636de87bb Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 26 Jun 2015 16:10:18 -0300 Subject: [PATCH 4/7] include unit when rendering template --- config.js | 1 + lib/emailservice.js | 46 ++++++++++++++++++++------------------ test/integration/server.js | 5 +++-- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/config.js b/config.js index f493af8..22128a1 100644 --- a/config.js +++ b/config.js @@ -54,6 +54,7 @@ var config = { from: 'wallet-service@bitcore.io', templatePath: './lib/templates', defaultLanguage: 'en', + defaultUnit: 'btc', }, }; module.exports = config; diff --git a/lib/emailservice.js b/lib/emailservice.js index a977352..ee4d623 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -66,6 +66,7 @@ EmailService.prototype.start = function(opts, cb) { var self = this; self.defaultLanguage = opts.defaultLanguage || 'en'; + self.defaultUnit = opts.defaultUnit || 'btc'; self.templatePath = path.normalize((opts.templatePath || (__dirname + '/templates')) + '/'); async.parallel([ @@ -163,7 +164,7 @@ EmailService.prototype._getRecipientsList = function(notification, emailType, cb copayerId: p.copayerId, emailAddress: p.email, language: p.language, - unit: p.unit || 'btc', + unit: p.unit || self.defaultUnit, }; })); @@ -171,13 +172,13 @@ EmailService.prototype._getRecipientsList = function(notification, emailType, cb }); }; -EmailService.prototype._getDataForTemplate = function(notification, cb) { +EmailService.prototype._getDataForTemplate = function(notification, recipient, cb) { var self = this; var data = _.cloneDeep(notification.data); data.subjectPrefix = _.trim(self.subjectPrefix) + ' '; if (data.amount) { - data.amount = WalletUtils.formatAmount(+data.amount, 'bit') + ' bits'; + data.amount = WalletUtils.formatAmount(+data.amount, recipient.unit) + ' ' + recipient.unit; } self.storage.fetchWallet(notification.walletId, function(err, wallet) { if (err) return cb(err); @@ -228,30 +229,31 @@ EmailService.prototype._send = function(email, cb) { EmailService.prototype._readAndApplyTemplates = function(notification, emailType, recipientsList, cb) { var self = this; - async.waterfall([ + async.map(recipientsList, function(recipient, next) { + async.waterfall([ - function(next) { - self._getDataForTemplate(notification, next); - }, - function(data, next) { - var languages = _.uniq(_.pluck(recipientsList, 'language')); - async.map(languages, function(lang, next) { - async.waterfall([ + function(next) { + async.parallel([ function(next) { - self._readTemplate(emailType.filename, lang, next); + self._readTemplate(emailType.filename, recipient.language, next); }, - function(template, next) { - self._applyTemplate(template, data, next); + function(next) { + self._getDataForTemplate(notification, recipient, next); }, - ], function(err, res) { - next(err, [lang, res]); - }); - }, function(err, res) { - return next(err, _.zipObject(res)); - }); - }, - ], cb); + ], next); + }, + function(result, next) { + var template = result[0]; + var data = result[1]; + self._applyTemplate(template, data, next); + }, + ], function(err, res) { + next(err, [recipient.language, res]); + }); + }, function(err, res) { + return cb(err, _.zipObject(res)); + }); }; EmailService.prototype.sendEmail = function(notification, cb) { diff --git a/test/integration/server.js b/test/integration/server.js index 6cf5040..e3a22a4 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -321,6 +321,7 @@ describe('Wallet service', function() { helpers.getAuthServer(copayer.id, function(server) { server.savePreferences({ email: 'copayer' + (++i) + '@domain.com', + unit: 'bit', }, next); }); }, function(err) { @@ -543,7 +544,7 @@ describe('Wallet service', function() { }); }); - it.only('should build each email using preferences of the copayers', function(done) { + it('should build each email using preferences of the copayers', function(done) { // Set same email address for copayer1 and copayer2 server.savePreferences({ email: 'copayer1@domain.com', @@ -571,7 +572,7 @@ describe('Wallet service', function() { spanish.from.should.equal('bws@dummy.net'); spanish.subject.should.contain('Nuevo pago recibido'); spanish.text.should.contain(wallet.name); - spanish.text.should.contain('0.123000 btc'); + spanish.text.should.contain('0.123 btc'); var english = _.find(emails, { to: 'copayer2@domain.com' }); From 15b0f45635199281ba92467478f854bb3a8630ec Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 26 Jun 2015 17:39:47 -0300 Subject: [PATCH 5/7] optional html body --- lib/emailservice.js | 68 ++++++++++++++++++++++++-------------- lib/model/email.js | 12 +++++-- test/integration/server.js | 12 +++++++ 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/lib/emailservice.js b/lib/emailservice.js index ee4d623..f82c78c 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -109,22 +109,33 @@ EmailService.prototype.start = function(opts, cb) { }); }; +EmailService.prototype._compileTemplate = function(template) { + var lines = template.split('\n'); + return { + subject: lines[0], + body: _.rest(lines).join('\n'), + }; +}; -// TODO: cache for X minutes -EmailService.prototype._readTemplate = function(filename, language, cb) { +EmailService.prototype._readTemplateFile = function(language, filename, cb) { var self = this; - var fullFilename = path.join(self.templatePath, language, filename + '.plain'); + var fullFilename = path.join(self.templatePath, language, filename); fs.readFile(fullFilename, 'utf8', function(err, template) { if (err) { - log.error('Could not read template file ' + fullFilename, err); - return cb(err); + return cb(new Error('Could not read template file ' + fullFilename, err)); } - var lines = template.split('\n'); - return cb(null, { - subject: lines[0], - body: _.rest(lines).join('\n'), - }); + return cb(null, template); + }); +}; + +// TODO: cache for X minutes +EmailService.prototype._loadTemplate = function(emailType, recipient, extension, cb) { + var self = this; + + self._readTemplateFile(recipient.language, emailType.filename + extension, function(err, template) { + if (err) return cb(err); + return cb(null, self._compileTemplate(template)); }); }; @@ -213,8 +224,11 @@ EmailService.prototype._send = function(email, cb) { from: email.from, to: email.to, subject: email.subject, - text: email.body, + text: email.bodyPlain, }; + if (email.bodyHtml) { + mailOptions.html = email.bodyHtml; + } self.mailer.sendMail(mailOptions, function(err, result) { if (err) { log.error('An error occurred when trying to send email to ' + email.to, err); @@ -233,20 +247,23 @@ EmailService.prototype._readAndApplyTemplates = function(notification, emailType async.waterfall([ function(next) { - async.parallel([ - - function(next) { - self._readTemplate(emailType.filename, recipient.language, next); - }, - function(next) { - self._getDataForTemplate(notification, recipient, next); - }, - ], next); + self._getDataForTemplate(notification, recipient, next); + }, + function(data, next) { + async.map(['plain', 'html'], function(type, next) { + self._loadTemplate(emailType, recipient, '.' + type, function(err, template) { + if (err && type == 'html') return next(); + if (err) return next(err); + self._applyTemplate(template, data, function(err, res) { + return next(err, [type, res]); + }); + }); + }, function(err, res) { + return next(err, _.zipObject(res)); + }); }, function(result, next) { - var template = result[0]; - var data = result[1]; - self._applyTemplate(template, data, next); + next(null, result); }, ], function(err, res) { next(err, [recipient.language, res]); @@ -288,8 +305,9 @@ EmailService.prototype.sendEmail = function(notification, cb) { copayerId: recipient.copayerId, from: self.from, to: recipient.emailAddress, - subject: content.subject, - body: content.body, + subject: content.plain.subject, + bodyPlain: content.plain.body, + bodyHtml: content.html ? content.html.body : null, notificationId: notification.id, }); self.storage.storeEmail(email, function(err) { diff --git a/lib/model/email.js b/lib/model/email.js index f6ebca6..5b169b9 100644 --- a/lib/model/email.js +++ b/lib/model/email.js @@ -4,7 +4,7 @@ var _ = require('lodash'); var Uuid = require('uuid'); function Email() { - this.version = '1.0.0'; + this.version = '1.0.1'; }; Email.create = function(opts) { @@ -20,7 +20,8 @@ Email.create = function(opts) { x.from = opts.from; x.to = opts.to; x.subject = opts.subject; - x.body = opts.body; + x.bodyPlain = opts.bodyPlain; + x.bodyHtml = opts.bodyHtml; x.status = 'pending'; x.attempts = 0; x.lastAttemptOn = null; @@ -39,7 +40,12 @@ Email.fromObj = function(obj) { x.from = obj.from; x.to = obj.to; x.subject = obj.subject; - x.body = obj.body; + if (obj.version == '1.0.0') { + x.bodyPlain = obj.body; + } else { + x.bodyPlain = obj.bodyPlain; + } + x.bodyHtml = obj.bodyHtml; x.status = obj.status; x.attempts = obj.attempts; x.lastAttemptOn = obj.lastAttemptOn; diff --git a/test/integration/server.js b/test/integration/server.js index e3a22a4..8dbc84b 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -350,6 +350,14 @@ describe('Wallet service', function() { }); it('should notify copayers a new tx proposal has been created', function(done) { + var _readTemplateFile_old = emailService._readTemplateFile; + emailService._readTemplateFile = function(language, filename, cb) { + if (_.endsWith(filename, '.html')) { + return cb(null, 'Subject\n{{walletName}}'); + } else { + _readTemplateFile_old.call(emailService, language, filename, cb); + } + }; helpers.stubUtxos(server, wallet, [1, 1], function() { var txOpts = helpers.createSimpleProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, 'some message', TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { @@ -366,9 +374,13 @@ describe('Wallet service', function() { one.subject.should.contain('New payment proposal'); one.text.should.contain(wallet.name); one.text.should.contain(wallet.copayers[0].name); + should.exist(one.html); + one.html.should.contain(''); + one.html.should.contain(wallet.name); server.storage.fetchUnsentEmails(function(err, unsent) { should.not.exist(err); unsent.should.be.empty; + emailService._readTemplateFile = _readTemplateFile_old; done(); }); }, 100); From 1be10e2384e29276448eaaa5ec6e2d5fdd4c0f54 Mon Sep 17 00:00:00 2001 From: dabura667 Date: Sat, 27 Jun 2015 10:11:51 +0900 Subject: [PATCH 6/7] Add Japanese to email templates --- lib/templates/ja/new_copayer.plain | 2 ++ lib/templates/ja/new_incoming_tx.plain | 2 ++ lib/templates/ja/new_outgoing_tx.plain | 2 ++ lib/templates/ja/new_tx_proposal.plain | 2 ++ lib/templates/ja/txp_finally_rejected.plain | 2 ++ lib/templates/ja/wallet_complete.plain | 2 ++ 6 files changed, 12 insertions(+) create mode 100644 lib/templates/ja/new_copayer.plain create mode 100644 lib/templates/ja/new_incoming_tx.plain create mode 100644 lib/templates/ja/new_outgoing_tx.plain create mode 100644 lib/templates/ja/new_tx_proposal.plain create mode 100644 lib/templates/ja/txp_finally_rejected.plain create mode 100644 lib/templates/ja/wallet_complete.plain diff --git a/lib/templates/ja/new_copayer.plain b/lib/templates/ja/new_copayer.plain new file mode 100644 index 0000000..acdbcd0 --- /dev/null +++ b/lib/templates/ja/new_copayer.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}ウォレットメンバー参加の知らせ +「{{walletName}}」のウォレットに新しいメンバーが加わりました。 diff --git a/lib/templates/ja/new_incoming_tx.plain b/lib/templates/ja/new_incoming_tx.plain new file mode 100644 index 0000000..eea9dde --- /dev/null +++ b/lib/templates/ja/new_incoming_tx.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}着金確認の知らせ +{{amount}} のビットコインがウォレット「{{walletName}}」に着金しました。 diff --git a/lib/templates/ja/new_outgoing_tx.plain b/lib/templates/ja/new_outgoing_tx.plain new file mode 100644 index 0000000..414d461 --- /dev/null +++ b/lib/templates/ja/new_outgoing_tx.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}送金のお知らせ +{{amount}}のビットコインがウォレット「{{walletName}}」から送金されました。 diff --git a/lib/templates/ja/new_tx_proposal.plain b/lib/templates/ja/new_tx_proposal.plain new file mode 100644 index 0000000..2fd7aeb --- /dev/null +++ b/lib/templates/ja/new_tx_proposal.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}送金の新規提案のお知らせ +「{{walletName}}」のウォレットにおいて {{copayerName}} さんが送金の提案をしました。 diff --git a/lib/templates/ja/txp_finally_rejected.plain b/lib/templates/ja/txp_finally_rejected.plain new file mode 100644 index 0000000..738597c --- /dev/null +++ b/lib/templates/ja/txp_finally_rejected.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}送金提案の却下のお知らせ +「{{walletName}}」のウォレットにおいて {{rejectorsNames}} さんが送金の提案を却下しました。 diff --git a/lib/templates/ja/wallet_complete.plain b/lib/templates/ja/wallet_complete.plain new file mode 100644 index 0000000..a315e31 --- /dev/null +++ b/lib/templates/ja/wallet_complete.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}ウォレット作成完了 +あなたの新しいウォレット「{{walletName}}」が完成されました。 From d3404a0dda9313a8fce3d0f5890e65141e0cd6d1 Mon Sep 17 00:00:00 2001 From: Kirvx Date: Sun, 28 Jun 2015 00:54:50 +0200 Subject: [PATCH 7/7] Add French to email templates --- lib/templates/fr/new_copayer.plain | 2 ++ lib/templates/fr/new_incoming_tx.plain | 2 ++ lib/templates/fr/new_outgoing_tx.plain | 2 ++ lib/templates/fr/new_tx_proposal.plain | 2 ++ lib/templates/fr/txp_finally_rejected.plain | 2 ++ lib/templates/fr/wallet_complete.plain | 2 ++ 6 files changed, 12 insertions(+) create mode 100644 lib/templates/fr/new_copayer.plain create mode 100644 lib/templates/fr/new_incoming_tx.plain create mode 100644 lib/templates/fr/new_outgoing_tx.plain create mode 100644 lib/templates/fr/new_tx_proposal.plain create mode 100644 lib/templates/fr/txp_finally_rejected.plain create mode 100644 lib/templates/fr/wallet_complete.plain diff --git a/lib/templates/fr/new_copayer.plain b/lib/templates/fr/new_copayer.plain new file mode 100644 index 0000000..c95f0dc --- /dev/null +++ b/lib/templates/fr/new_copayer.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Nouveau copayer +Un nouveau copayer vient de rejoindre votre portefeuille {{walletName}}. diff --git a/lib/templates/fr/new_incoming_tx.plain b/lib/templates/fr/new_incoming_tx.plain new file mode 100644 index 0000000..7d3ea86 --- /dev/null +++ b/lib/templates/fr/new_incoming_tx.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Nouveau paiement reçu +Un paiement de {{amount}} a été reçu dans votre portefeuille {{walletName}}. diff --git a/lib/templates/fr/new_outgoing_tx.plain b/lib/templates/fr/new_outgoing_tx.plain new file mode 100644 index 0000000..ed97826 --- /dev/null +++ b/lib/templates/fr/new_outgoing_tx.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Paiement envoyé +Un paiement de {{amount}} a été envoyé de votre portefeuille {{walletName}}. diff --git a/lib/templates/fr/new_tx_proposal.plain b/lib/templates/fr/new_tx_proposal.plain new file mode 100644 index 0000000..d0f70f2 --- /dev/null +++ b/lib/templates/fr/new_tx_proposal.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Nouvelle proposition de paiement +Une nouvelle proposition de paiement a été créée dans votre portefeuille {{walletName}} par {{copayerName}}. diff --git a/lib/templates/fr/txp_finally_rejected.plain b/lib/templates/fr/txp_finally_rejected.plain new file mode 100644 index 0000000..b25e1f7 --- /dev/null +++ b/lib/templates/fr/txp_finally_rejected.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Proposition de paiement rejetée +Une proposition de paiement dans votre portefeuille {{walletName}} a été rejetée par {{rejectorsNames}}. diff --git a/lib/templates/fr/wallet_complete.plain b/lib/templates/fr/wallet_complete.plain new file mode 100644 index 0000000..634b1f0 --- /dev/null +++ b/lib/templates/fr/wallet_complete.plain @@ -0,0 +1,2 @@ +{{subjectPrefix}}Portefeuille terminé +Votre portefeuille {{walletName}} est terminé.