From d61695932014f65af4eacbb733faadb306c4dfeb Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 27 Apr 2015 15:38:33 -0300 Subject: [PATCH 01/25] add preferences --- lib/model/index.js | 1 + lib/model/preferences.js | 30 +++++++++++++++++++++++ lib/server.js | 49 ++++++++++++++++++++++++++++++++++---- lib/storage.js | 21 ++++++++++++++++ test/integration/server.js | 45 ++++++++++++++++++++++++++++++++-- 5 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 lib/model/preferences.js diff --git a/lib/model/index.js b/lib/model/index.js index 3a3fcf2..1061a30 100644 --- a/lib/model/index.js +++ b/lib/model/index.js @@ -5,5 +5,6 @@ Model.Copayer = require('./copayer'); Model.TxProposal = require('./txproposal'); Model.Address = require('./address'); Model.Notification = require('./notification'); +Model.Preferences = require('./preferences'); module.exports = Model; diff --git a/lib/model/preferences.js b/lib/model/preferences.js new file mode 100644 index 0000000..ab2fb92 --- /dev/null +++ b/lib/model/preferences.js @@ -0,0 +1,30 @@ +'use strict'; + +function Preferences() { + this.version = '1.0.0'; +}; + +Preferences.create = function(opts) { + opts = opts || {}; + + var x = new Preferences(); + + x.createdOn = Math.floor(Date.now() / 1000); + x.walletId = opts.walletId; + x.copayerId = opts.copayerId; + x.email = opts.email; + return x; +}; + +Preferences.fromObj = function(obj) { + var x = new Preferences(); + + x.createdOn = obj.createdOn; + x.walletId = obj.walletId; + x.copayerId = obj.copayerId; + x.email = obj.email; + return x; +}; + + +module.exports = Preferences; diff --git a/lib/server.js b/lib/server.js index bf71221..8df35ad 100644 --- a/lib/server.js +++ b/lib/server.js @@ -19,11 +19,13 @@ var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); var BlockchainExplorer = require('./blockchainexplorer'); -var Wallet = require('./model/wallet'); -var Copayer = require('./model/copayer'); -var Address = require('./model/address'); -var TxProposal = require('./model/txproposal'); -var Notification = require('./model/notification'); +var Model = require('./model'); +var Wallet = Model.Wallet; +var Copayer = Model.Copayer; +var Address = Model.Address; +var TxProposal = Model.TxProposal; +var Notification = Model.Notification; +var Preferences = Model.Preferences; var initialized = false; var lock, storage, blockchainExplorer, blockchainExplorerOpts; @@ -412,6 +414,43 @@ 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. + */ +WalletService.prototype.savePreferences = function(opts, cb) { + var self = this; + + opts = opts || {}; + + self._runLocked(cb, function(cb) { + var preferences = Preferences.create({ + walletId: self.walletId, + copayerId: self.copayerId, + email: opts.email, + }); + self.storage.storePreferences(preferences, function(err) { + return cb(err); + }); + }); +}; + +/** + * Retrieves a preferences for the current wallet/copayer pair. + * @param {Object} opts + * @returns {Object} preferences + */ +WalletService.prototype.getPreferences = function(opts, cb) { + var self = this; + + self.storage.fetchPreferences(self.walletId, self.copayerId, function(err, preferences) { + if (err) return cb(err); + return cb(null, preferences || {}); + }); +}; + + /** * Creates a new address. * @param {Object} opts diff --git a/lib/storage.js b/lib/storage.js index 8a96663..065e897 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -18,6 +18,7 @@ var collections = { ADDRESSES: 'addresses', NOTIFICATIONS: 'notifications', COPAYERS_LOOKUP: 'copayers_lookup', + PREFERENCES: 'preferences', }; var Storage = function(opts) { @@ -353,6 +354,26 @@ Storage.prototype.fetchAddress = function(address, cb) { }); }; +Storage.prototype.fetchPreferences = function(walletId, copayerId, cb) { + this.db.collection(collections.PREFERENCES).findOne({ + walletId: walletId, + copayerId: copayerId, + }, function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + return cb(null, Model.Preferences.fromObj(result)); + }); +}; + +Storage.prototype.storePreferences = function(preferences, cb) { + this.db.collection(collections.PREFERENCES).update({ + walletId: preferences.walletId, + copayerId: preferences.copayerId, + }, preferences, { + w: 1, + upsert: true, + }, cb); +}; Storage.prototype._dump = function(cb, fn) { fn = fn || console.log; diff --git a/test/integration/server.js b/test/integration/server.js index 8828674..5942a95 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -25,6 +25,7 @@ var Wallet = Model.Wallet; var TxProposal = Model.TxProposal; var Address = Model.Address; var Copayer = Model.Copayer; +var Preferences = Model.Preferences; var WalletService = require('../../lib/server'); var TestData = require('../testdata'); @@ -673,7 +674,7 @@ describe('Wallet service', function() { it('should create address', function(done) { server.createAddress({}, function(err, address) { should.not.exist(err); - address.should.exist; + should.exist(address); address.walletId.should.equal(wallet.id); address.network.should.equal('livenet'); address.address.should.equal('3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg'); @@ -718,7 +719,7 @@ describe('Wallet service', function() { server.storage.storeAddressAndWallet.restore(); server.createAddress({}, function(err, address) { should.not.exist(err); - address.should.exist; + should.exist(address); address.address.should.equal('3KxttbKQQPWmpsnXZ3rB4mgJTuLnVR7frg'); address.path.should.equal('m/2147483647/0/0'); done(); @@ -728,6 +729,46 @@ describe('Wallet service', function() { }); }); + describe.only('Preferences', function() { + var server, wallet; + beforeEach(function(done) { + helpers.createAndJoinWallet(2, 2, function(s, w) { + server = s; + wallet = w; + done(); + }); + }); + + it('should save & retrieve preferences', function(done) { + server.savePreferences({ + email: 'dummy@dummy.com' + }, 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'); + done(); + }); + }); + }); + it('should save preferences only for requesting copayer', function(done) { + server.savePreferences({ + email: 'dummy@dummy.com' + }, function(err) { + should.not.exist(err); + helpers.getAuthServer(wallet.copayers[1].id, function(server2) { + server2.getPreferences({}, function(err, preferences) { + should.not.exist(err); + should.not.exist(preferences.email); + done(); + }); + }); + }); + }); + it.skip('should save preferences only for requesting wallet', function(done) {}); + }); + describe('#getBalance', function() { var server, wallet; beforeEach(function(done) { From eace1295eae19d39918180b83f75389bdbfe950c Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 27 Apr 2015 15:55:42 -0300 Subject: [PATCH 02/25] add preferences to wallet status --- lib/expressapp.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/expressapp.js b/lib/expressapp.js index 4b7dad2..1bdad35 100644 --- a/lib/expressapp.js +++ b/lib/expressapp.js @@ -178,6 +178,13 @@ ExpressApp.prototype.start = function(opts, cb) { next(); }); }, + function(next) { + server.getPreferences({}, function(err, preferences) { + if (err) return next(err); + result.preferences = preferences; + next(); + }); + }, ], function(err) { if (err) return returnError(err, res, req); res.json(result); From de037a322793f8de37be156d6a89ad3a668ab956 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 28 Apr 2015 09:47:25 -0300 Subject: [PATCH 03/25] add email model --- lib/model/email.js | 40 ++++++++++++++++++++++++++++++++++++++++ lib/storage.js | 21 +++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 lib/model/email.js diff --git a/lib/model/email.js b/lib/model/email.js new file mode 100644 index 0000000..f3a90ab --- /dev/null +++ b/lib/model/email.js @@ -0,0 +1,40 @@ +'use strict'; + +function Email() { + this.version = '1.0.0'; +}; + +Email.create = function(opts) { + opts = opts || {}; + + var x = new Email(); + + x.createdOn = Math.floor(Date.now() / 1000); + x.walletId = opts.walletId; + x.copayerId = opts.copayerId; + x.to = opts.to; + x.subject = opts.subject; + x.body = opts.body; + x.status = 'pending'; + x.attempts = 0; + x.sentOn = null; + return x; +}; + +Email.fromObj = function(obj) { + var x = new Email(); + + x.createdOn = obj.createdOn; + x.walletId = obj.walletId; + x.copayerId = obj.copayerId; + x.to = obj.to; + x.subject = obj.subject; + x.body = obj.body; + x.status = obj.status; + x.attempts = obj.attempts; + x.sentOn = obj.sentOn; + return x; +}; + + +module.exports = Email; diff --git a/lib/storage.js b/lib/storage.js index 065e897..8b29a3c 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -19,6 +19,7 @@ var collections = { NOTIFICATIONS: 'notifications', COPAYERS_LOOKUP: 'copayers_lookup', PREFERENCES: 'preferences', + EMAIL_QUEUE: 'email_queue', }; var Storage = function(opts) { @@ -375,6 +376,26 @@ Storage.prototype.storePreferences = function(preferences, cb) { }, cb); }; +Storage.prototype.storeEmail = function(email, cb) { + this.db.collection(collections.EMAIL_QUEUE).update({ + id: email.id, + }, txp, { + w: 1, + upsert: true, + }, cb); +}; + +Storage.prototype.fetchUnsentEmails = function(cb) { + this.db.collection(collections.EMAIL_QUEUE).find({ + status: 'pending', + }, function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + return cb(null, Model.Email.fromObj(result)); + }); +}; + + Storage.prototype._dump = function(cb, fn) { fn = fn || console.log; cb = cb || function() {}; From 37a02c37ecd7d539c25f2d1dc37e75f913c17cf2 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 28 Apr 2015 10:34:26 -0300 Subject: [PATCH 04/25] refactor notification dispatching --- lib/blockchainmonitor.js | 45 ++++++++++++++++++++++++++++++++++ lib/notificationbroadcaster.js | 25 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 lib/notificationbroadcaster.js diff --git a/lib/blockchainmonitor.js b/lib/blockchainmonitor.js index 3d2a245..8495400 100644 --- a/lib/blockchainmonitor.js +++ b/lib/blockchainmonitor.js @@ -10,11 +10,15 @@ var BlockchainExplorer = require('./blockchainexplorer'); var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); +<<<<<<< HEAD var Notification = require('./model/notification'); function BlockchainMonitor() {}; BlockchainMonitor.prototype.start = function(opts, cb) { +======= +function BlockchainMonitor(opts) { +>>>>>>> refactor notification dispatching opts = opts || {}; $.checkArgument(opts.blockchainExplorerOpts); $.checkArgument(opts.storageOpts); @@ -65,23 +69,42 @@ BlockchainMonitor.prototype._initExplorer = function(provider, network, url) { }); socket.on('tx', _.bind(self._handleIncommingTx, self)); +<<<<<<< HEAD return explorer; }; +======= +BlockchainMonitor.prototype.subscribeAddresses = function(walletService, addresses) { + $.checkArgument(walletService); + $.checkArgument(walletService.walletId); +>>>>>>> refactor notification dispatching BlockchainMonitor.prototype._handleIncommingTx = function(data) { var self = this; + var walletId = walletService.walletId; if (!data || !data.vout) return; +<<<<<<< HEAD var outs = _.compact(_.map(data.vout, function(v) { var addr = _.keys(v)[0]; var startingChar = addr.charAt(0); if (startingChar != '2' && startingChar != '3') return; +======= + function handlerFor(address, txid) { + var data = { + walletId: this.walletId, + address: address, + txid: txid, + }; + self.emit('NewIncomingTx', data, this); + }; +>>>>>>> refactor notification dispatching return { address: addr, amount: +v[addr] }; +<<<<<<< HEAD })); if (_.isEmpty(outs)) return; @@ -99,12 +122,23 @@ BlockchainMonitor.prototype._handleIncommingTx = function(data) { }); }, function(err) { return; +======= + }; + + var addresses = [].concat(addresses); + var network = Bitcore.Address.fromString(addresses[0]).network.name; + var subscriber = self.subscriber[network]; + _.each(addresses, function(address) { + self.subscriptions[walletId].addresses.push(address); + subscriber.subscribe(address, _.bind(handlerFor, walletService, address)); +>>>>>>> refactor notification dispatching }); }; BlockchainMonitor.prototype._createNotification = function(walletId, txid, address, amount, cb) { var self = this; +<<<<<<< HEAD var n = Notification.create({ type: 'NewIncomingTx', data: { @@ -116,6 +150,17 @@ BlockchainMonitor.prototype._createNotification = function(walletId, txid, addre }); self.storage.storeNotification(walletId, n, function() { self.messageBroker.send(n) +======= + var walletId = walletService.walletId; + if (self.subscriptions[walletId]) return; + + walletService.getMainAddresses({}, function(err, addresses) { + if (err) { + delete self.subscriptions[walletId]; + return cb(new Error('Could not subscribe to addresses for wallet ' + walletId)); + } + self.subscribeAddresses(walletService, _.pluck(addresses, 'address')); +>>>>>>> refactor notification dispatching return cb(); }); }; diff --git a/lib/notificationbroadcaster.js b/lib/notificationbroadcaster.js new file mode 100644 index 0000000..4357d3d --- /dev/null +++ b/lib/notificationbroadcaster.js @@ -0,0 +1,25 @@ +'use strict'; + +var log = require('npmlog'); +log.debug = log.verbose; +var inherits = require('inherits'); +var events = require('events'); +var nodeutil = require('util'); + +function NotificationBroadcaster() {}; + +nodeutil.inherits(NotificationBroadcaster, events.EventEmitter); + +NotificationBroadcaster.prototype.broadcast = function(eventName, notification, walletService) { + this.emit(eventName, notification, walletService); +}; + +var _instance; +NotificationBroadcaster.singleton = function() { + if (!_instance) { + _instance = new NotificationBroadcaster(); + } + return _instance; +}; + +module.exports = NotificationBroadcaster.singleton(); From 39b254b163e8b315a88a217dcd8296ecb4c5d4df Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Tue, 28 Apr 2015 12:49:43 -0300 Subject: [PATCH 05/25] fix tests --- lib/model/index.js | 1 + lib/server.js | 14 +++++--------- test/integration/server.js | 7 +------ 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/model/index.js b/lib/model/index.js index 1061a30..eeb60e9 100644 --- a/lib/model/index.js +++ b/lib/model/index.js @@ -6,5 +6,6 @@ Model.TxProposal = require('./txproposal'); Model.Address = require('./address'); Model.Notification = require('./notification'); Model.Preferences = require('./preferences'); +Model.Email = require('./email'); module.exports = Model; diff --git a/lib/server.js b/lib/server.js index 8df35ad..92b46f2 100644 --- a/lib/server.js +++ b/lib/server.js @@ -21,11 +21,6 @@ var BlockchainExplorer = require('./blockchainexplorer'); var Model = require('./model'); var Wallet = Model.Wallet; -var Copayer = Model.Copayer; -var Address = Model.Address; -var TxProposal = Model.TxProposal; -var Notification = Model.Notification; -var Preferences = Model.Preferences; var initialized = false; var lock, storage, blockchainExplorer, blockchainExplorerOpts; @@ -329,7 +324,7 @@ WalletService.prototype._notify = function(type, data, opts, cb) { $.checkState(walletId); - var n = Notification.create({ + var notification = Model.Notification.create({ type: type, data: data, ticker: this.notifyTicker++, @@ -342,6 +337,7 @@ WalletService.prototype._notify = function(type, data, opts, cb) { }); }; + /** * Joins a wallet in creation. * @param {Object} opts @@ -380,7 +376,7 @@ WalletService.prototype.joinWallet = function(opts, cb) { if (wallet.copayers.length == wallet.n) return cb(new ClientError('WFULL', 'Wallet full')); - var copayer = Copayer.create({ + var copayer = Model.Copayer.create({ name: opts.name, copayerIndex: wallet.copayers.length, xPubKey: opts.xPubKey, @@ -425,7 +421,7 @@ WalletService.prototype.savePreferences = function(opts, cb) { opts = opts || {}; self._runLocked(cb, function(cb) { - var preferences = Preferences.create({ + var preferences = Model.Preferences.create({ walletId: self.walletId, copayerId: self.copayerId, email: opts.email, @@ -746,7 +742,7 @@ WalletService.prototype.createTx = function(opts, cb) { var changeAddress = wallet.createAddress(true); - var txp = TxProposal.create({ + var txp = Model.TxProposal.create({ walletId: self.walletId, creatorId: self.copayerId, toAddress: opts.toAddress, diff --git a/test/integration/server.js b/test/integration/server.js index 5942a95..bb1e8b8 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -21,11 +21,6 @@ var Bitcore = WalletUtils.Bitcore; var Storage = require('../../lib/storage'); var Model = require('../../lib/model'); -var Wallet = Model.Wallet; -var TxProposal = Model.TxProposal; -var Address = Model.Address; -var Copayer = Model.Copayer; -var Preferences = Model.Preferences; var WalletService = require('../../lib/server'); var TestData = require('../testdata'); @@ -729,7 +724,7 @@ describe('Wallet service', function() { }); }); - describe.only('Preferences', function() { + describe('Preferences', function() { var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(2, 2, function(s, w) { From 12232dbe16c97d5fb4b8bb4aaa8a65da6d81d6eb Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 29 Apr 2015 00:34:18 -0300 Subject: [PATCH 06/25] email service --- lib/emailservice.js | 111 ++++++++++++++++++++++++++++ lib/server.js | 44 ++++++++--- lib/storage.js | 24 ++++-- lib/templates/new_tx_proposal.plain | 2 + 4 files changed, 164 insertions(+), 17 deletions(-) create mode 100644 lib/emailservice.js create mode 100644 lib/templates/new_tx_proposal.plain diff --git a/lib/emailservice.js b/lib/emailservice.js new file mode 100644 index 0000000..15e54fb --- /dev/null +++ b/lib/emailservice.js @@ -0,0 +1,111 @@ +'use strict'; + +var _ = require('lodash'); +var $ = require('preconditions').singleton(); +var async = require('async'); +var log = require('npmlog'); +log.debug = log.verbose; +var fs = require('fs'); + +var Model = require('./model'); + +var EMAIL_TYPES = { + 'NewTxProposal': { + filename: 'new_tx_proposal', + notifyCreator: false, + notifyDoer: false, + }, + 'NewOutgoingTx': { + filename: 'new_outgoing_tx', + notifyCreator: true, + notifyDoer: true, + }, + 'NewIncomingTx': { + filename: 'new_incoming_tx', + notifyCreator: true, + notifyDoer: true, + }, + 'TxProposalFinallyRejected': { + filename: 'txp_finally_rejected', + notifyCreator: true, + notifyDoer: false, + }, +}; + + +function EmailService(opts) { + this.storage = opts.storage; + this.lock = opts.lock; +}; + +EmailService.prototype._readTemplate = function(filename, cb) { + fs.readFile(__dirname + '/templates/' + filename + '.plain', 'utf8', function(err, template) { + var lines = template.split('\n'); + return cb(null, { + subject: _.template(lines[0]), + body: _.template(_.rest(lines).join('\n')), + }); + }); +}; + +EmailService.prototype._applyTemplate = function(template, data, cb) { + var result = _.mapValues(template, function(t) { + // TODO: If this throws exception, log and abort email generation + return t(data); + }); + return cb(null, result); +}; + +EmailService.prototype._generateFromNotification = function(notification, cb) { + var self = this; + + var emailType = EMAIL_TYPES[notification.type]; + if (!emailType) return cb(); + + self.storage.fetchPreferences(notification.walletId, null, function(err, preferences) { + if (_.isEmpty(preferences)) return cb(); + + var addressesByCopayer = _.reduce(preferences, function(memo, p) { + if (p.email) { + memo[p.copayerId] = p.email; + } + return memo; + }, {}); + + if (_.isEmpty(addressesByCopayer)) return cb(); + + self._readTemplate(emailType.filename, function(err, template) { + if (err) return cb(err); + + self._applyTemplate(template, notification.data, function(err, content) { + if (err) return cb(err); + + _.each(addressesByCopayer, function(emailAddress, copayerId) { + var email = Model.Email.create({ + walletId: notification.walletId, + copayerId: copayerId, + to: emailAddress, + subject: content.subject, + body: content.body, + }); + self.storage.storeEmail(email, function(err) { + return cb(err); + }); + }); + }); + }); + }); + + return cb(); +}; + +EmailService.prototype._send = function(cb) { + var self = this; + + this.lock.runLocked('emails', cb, function() { + //self._fetchUnsentEmails(); + + }); +}; + +module.exports = EmailService; diff --git a/lib/server.js b/lib/server.js index 92b46f2..d58263f 100644 --- a/lib/server.js +++ b/lib/server.js @@ -18,13 +18,19 @@ var Lock = require('./lock'); var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); var BlockchainExplorer = require('./blockchainexplorer'); +var EmailService = require('./emailservice'); var Model = require('./model'); var Wallet = Model.Wallet; var initialized = false; -var lock, storage, blockchainExplorer, blockchainExplorerOpts; + +var lock; +var storage; +var blockchainExplorer; +var blockchainExplorerOpts; var messageBroker; +var emailService; /** @@ -41,6 +47,7 @@ function WalletService() { this.blockchainExplorerOpts = blockchainExplorerOpts; this.messageBroker = messageBroker; this.notifyTicker = 0; + this.emailService = emailService; }; /** @@ -58,20 +65,28 @@ WalletService.initialize = function(opts, cb) { blockchainExplorer = opts.blockchainExplorer; blockchainExplorerOpts = opts.blockchainExplorerOpts; - if (initialized) - return cb(); + if (initialized) return cb(); function initStorage(cb) { if (opts.storage) { storage = opts.storage; return cb(); + } else { + var newStorage = new Storage(); + newStorage.connect(opts.storageOpts, function(err) { + if (err) return cb(err); + storage = newStorage; + return cb(); + }); } - var newStorage = new Storage(); - newStorage.connect(opts.storageOpts, function(err) { - if (err) return cb(err); - storage = newStorage; - return cb(); + }; + + function initEmailService(cb) { + emailService = new EmailService({ + lock: lock, + storage: storage, }); + return cb(); }; function initMessageBroker(cb) { @@ -91,11 +106,15 @@ WalletService.initialize = function(opts, cb) { function(next) { initMessageBroker(next); }, + function(next) { + initEmailService(next); + }, ], function(err) { if (err) { log.error('Could not initialize', err); throw err; } + ], function() { initialized = true; return cb(); }); @@ -331,9 +350,12 @@ WalletService.prototype._notify = function(type, data, opts, cb) { creatorId: opts.isGlobal ? null : copayerId, walletId: walletId, }); - this.storage.storeNotification(walletId, n, function() { - self.messageBroker.send(n); - if (cb) return cb(); + + this.storage.storeNotification(walletId, notification, function() { + self.messageBroker.send(notification); + self.emailService._generateFromNotification(notification, function() { + if (cb) return cb(); + }); }); }; diff --git a/lib/storage.js b/lib/storage.js index 8b29a3c..b340f3e 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -356,13 +356,25 @@ Storage.prototype.fetchAddress = function(address, cb) { }; Storage.prototype.fetchPreferences = function(walletId, copayerId, cb) { - this.db.collection(collections.PREFERENCES).findOne({ + this.db.collection(collections.PREFERENCES).find({ walletId: walletId, - copayerId: copayerId, - }, function(err, result) { + }).toArray(function(err, result) { if (err) return cb(err); + + if (copayerId) { + result = _.find(result, { + copayerId: copayerId + }); + } if (!result) return cb(); - return cb(null, Model.Preferences.fromObj(result)); + + var preferences = _.map([].concat(result), function(r) { + return Model.Preferences.fromObj(r); + }); + if (copayerId) { + preferences = preferences[0]; + } + return cb(null, preferences); }); }; @@ -379,7 +391,7 @@ Storage.prototype.storePreferences = function(preferences, cb) { Storage.prototype.storeEmail = function(email, cb) { this.db.collection(collections.EMAIL_QUEUE).update({ id: email.id, - }, txp, { + }, email, { w: 1, upsert: true, }, cb); @@ -388,7 +400,7 @@ Storage.prototype.storeEmail = function(email, cb) { Storage.prototype.fetchUnsentEmails = function(cb) { this.db.collection(collections.EMAIL_QUEUE).find({ status: 'pending', - }, function(err, result) { + }).toArray(function(err, result) { if (err) return cb(err); if (!result) return cb(); return cb(null, Model.Email.fromObj(result)); diff --git a/lib/templates/new_tx_proposal.plain b/lib/templates/new_tx_proposal.plain new file mode 100644 index 0000000..2c39404 --- /dev/null +++ b/lib/templates/new_tx_proposal.plain @@ -0,0 +1,2 @@ +New transaction proposal! +A new transaction proposal has been created by another copayer. From b78395b8514945465bbb4a0c788286c7a28fcea9 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 29 Apr 2015 12:10:01 -0300 Subject: [PATCH 07/25] add nodemailer + templates --- config.js | 7 ++ lib/emailservice.js | 116 +++++++++++++++-------- lib/model/email.js | 2 + lib/templates/new_incoming_tx.plain | 3 + lib/templates/new_outgoing_tx.plain | 3 + lib/templates/new_tx_proposal.plain | 3 +- lib/templates/txp_finally_rejected.plain | 3 + package.json | 1 + 8 files changed, 96 insertions(+), 42 deletions(-) create mode 100644 lib/templates/new_incoming_tx.plain create mode 100644 lib/templates/new_outgoing_tx.plain create mode 100644 lib/templates/txp_finally_rejected.plain diff --git a/config.js b/config.js index e64b966..d114e60 100644 --- a/config.js +++ b/config.js @@ -39,5 +39,12 @@ var config = { url: 'https://test-insight.bitpay.com:443', }, }, + email: { + service: 'Gmail', + auth: { + user: '', + pass: '' + } + } }; module.exports = config; diff --git a/lib/emailservice.js b/lib/emailservice.js index 15e54fb..4a212c8 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -6,28 +6,25 @@ var async = require('async'); var log = require('npmlog'); log.debug = log.verbose; var fs = require('fs'); +var nodemailer = require('nodemailer'); var Model = require('./model'); var EMAIL_TYPES = { 'NewTxProposal': { filename: 'new_tx_proposal', - notifyCreator: false, notifyDoer: false, }, 'NewOutgoingTx': { filename: 'new_outgoing_tx', - notifyCreator: true, notifyDoer: true, }, 'NewIncomingTx': { filename: 'new_incoming_tx', - notifyCreator: true, notifyDoer: true, }, 'TxProposalFinallyRejected': { filename: 'txp_finally_rejected', - notifyCreator: true, notifyDoer: false, }, }; @@ -36,10 +33,17 @@ var EMAIL_TYPES = { function EmailService(opts) { this.storage = opts.storage; this.lock = opts.lock; + this.mailer = opts.mailer || nodemailer.createTransport(opts.email); + $.checkState(this.mailer); }; +// TODO: cache for X minutes EmailService.prototype._readTemplate = function(filename, cb) { fs.readFile(__dirname + '/templates/' + filename + '.plain', 'utf8', function(err, template) { + if (err) { + log.error('Could not read template file ' + filename, err); + return cb(err); + } var lines = template.split('\n'); return cb(null, { subject: _.template(lines[0]), @@ -50,20 +54,20 @@ EmailService.prototype._readTemplate = function(filename, cb) { EmailService.prototype._applyTemplate = function(template, data, cb) { var result = _.mapValues(template, function(t) { - // TODO: If this throws exception, log and abort email generation - return t(data); + try { + return t(data); + } catch (e) { + log.error('Could not apply data to template', e); + return cb(e); + } }); return cb(null, result); }; -EmailService.prototype._generateFromNotification = function(notification, cb) { - var self = this; - - var emailType = EMAIL_TYPES[notification.type]; - if (!emailType) return cb(); - - self.storage.fetchPreferences(notification.walletId, null, function(err, preferences) { - if (_.isEmpty(preferences)) return cb(); +EmailService.prototype._getEmailAddresses = function(walletId, cb) { + self.storage.fetchPreferences(walletId, null, function(err, preferences) { + if (err) return cb(err); + if (_.isEmpty(preferences)) return cb(null, {}); var addressesByCopayer = _.reduce(preferences, function(memo, p) { if (p.email) { @@ -72,40 +76,70 @@ EmailService.prototype._generateFromNotification = function(notification, cb) { return memo; }, {}); - if (_.isEmpty(addressesByCopayer)) return cb(); - - self._readTemplate(emailType.filename, function(err, template) { - if (err) return cb(err); - - self._applyTemplate(template, notification.data, function(err, content) { - if (err) return cb(err); - - _.each(addressesByCopayer, function(emailAddress, copayerId) { - var email = Model.Email.create({ - walletId: notification.walletId, - copayerId: copayerId, - to: emailAddress, - subject: content.subject, - body: content.body, - }); - self.storage.storeEmail(email, function(err) { - return cb(err); - }); - }); - }); - }); + return cb(null, addressesByCopayer); }); +}; - return cb(); +EmailService.prototype._send = function(email, cb) { + var self = this; + + var mailOptions = { + from: email.from, + to: email.to, + subject: email.subject, + text: email.body, + }; + self.mailer.sendMail(mailOptions, function(err, result) { + if (err) { + log.error('An error occurred when trying to send email to ' + email.to, err); + return cb(err); + } + log.debug('Message sent: ', result || ''); + return cb(err, result); + }); }; -EmailService.prototype._send = function(cb) { +EmailService.prototype._generateFromNotification = function(notification, cb) { var self = this; - this.lock.runLocked('emails', cb, function() { - //self._fetchUnsentEmails(); + var emailType = EMAIL_TYPES[notification.type]; + if (!emailType) return cb(); - }); + var emailByCopayer; + + async.waterfall([ + + function(next) { + self._getEmailAddresses(notification.walletId, next); + }, + function(emailAddresses, next) { + if (_.isEmpty(emailAddresses)) return cb(); + emailByCopayer = emailAddresses; + self._readTemplate(emailType.filename, next); + }, + function(template, next) { + self._applyTemplate(template, notification.data, next); + }, + function(content, next) { + _.each(emailByCopayer, function(address, copayerId) { + var email = Model.Email.create({ + walletId: notification.walletId, + copayerId: copayerId, + to: address, + subject: content.subject, + body: content.body, + }); + self.storage.storeEmail(email, function(err) { + return next(err, email); + }); + }); + }, + function(email, next) { + self._send(email, next); + }, + ], cb); + + return cb(); }; module.exports = EmailService; diff --git a/lib/model/email.js b/lib/model/email.js index f3a90ab..002facf 100644 --- a/lib/model/email.js +++ b/lib/model/email.js @@ -12,6 +12,7 @@ Email.create = function(opts) { x.createdOn = Math.floor(Date.now() / 1000); x.walletId = opts.walletId; x.copayerId = opts.copayerId; + x.from = opts.from; x.to = opts.to; x.subject = opts.subject; x.body = opts.body; @@ -27,6 +28,7 @@ Email.fromObj = function(obj) { x.createdOn = obj.createdOn; x.walletId = obj.walletId; x.copayerId = obj.copayerId; + x.from = obj.from; x.to = obj.to; x.subject = obj.subject; x.body = obj.body; diff --git a/lib/templates/new_incoming_tx.plain b/lib/templates/new_incoming_tx.plain new file mode 100644 index 0000000..51d0575 --- /dev/null +++ b/lib/templates/new_incoming_tx.plain @@ -0,0 +1,3 @@ +copay@copay.io +[Copay] Funds received! +Funds received on your wallet. diff --git a/lib/templates/new_outgoing_tx.plain b/lib/templates/new_outgoing_tx.plain new file mode 100644 index 0000000..52036b1 --- /dev/null +++ b/lib/templates/new_outgoing_tx.plain @@ -0,0 +1,3 @@ +copay@copay.io +[Copay] Transaction broadcasted! +A transaction has been broadcasted. diff --git a/lib/templates/new_tx_proposal.plain b/lib/templates/new_tx_proposal.plain index 2c39404..643f616 100644 --- a/lib/templates/new_tx_proposal.plain +++ b/lib/templates/new_tx_proposal.plain @@ -1,2 +1,3 @@ -New transaction proposal! +copay@copay.io +[Copay] New transaction proposal! A new transaction proposal has been created by another copayer. diff --git a/lib/templates/txp_finally_rejected.plain b/lib/templates/txp_finally_rejected.plain new file mode 100644 index 0000000..70cc64e --- /dev/null +++ b/lib/templates/txp_finally_rejected.plain @@ -0,0 +1,3 @@ +copay@copay.io +[Copay] A transaction proposal was rejected +A transaction proposal was rejected by your copayers. diff --git a/package.json b/package.json index 17be107..9fbc3ea 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "mocha-lcov-reporter": "0.0.1", "mongodb": "^2.0.27", "morgan": "*", + "nodemailer": "^1.3.4", "npmlog": "^0.1.1", "preconditions": "^1.0.7", "read": "^1.0.5", From 4a0a3f1bad22caa58b79337c500c7a5f5197cde3 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 29 Apr 2015 14:35:07 -0300 Subject: [PATCH 08/25] filter recipient list --- lib/emailservice.js | 77 ++++++++++++++++++++++++++++++++------------- lib/lock.js | 2 +- lib/server.js | 2 +- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/lib/emailservice.js b/lib/emailservice.js index 4a212c8..5e78eb9 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -64,19 +64,44 @@ EmailService.prototype._applyTemplate = function(template, data, cb) { return cb(null, result); }; -EmailService.prototype._getEmailAddresses = function(walletId, cb) { - self.storage.fetchPreferences(walletId, null, function(err, preferences) { +EmailService.prototype._getRecipientsList = function(notification, emailType, cb) { + var self = this; + + self.storage.fetchPreferences(notification.walletId, null, function(err, preferences) { if (err) return cb(err); if (_.isEmpty(preferences)) return cb(null, {}); - var addressesByCopayer = _.reduce(preferences, function(memo, p) { - if (p.email) { - memo[p.copayerId] = p.email; - } - return memo; - }, {}); + var recipients = _.compact(_.map(preferences, function(p) { + if (!p.email) return; + if (notification.creatorId == p.copayerId && !emailType.notifyDoer) return; + return { + copayerId: p.copayerId, + emailAddress: p.email + }; + })); + + return cb(null, recipients); + }); +}; + +EmailService.prototype._getDataForTemplate = function(notification, cb) { + var self = this; - return cb(null, addressesByCopayer); + var data = _.cloneDeep(notification.data); + self.storage.fetchWallet(notification.walletId, function(err, wallet) { + if (err) return cb(err); + data.walletId = wallet.id; + data.walletName = wallet.name; + data.walletM = wallet.m; + data.walletN = wallet.n; + var copayer = _.find(wallet.copayers, { + copayerId: notification.creatorId + }); + if (copayer) { + data.creatorId = copayer.id; + data.creatorName = copayer.name; + } + return cb(null, data); }); }; @@ -99,33 +124,43 @@ EmailService.prototype._send = function(email, cb) { }); }; -EmailService.prototype._generateFromNotification = function(notification, cb) { +EmailService.prototype.sendEmail = function(notification, cb) { var self = this; var emailType = EMAIL_TYPES[notification.type]; if (!emailType) return cb(); - var emailByCopayer; + var recipientsList; async.waterfall([ function(next) { - self._getEmailAddresses(notification.walletId, next); + self._getRecipientsList(notification, emailType, function(err, list) { + if (_.isEmpty(list)) return cb(); + recipientsList = list; + return next(); + }); }, - function(emailAddresses, next) { - if (_.isEmpty(emailAddresses)) return cb(); - emailByCopayer = emailAddresses; - self._readTemplate(emailType.filename, next); + function(next) { + async.parallel([ + + function(next) { + self._readTemplate(emailType.filename, next); + }, + function(next) { + self._getDataForTemplate(notification, next); + }, + ], next); }, - function(template, next) { - self._applyTemplate(template, notification.data, next); + function(template, data, next) { + self._applyTemplate(template, data, next); }, function(content, next) { - _.each(emailByCopayer, function(address, copayerId) { + _.each(recipientsList, function(recipient) { var email = Model.Email.create({ walletId: notification.walletId, - copayerId: copayerId, - to: address, + copayerId: recipient.copayerId, + to: recipient.emailAddress, subject: content.subject, body: content.body, }); diff --git a/lib/lock.js b/lib/lock.js index 9bdaf37..255efb3 100644 --- a/lib/lock.js +++ b/lib/lock.js @@ -12,7 +12,7 @@ function Lock(opts) { if (opts.lockerServer) { this.lock = new RemoteLock(opts.lockerServer.port, opts.lockerServer.host); - log.info('Using locker server:' + opts.lockerServer.host + ':' + opts.lockerServer.port); + log.info('Using locker server:' + opts.lockerServer.host + ':' + opts.lockerServer.port); this.lock.on('reset', function() { log.debug('Locker server reset'); diff --git a/lib/server.js b/lib/server.js index d58263f..1a15a87 100644 --- a/lib/server.js +++ b/lib/server.js @@ -353,7 +353,7 @@ WalletService.prototype._notify = function(type, data, opts, cb) { this.storage.storeNotification(walletId, notification, function() { self.messageBroker.send(notification); - self.emailService._generateFromNotification(notification, function() { + self.emailService.sendEmail(notification, function() { if (cb) return cb(); }); }); From fa9a8cd38c69df1c8efd7094c791f859dfd4febf Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 29 Apr 2015 18:39:30 -0300 Subject: [PATCH 09/25] add tests --- lib/emailservice.js | 20 +++++----- lib/server.js | 2 + test/integration/server.js | 79 +++++++++++++++++++++++++++++++------- test/storage.js | 7 ++++ 4 files changed, 86 insertions(+), 22 deletions(-) diff --git a/lib/emailservice.js b/lib/emailservice.js index 5e78eb9..8dc6ab4 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -69,7 +69,7 @@ EmailService.prototype._getRecipientsList = function(notification, emailType, cb self.storage.fetchPreferences(notification.walletId, null, function(err, preferences) { if (err) return cb(err); - if (_.isEmpty(preferences)) return cb(null, {}); + if (_.isEmpty(preferences)) return cb(null, []); var recipients = _.compact(_.map(preferences, function(p) { if (!p.email) return; @@ -138,7 +138,7 @@ EmailService.prototype.sendEmail = function(notification, cb) { self._getRecipientsList(notification, emailType, function(err, list) { if (_.isEmpty(list)) return cb(); recipientsList = list; - return next(); + next(); }); }, function(next) { @@ -150,13 +150,15 @@ EmailService.prototype.sendEmail = function(notification, cb) { function(next) { self._getDataForTemplate(notification, next); }, - ], next); + ], function(err, res) { + next(err, res[0], res[1]); + }); }, function(template, data, next) { self._applyTemplate(template, data, next); }, function(content, next) { - _.each(recipientsList, function(recipient) { + async.map(recipientsList, function(recipient, next) { var email = Model.Email.create({ walletId: notification.walletId, copayerId: recipient.copayerId, @@ -167,14 +169,14 @@ EmailService.prototype.sendEmail = function(notification, cb) { self.storage.storeEmail(email, function(err) { return next(err, email); }); - }); + }, next); }, - function(email, next) { - self._send(email, next); + function(emails, next) { + async.each(emails, function(email, next) { + self._send(email, next); + }, next); }, ], cb); - - return cb(); }; module.exports = EmailService; diff --git a/lib/server.js b/lib/server.js index 1a15a87..a873576 100644 --- a/lib/server.js +++ b/lib/server.js @@ -85,6 +85,8 @@ WalletService.initialize = function(opts, cb) { emailService = new EmailService({ lock: lock, storage: storage, + mailer: opts.mailer, + email: opts.email, }); return cb(); }; diff --git a/test/integration/server.js b/test/integration/server.js index bb1e8b8..6b96620 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -27,15 +27,15 @@ var TestData = require('../testdata'); var helpers = {}; helpers.getAuthServer = function(copayerId, cb) { - var signatureStub = sinon.stub(WalletService.prototype, '_verifySignature'); - signatureStub.returns(true); + var verifyStub = sinon.stub(WalletService.prototype, '_verifySignature'); + verifyStub.returns(true); WalletService.getInstanceWithAuth({ copayerId: copayerId, message: 'dummy', signature: 'dummy', }, function(err, server) { + verifyStub.restore(); if (err || !server) throw new Error('Could not login as copayerId ' + copayerId); - signatureStub.restore(); return cb(server); }); }; @@ -132,7 +132,6 @@ helpers.toSatoshi = function(btc) { } }; -// Amounts in satoshis helpers.stubUtxos = function(server, wallet, amounts, cb) { var amounts = [].concat(amounts); @@ -211,16 +210,22 @@ helpers.createAddresses = function(server, wallet, main, change, cb) { }); }; -var db, storage, blockchainExplorer; +var db, storage, blockchainExplorer, mailer; function openDb(cb) { db = new tingodb.Db('./db/test', {}); + // HACK: There appears to be a bug in TingoDB's close function where the callback is not being executed + db.__close = db.close; + db.close = function(force, cb) { + this.__close(force, cb); + return cb(); + }; return cb(); }; function resetDb(cb) { - if (!db) return cb(); - db.dropDatabase(function(err) { + if (!storage.db) return cb(); + storage.db.dropDatabase(function(err) { return cb(); }); }; @@ -228,19 +233,27 @@ function resetDb(cb) { describe('Wallet service', function() { before(function(done) { - openDb(function() { - storage = new Storage({ - db: db - }); - done(); - }); + // openDb(function() { + // storage = new Storage({ + // db: db + // }); + // done(); + // }); + storage = new Storage(); + storage.connect({ + mongoDb: { + uri: 'mongodb://localhost:27017/bws_test' + } + }, done); }); beforeEach(function(done) { resetDb(function() { blockchainExplorer = sinon.stub(); + mailer = sinon.stub(); WalletService.initialize({ storage: storage, blockchainExplorer: blockchainExplorer, + mailer: mailer, }, done); }); }); @@ -3066,4 +3079,44 @@ describe('Wallet service', function() { }); }); }); + describe('Email notifications', function() { + var server, wallet; + beforeEach(function(done) { + mailer.sendMail = sinon.stub().yields(); + helpers.createAndJoinWallet(2, 3, function(s, w) { + server = s; + wallet = w; + var i = 0; + async.eachSeries(w.copayers, function(copayer, next) { + helpers.getAuthServer(copayer.id, function(server) { + server.savePreferences({ + email: 'copayer' + (i++) + '@domain.com', + }, next); + }); + }, done); + }); + }); + afterEach(function() { + NotificationBroadcaster.removeAllListeners(); + }); + + it('should notify copayers a new tx proposal has been created', function(done) { + WalletService.onNotification(function(n) { + if (n.type != 'NewTxProposal') return; + var calls = mailer.sendMail.getCalls(); + calls.length.should.equal(2); + var recipients = _.pluck(_.map(calls, function(c) { + return c.args[0]; + }), 'to'); + _.difference(['copayer1@domain.com', 'copayer2@domain.com'], recipients).should.be.empty; + done(); + }); + helpers.stubUtxos(server, wallet, [1, 1], function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, 'some message', TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + }); + }); + }); + }); }); diff --git a/test/storage.js b/test/storage.js index f66ad46..db0d8a7 100644 --- a/test/storage.js +++ b/test/storage.js @@ -16,9 +16,16 @@ var db, storage; function openDb(cb) { db = new tingodb.Db('./db/test', {}); + // HACK: There appears to be a bug in TingoDB's close function where the callback is not being executed + db.__close = db.close; + db.close = function(force, cb) { + this.__close(force, cb); + return cb(); + }; return cb(); }; + function resetDb(cb) { if (!db) return cb(); db.dropDatabase(function(err) { From 5bd4f7a565d229306632359c6440828030396ee0 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 29 Apr 2015 18:47:40 -0300 Subject: [PATCH 10/25] back to TingoDB --- test/integration/server.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/integration/server.js b/test/integration/server.js index 6b96620..c50e12e 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -233,19 +233,19 @@ function resetDb(cb) { describe('Wallet service', function() { before(function(done) { - // openDb(function() { - // storage = new Storage({ - // db: db - // }); - // done(); - // }); - storage = new Storage(); - storage.connect({ - mongoDb: { - uri: 'mongodb://localhost:27017/bws_test' - } - }, done); - }); + openDb(function() { + storage = new Storage({ + db: db + }); + done(); + }); + // storage = new Storage(); + // storage.connect({ + // mongoDb: { + // uri: 'mongodb://localhost:27017/bws_test' + // } + // }, done); +}); beforeEach(function(done) { resetDb(function() { blockchainExplorer = sinon.stub(); From 132afca15c03aead11eb2e94c221ef8ef656b151 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 29 Apr 2015 21:02:30 -0300 Subject: [PATCH 11/25] add from param --- config.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config.js b/config.js index d114e60..18c47f7 100644 --- a/config.js +++ b/config.js @@ -43,8 +43,9 @@ var config = { service: 'Gmail', auth: { user: '', - pass: '' - } - } + pass: '', + }, + from: '', + }, }; module.exports = config; From 8efc0065e6622053870ecfe53267c17ba889116e Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 29 Apr 2015 21:02:56 -0300 Subject: [PATCH 12/25] improve template text --- lib/templates/new_incoming_tx.plain | 5 ++--- lib/templates/new_outgoing_tx.plain | 5 ++--- lib/templates/new_tx_proposal.plain | 5 ++--- lib/templates/txp_finally_rejected.plain | 5 ++--- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/templates/new_incoming_tx.plain b/lib/templates/new_incoming_tx.plain index 51d0575..cd335e0 100644 --- a/lib/templates/new_incoming_tx.plain +++ b/lib/templates/new_incoming_tx.plain @@ -1,3 +1,2 @@ -copay@copay.io -[Copay] Funds received! -Funds received on your wallet. +[Copay] New payment received +A Payment has been received into your Copay wallet <%= walletName %>. diff --git a/lib/templates/new_outgoing_tx.plain b/lib/templates/new_outgoing_tx.plain index 52036b1..844bc4a 100644 --- a/lib/templates/new_outgoing_tx.plain +++ b/lib/templates/new_outgoing_tx.plain @@ -1,3 +1,2 @@ -copay@copay.io -[Copay] Transaction broadcasted! -A transaction has been broadcasted. +[Copay] Payment sent +A Payment has been sent from your Copay wallet <%= walletName %>. diff --git a/lib/templates/new_tx_proposal.plain b/lib/templates/new_tx_proposal.plain index 643f616..0c8e95a 100644 --- a/lib/templates/new_tx_proposal.plain +++ b/lib/templates/new_tx_proposal.plain @@ -1,3 +1,2 @@ -copay@copay.io -[Copay] New transaction proposal! -A new transaction proposal has been created by another copayer. +[Copay] New spend proposal +A new spend proposal has been created in your Copay wallet <%= walletName %> by <%= creatorName %>. diff --git a/lib/templates/txp_finally_rejected.plain b/lib/templates/txp_finally_rejected.plain index 70cc64e..d07e7e2 100644 --- a/lib/templates/txp_finally_rejected.plain +++ b/lib/templates/txp_finally_rejected.plain @@ -1,3 +1,2 @@ -copay@copay.io -[Copay] A transaction proposal was rejected -A transaction proposal was rejected by your copayers. +[Copay] Spend proposal rejected +A spend proposal in your Copay wallet <%= walletName %> has been rejected by <%= creatorName %>. From 3d901852abdefd849b0af0f31876ec9b6d6ccbc7 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Wed, 29 Apr 2015 21:03:47 -0300 Subject: [PATCH 13/25] more robust send loop --- lib/emailservice.js | 14 ++++++++++++-- lib/model/email.js | 8 +++++++- test/integration/server.js | 17 ++++++++++------- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/emailservice.js b/lib/emailservice.js index 8dc6ab4..8aef86e 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -31,10 +31,17 @@ var EMAIL_TYPES = { function EmailService(opts) { + $.checkArgument(opts); + + opts.email = opts.email || {}; + this.storage = opts.storage; this.lock = opts.lock; this.mailer = opts.mailer || nodemailer.createTransport(opts.email); + this.from = opts.email.from; + $.checkState(this.mailer); + $.checkState(this.from); }; // TODO: cache for X minutes @@ -95,7 +102,7 @@ EmailService.prototype._getDataForTemplate = function(notification, cb) { data.walletM = wallet.m; data.walletN = wallet.n; var copayer = _.find(wallet.copayers, { - copayerId: notification.creatorId + id: notification.creatorId }); if (copayer) { data.creatorId = copayer.id; @@ -162,6 +169,7 @@ EmailService.prototype.sendEmail = function(notification, cb) { var email = Model.Email.create({ walletId: notification.walletId, copayerId: recipient.copayerId, + from: self.from, to: recipient.emailAddress, subject: content.subject, body: content.body, @@ -174,7 +182,9 @@ EmailService.prototype.sendEmail = function(notification, cb) { function(emails, next) { async.each(emails, function(email, next) { self._send(email, next); - }, next); + }, function(err) { + return next(); + }); }, ], cb); }; diff --git a/lib/model/email.js b/lib/model/email.js index 002facf..8671038 100644 --- a/lib/model/email.js +++ b/lib/model/email.js @@ -1,5 +1,8 @@ 'use strict'; +var _ = require('lodash'); +var Uuid = require('uuid'); + function Email() { this.version = '1.0.0'; }; @@ -9,7 +12,9 @@ Email.create = function(opts) { var x = new Email(); - x.createdOn = Math.floor(Date.now() / 1000); + var now = Date.now(); + x.createdOn = Math.floor(now / 1000); + x.id = _.padLeft(now, 14, '0') + Uuid.v4(); x.walletId = opts.walletId; x.copayerId = opts.copayerId; x.from = opts.from; @@ -26,6 +31,7 @@ Email.fromObj = function(obj) { var x = new Email(); x.createdOn = obj.createdOn; + x.id = obj.id; x.walletId = obj.walletId; x.copayerId = obj.copayerId; x.from = obj.from; diff --git a/test/integration/server.js b/test/integration/server.js index c50e12e..f027f6d 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -239,13 +239,13 @@ describe('Wallet service', function() { }); done(); }); - // storage = new Storage(); - // storage.connect({ - // mongoDb: { - // uri: 'mongodb://localhost:27017/bws_test' - // } - // }, done); -}); + // storage = new Storage(); + // storage.connect({ + // mongoDb: { + // uri: 'mongodb://localhost:27017/bws_test' + // } + // }, done); + }); beforeEach(function(done) { resetDb(function() { blockchainExplorer = sinon.stub(); @@ -254,6 +254,9 @@ describe('Wallet service', function() { storage: storage, blockchainExplorer: blockchainExplorer, mailer: mailer, + email: { + from: 'bws@dummy.net', + } }, done); }); }); From 17d97430ada29e5f10380864cec74b45075deca3 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 30 Apr 2015 14:50:48 -0300 Subject: [PATCH 14/25] return after email --- config.js | 2 +- lib/emailservice.js | 91 ++++++++++++++++++-------------------- lib/server.js | 2 + test/integration/server.js | 75 +++++++++++++++---------------- 4 files changed, 84 insertions(+), 86 deletions(-) diff --git a/config.js b/config.js index 18c47f7..d246a7d 100644 --- a/config.js +++ b/config.js @@ -45,7 +45,7 @@ var config = { user: '', pass: '', }, - from: '', + from: 'wallet-service@bitcore.io', }, }; module.exports = config; diff --git a/lib/emailservice.js b/lib/emailservice.js index 8aef86e..3153374 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -137,56 +137,51 @@ EmailService.prototype.sendEmail = function(notification, cb) { var emailType = EMAIL_TYPES[notification.type]; if (!emailType) return cb(); - var recipientsList; - - async.waterfall([ - - function(next) { - self._getRecipientsList(notification, emailType, function(err, list) { - if (_.isEmpty(list)) return cb(); - recipientsList = list; - next(); - }); - }, - 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); - }, - function(content, next) { - async.map(recipientsList, function(recipient, next) { - var email = Model.Email.create({ - walletId: notification.walletId, - copayerId: recipient.copayerId, - from: self.from, - to: recipient.emailAddress, - subject: content.subject, - body: content.body, + self._getRecipientsList(notification, emailType, function(err, recipientsList) { + if (_.isEmpty(recipientsList)) return 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]); }); - self.storage.storeEmail(email, function(err) { - return next(err, email); + }, + function(template, data, next) { + self._applyTemplate(template, data, next); + }, + function(content, next) { + async.map(recipientsList, function(recipient, next) { + var email = Model.Email.create({ + walletId: notification.walletId, + copayerId: recipient.copayerId, + from: self.from, + to: recipient.emailAddress, + subject: content.subject, + body: content.body, + }); + self.storage.storeEmail(email, function(err) { + return next(err, email); + }); + }, next); + }, + function(emails, next) { + async.each(emails, function(email, next) { + self._send(email, next); + }, function(err) { + return next(); }); - }, next); - }, - function(emails, next) { - async.each(emails, function(email, next) { - self._send(email, next); - }, function(err) { - return next(); - }); - }, - ], cb); + }, + ], cb); + }); }; module.exports = EmailService; diff --git a/lib/server.js b/lib/server.js index a873576..42584ad 100644 --- a/lib/server.js +++ b/lib/server.js @@ -340,6 +340,8 @@ WalletService.prototype._notify = function(type, data, opts, cb) { log.debug('Notification', type, data); + cb = cb || function() {}; + var walletId = self.walletId || data.walletId; var copayerId = self.copayerId || data.copayerId; diff --git a/test/integration/server.js b/test/integration/server.js index f027f6d..21cc116 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -210,20 +210,32 @@ helpers.createAddresses = function(server, wallet, main, change, cb) { }); }; -var db, storage, blockchainExplorer, mailer; - -function openDb(cb) { - db = new tingodb.Db('./db/test', {}); - // HACK: There appears to be a bug in TingoDB's close function where the callback is not being executed - db.__close = db.close; - db.close = function(force, cb) { - this.__close(force, cb); +var storage, blockchainExplorer, mailer; + +var useMongo = false; + +function initStorage(cb) { + function getDb(cb) { + if (useMongo) { + var mongodb = require('mongodb'); + mongodb.MongoClient.connect('mongodb://localhost:27017/bws_test', function(err, db) { + if (err) throw err; + return cb(db); + }); + } else { + var db = new tingodb.Db('./db/test', {}); + return cb(db); + } + } + getDb(function(db) { + storage = new Storage({ + db: db + }); return cb(); - }; - return cb(); + }); }; -function resetDb(cb) { +function resetStorage(cb) { if (!storage.db) return cb(); storage.db.dropDatabase(function(err) { return cb(); @@ -233,21 +245,10 @@ function resetDb(cb) { describe('Wallet service', function() { before(function(done) { - openDb(function() { - storage = new Storage({ - db: db - }); - done(); - }); - // storage = new Storage(); - // storage.connect({ - // mongoDb: { - // uri: 'mongodb://localhost:27017/bws_test' - // } - // }, done); + initStorage(done); }); beforeEach(function(done) { - resetDb(function() { + resetStorage(function() { blockchainExplorer = sinon.stub(); mailer = sinon.stub(); WalletService.initialize({ @@ -1566,7 +1567,7 @@ describe('Wallet service', function() { }); }); - it('should brodcast a tx', function(done) { + it('should broadcast a tx', function(done) { var clock = sinon.useFakeTimers(1234000); helpers.stubBroadcast('999'); server.broadcastTx({ @@ -3083,12 +3084,15 @@ describe('Wallet service', function() { }); }); describe('Email notifications', function() { - var server, wallet; + var server, wallet, sendMailStub; beforeEach(function(done) { - mailer.sendMail = sinon.stub().yields(); helpers.createAndJoinWallet(2, 3, function(s, w) { server = s; wallet = w; + sendMailStub = sinon.stub(); + sendMailStub.yields(); + server.emailService.mailer.sendMail = sendMailStub; + var i = 0; async.eachSeries(w.copayers, function(copayer, next) { helpers.getAuthServer(copayer.id, function(server) { @@ -3104,20 +3108,17 @@ describe('Wallet service', function() { }); it('should notify copayers a new tx proposal has been created', function(done) { - WalletService.onNotification(function(n) { - if (n.type != 'NewTxProposal') return; - var calls = mailer.sendMail.getCalls(); - calls.length.should.equal(2); - var recipients = _.pluck(_.map(calls, function(c) { - return c.args[0]; - }), 'to'); - _.difference(['copayer1@domain.com', 'copayer2@domain.com'], recipients).should.be.empty; - done(); - }); helpers.stubUtxos(server, wallet, [1, 1], function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 0.8, 'some message', TestData.copayers[0].privKey_1H_0); server.createTx(txOpts, function(err, tx) { should.not.exist(err); + var calls = sendMailStub.getCalls(); + calls.length.should.equal(2); + var recipients = _.pluck(_.map(calls, function(c) { + return c.args[0]; + }), 'to'); + _.difference(['copayer1@domain.com', 'copayer2@domain.com'], recipients).should.be.empty; + done(); }); }); }); From dfef5ae39cb0466b977dcc32c5ca2867fbb7ce59 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 30 Apr 2015 16:05:10 -0300 Subject: [PATCH 15/25] make email sending optional --- config.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/config.js b/config.js index d246a7d..8f637ad 100644 --- a/config.js +++ b/config.js @@ -39,13 +39,15 @@ var config = { url: 'https://test-insight.bitpay.com:443', }, }, - email: { - service: 'Gmail', - auth: { - user: '', - pass: '', - }, - from: 'wallet-service@bitcore.io', - }, + // To use email notifications uncomment this: + // email: { + // service: 'Gmail', + // auth: { + // user: '', + // pass: '', + // }, + // subjectPrefix: '[Copay]', + // from: 'wallet-service@bitcore.io', + // }, }; module.exports = config; From b83d220fa237bb4a64c19148c4839c2d60a82b1c Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 30 Apr 2015 16:05:36 -0300 Subject: [PATCH 16/25] configure subject prefix --- lib/templates/new_incoming_tx.plain | 4 ++-- lib/templates/new_outgoing_tx.plain | 4 ++-- lib/templates/new_tx_proposal.plain | 4 ++-- lib/templates/txp_finally_rejected.plain | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/templates/new_incoming_tx.plain b/lib/templates/new_incoming_tx.plain index cd335e0..c980ef5 100644 --- a/lib/templates/new_incoming_tx.plain +++ b/lib/templates/new_incoming_tx.plain @@ -1,2 +1,2 @@ -[Copay] New payment received -A Payment has been received into your Copay wallet <%= walletName %>. +<%= subjectPrefix %>New payment received +A Payment has been received into your wallet <%= walletName %>. diff --git a/lib/templates/new_outgoing_tx.plain b/lib/templates/new_outgoing_tx.plain index 844bc4a..a5cba78 100644 --- a/lib/templates/new_outgoing_tx.plain +++ b/lib/templates/new_outgoing_tx.plain @@ -1,2 +1,2 @@ -[Copay] Payment sent -A Payment has been sent from your Copay wallet <%= walletName %>. +<%= subjectPrefix %>Payment sent +A Payment has been sent from your wallet <%= walletName %>. diff --git a/lib/templates/new_tx_proposal.plain b/lib/templates/new_tx_proposal.plain index 0c8e95a..b44b1de 100644 --- a/lib/templates/new_tx_proposal.plain +++ b/lib/templates/new_tx_proposal.plain @@ -1,2 +1,2 @@ -[Copay] New spend proposal -A new spend proposal has been created in your Copay wallet <%= walletName %> by <%= creatorName %>. +<%= subjectPrefix %>New spend proposal +A new spend proposal has been created in your wallet <%= walletName %> by <%= creatorName %>. diff --git a/lib/templates/txp_finally_rejected.plain b/lib/templates/txp_finally_rejected.plain index d07e7e2..60e4b76 100644 --- a/lib/templates/txp_finally_rejected.plain +++ b/lib/templates/txp_finally_rejected.plain @@ -1,2 +1,2 @@ -[Copay] Spend proposal rejected -A spend proposal in your Copay wallet <%= walletName %> has been rejected by <%= creatorName %>. +<%= subjectPrefix %>Spend proposal rejected +A spend proposal in your wallet <%= walletName %> has been rejected by <%= creatorName %>. From 897b39f8cd447e89a036b944c0537d53ceef2ba7 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 30 Apr 2015 16:05:49 -0300 Subject: [PATCH 17/25] mark emails as sent/fail --- lib/emailservice.js | 11 ++++++++++- lib/model/email.js | 18 ++++++++++++++++-- lib/server.js | 1 + lib/storage.js | 2 +- test/integration/server.js | 17 +++++++++++++---- 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/lib/emailservice.js b/lib/emailservice.js index 3153374..6742071 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -38,6 +38,7 @@ function EmailService(opts) { this.storage = opts.storage; this.lock = opts.lock; this.mailer = opts.mailer || nodemailer.createTransport(opts.email); + this.subjectPrefix = opts.email.subjectPrefix || '[Wallet service]'; this.from = opts.email.from; $.checkState(this.mailer); @@ -95,6 +96,7 @@ EmailService.prototype._getDataForTemplate = function(notification, cb) { var self = this; var data = _.cloneDeep(notification.data); + data.subjectPrefix = _.trim(self.subjectPrefix) + ' '; self.storage.fetchWallet(notification.walletId, function(err, wallet) { if (err) return cb(err); data.walletId = wallet.id; @@ -175,7 +177,14 @@ EmailService.prototype.sendEmail = function(notification, cb) { }, function(emails, next) { async.each(emails, function(email, next) { - self._send(email, next); + self._send(email, function(err) { + if (err) { + email.setFail(); + } else { + email.setSent(); + } + self.storage.storeEmail(email, next); + }); }, function(err) { return next(); }); diff --git a/lib/model/email.js b/lib/model/email.js index 8671038..2d30f07 100644 --- a/lib/model/email.js +++ b/lib/model/email.js @@ -23,7 +23,7 @@ Email.create = function(opts) { x.body = opts.body; x.status = 'pending'; x.attempts = 0; - x.sentOn = null; + x.lastAttemptOn = null; return x; }; @@ -40,9 +40,23 @@ Email.fromObj = function(obj) { x.body = obj.body; x.status = obj.status; x.attempts = obj.attempts; - x.sentOn = obj.sentOn; + x.lastAttemptOn = obj.lastAttemptOn; return x; }; +Email.prototype._logAttempt = function(result) { + this.attempts++; + this.lastAttemptOn = Math.floor(Date.now() / 1000); + this.status = result; +}; + +Email.prototype.setSent = function() { + this._logAttempt('sent'); +}; + +Email.prototype.setFail = function() { + this._logAttempt('fail'); +}; + module.exports = Email; diff --git a/lib/server.js b/lib/server.js index 42584ad..96d5a44 100644 --- a/lib/server.js +++ b/lib/server.js @@ -82,6 +82,7 @@ WalletService.initialize = function(opts, cb) { }; function initEmailService(cb) { + if (!opts.mailer && !opts.email) return cb(); emailService = new EmailService({ lock: lock, storage: storage, diff --git a/lib/storage.js b/lib/storage.js index b340f3e..15a512a 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -402,7 +402,7 @@ Storage.prototype.fetchUnsentEmails = function(cb) { status: 'pending', }).toArray(function(err, result) { if (err) return cb(err); - if (!result) return cb(); + if (!result || _.isEmpty(result)) return cb(null, []); return cb(null, Model.Email.fromObj(result)); }); }; diff --git a/test/integration/server.js b/test/integration/server.js index 21cc116..5982cba 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3114,11 +3114,20 @@ describe('Wallet service', function() { should.not.exist(err); var calls = sendMailStub.getCalls(); calls.length.should.equal(2); - var recipients = _.pluck(_.map(calls, function(c) { + var emails = _.map(calls, function(c) { return c.args[0]; - }), 'to'); - _.difference(['copayer1@domain.com', 'copayer2@domain.com'], recipients).should.be.empty; - done(); + }); + _.difference(['copayer1@domain.com', 'copayer2@domain.com'], _.pluck(emails, 'to')).should.be.empty; + var one = emails[0]; + one.from.should.equal('bws@dummy.net'); + one.subject.should.contain('New spend proposal'); + one.text.should.contain(wallet.name); + one.text.should.contain(wallet.copayers[0].name); + server.storage.fetchUnsentEmails(function(err, unsent) { + should.not.exist(err); + unsent.should.be.empty; + done(); + }); }); }); }); From 36ed2f25bc45cae36637641ffed115f0f61eeb03 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 30 Apr 2015 18:07:53 -0300 Subject: [PATCH 18/25] change default email config --- config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config.js b/config.js index 8f637ad..f29e398 100644 --- a/config.js +++ b/config.js @@ -41,12 +41,12 @@ var config = { }, // To use email notifications uncomment this: // email: { - // service: 'Gmail', // auth: { - // user: '', - // pass: '', + // host: 'localhost', + // port: 25, + // ignoreTLS: true, // }, - // subjectPrefix: '[Copay]', + // subjectPrefix: '[Wallet Service]', // from: 'wallet-service@bitcore.io', // }, }; From 3202741a1066b3fa1d75dfb71c702ba224de725b Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Thu, 7 May 2015 18:55:02 -0300 Subject: [PATCH 19/25] introduce message broker through rebase --- lib/blockchainmonitor.js | 44 -------------------------------------- lib/server.js | 1 - test/integration/server.js | 3 --- 3 files changed, 48 deletions(-) diff --git a/lib/blockchainmonitor.js b/lib/blockchainmonitor.js index 8495400..787c7fd 100644 --- a/lib/blockchainmonitor.js +++ b/lib/blockchainmonitor.js @@ -10,15 +10,11 @@ var BlockchainExplorer = require('./blockchainexplorer'); var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); -<<<<<<< HEAD var Notification = require('./model/notification'); function BlockchainMonitor() {}; BlockchainMonitor.prototype.start = function(opts, cb) { -======= -function BlockchainMonitor(opts) { ->>>>>>> refactor notification dispatching opts = opts || {}; $.checkArgument(opts.blockchainExplorerOpts); $.checkArgument(opts.storageOpts); @@ -69,14 +65,8 @@ BlockchainMonitor.prototype._initExplorer = function(provider, network, url) { }); socket.on('tx', _.bind(self._handleIncommingTx, self)); -<<<<<<< HEAD return explorer; }; -======= -BlockchainMonitor.prototype.subscribeAddresses = function(walletService, addresses) { - $.checkArgument(walletService); - $.checkArgument(walletService.walletId); ->>>>>>> refactor notification dispatching BlockchainMonitor.prototype._handleIncommingTx = function(data) { var self = this; @@ -84,27 +74,15 @@ BlockchainMonitor.prototype._handleIncommingTx = function(data) { if (!data || !data.vout) return; -<<<<<<< HEAD var outs = _.compact(_.map(data.vout, function(v) { var addr = _.keys(v)[0]; var startingChar = addr.charAt(0); if (startingChar != '2' && startingChar != '3') return; -======= - function handlerFor(address, txid) { - var data = { - walletId: this.walletId, - address: address, - txid: txid, - }; - self.emit('NewIncomingTx', data, this); - }; ->>>>>>> refactor notification dispatching return { address: addr, amount: +v[addr] }; -<<<<<<< HEAD })); if (_.isEmpty(outs)) return; @@ -122,23 +100,12 @@ BlockchainMonitor.prototype._handleIncommingTx = function(data) { }); }, function(err) { return; -======= - }; - - var addresses = [].concat(addresses); - var network = Bitcore.Address.fromString(addresses[0]).network.name; - var subscriber = self.subscriber[network]; - _.each(addresses, function(address) { - self.subscriptions[walletId].addresses.push(address); - subscriber.subscribe(address, _.bind(handlerFor, walletService, address)); ->>>>>>> refactor notification dispatching }); }; BlockchainMonitor.prototype._createNotification = function(walletId, txid, address, amount, cb) { var self = this; -<<<<<<< HEAD var n = Notification.create({ type: 'NewIncomingTx', data: { @@ -150,17 +117,6 @@ BlockchainMonitor.prototype._createNotification = function(walletId, txid, addre }); self.storage.storeNotification(walletId, n, function() { self.messageBroker.send(n) -======= - var walletId = walletService.walletId; - if (self.subscriptions[walletId]) return; - - walletService.getMainAddresses({}, function(err, addresses) { - if (err) { - delete self.subscriptions[walletId]; - return cb(new Error('Could not subscribe to addresses for wallet ' + walletId)); - } - self.subscribeAddresses(walletService, _.pluck(addresses, 'address')); ->>>>>>> refactor notification dispatching return cb(); }); }; diff --git a/lib/server.js b/lib/server.js index 96d5a44..635d7d3 100644 --- a/lib/server.js +++ b/lib/server.js @@ -117,7 +117,6 @@ WalletService.initialize = function(opts, cb) { log.error('Could not initialize', err); throw err; } - ], function() { initialized = true; return cb(); }); diff --git a/test/integration/server.js b/test/integration/server.js index 5982cba..c5018cd 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -3103,9 +3103,6 @@ describe('Wallet service', function() { }, done); }); }); - afterEach(function() { - NotificationBroadcaster.removeAllListeners(); - }); it('should notify copayers a new tx proposal has been created', function(done) { helpers.stubUtxos(server, wallet, [1, 1], function() { From 910a1c8ddc3bd50c35a318f01c0351d42f54a80e Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Fri, 8 May 2015 17:31:33 -0300 Subject: [PATCH 20/25] update config --- config.js | 16 +++++++--------- lib/server.js | 8 +++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/config.js b/config.js index f29e398..12d471b 100644 --- a/config.js +++ b/config.js @@ -40,14 +40,12 @@ var config = { }, }, // To use email notifications uncomment this: - // email: { - // auth: { - // host: 'localhost', - // port: 25, - // ignoreTLS: true, - // }, - // subjectPrefix: '[Wallet Service]', - // from: 'wallet-service@bitcore.io', - // }, + email: { + host: 'localhost', + port: 25, + ignoreTLS: true, + subjectPrefix: '[Wallet Service]', + from: 'wallet-service@bitcore.io', + }, }; module.exports = config; diff --git a/lib/server.js b/lib/server.js index 635d7d3..ce4492b 100644 --- a/lib/server.js +++ b/lib/server.js @@ -357,9 +357,11 @@ WalletService.prototype._notify = function(type, data, opts, cb) { this.storage.storeNotification(walletId, notification, function() { self.messageBroker.send(notification); - self.emailService.sendEmail(notification, function() { - if (cb) return cb(); - }); + if (self.emailService) { + self.emailService.sendEmail(notification, function() { + if (cb) return cb(); + }); + } }); }; From a643819b463a56e5cf6dcbc289a1e886b20ea60d Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 11 May 2015 11:46:28 -0300 Subject: [PATCH 21/25] add email validation --- config.js | 2 +- lib/emailservice.js | 8 ++++---- lib/server.js | 8 +++++++- test/integration/server.js | 17 ++++++++++++++++- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/config.js b/config.js index 12d471b..db5adf8 100644 --- a/config.js +++ b/config.js @@ -40,7 +40,7 @@ var config = { }, }, // To use email notifications uncomment this: - email: { + emailOpts: { host: 'localhost', port: 25, ignoreTLS: true, diff --git a/lib/emailservice.js b/lib/emailservice.js index 6742071..d5038b8 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -33,13 +33,13 @@ var EMAIL_TYPES = { function EmailService(opts) { $.checkArgument(opts); - opts.email = opts.email || {}; + opts.emailOpts = opts.emailOpts || {}; this.storage = opts.storage; this.lock = opts.lock; - this.mailer = opts.mailer || nodemailer.createTransport(opts.email); - this.subjectPrefix = opts.email.subjectPrefix || '[Wallet service]'; - this.from = opts.email.from; + this.mailer = opts.mailer || nodemailer.createTransport(opts.emailOpts); + this.subjectPrefix = opts.emailOpts.subjectPrefix || '[Wallet service]'; + this.from = opts.emailOpts.from; $.checkState(this.mailer); $.checkState(this.from); diff --git a/lib/server.js b/lib/server.js index ce4492b..6eae584 100644 --- a/lib/server.js +++ b/lib/server.js @@ -87,7 +87,7 @@ WalletService.initialize = function(opts, cb) { lock: lock, storage: storage, mailer: opts.mailer, - email: opts.email, + emailOpts: opts.emailOpts, }); return cb(); }; @@ -448,6 +448,12 @@ WalletService.prototype.savePreferences = function(opts, cb) { opts = opts || {}; + if (opts.email) { + if (opts.email.length > 254 || opts.email.indexOf('@') == -1) { + return cb(new ClientError('Invalid email address')); + } + } + self._runLocked(cb, function(cb) { var preferences = Model.Preferences.create({ walletId: self.walletId, diff --git a/test/integration/server.js b/test/integration/server.js index c5018cd..4cf42a4 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -255,7 +255,7 @@ describe('Wallet service', function() { storage: storage, blockchainExplorer: blockchainExplorer, mailer: mailer, - email: { + emailOpts: { from: 'bws@dummy.net', } }, done); @@ -779,6 +779,21 @@ 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({ + email: 'dummy@' + _.repeat('domain', 50), + }, function(err) { + should.exist(err); + err.message.should.contain('email'); + done(); + }); + }); + }); }); describe('#getBalance', function() { From c864675b201a8b91922a3a24a4929bf92b3ab602 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 11 May 2015 15:03:17 -0300 Subject: [PATCH 22/25] send email from blockchain monitor --- lib/blockchainmonitor.js | 30 ++++++++++++++++++++++++------ lib/server.js | 2 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/blockchainmonitor.js b/lib/blockchainmonitor.js index 787c7fd..f89edb6 100644 --- a/lib/blockchainmonitor.js +++ b/lib/blockchainmonitor.js @@ -18,6 +18,8 @@ BlockchainMonitor.prototype.start = function(opts, cb) { opts = opts || {}; $.checkArgument(opts.blockchainExplorerOpts); $.checkArgument(opts.storageOpts); + $.checkArgument(opts.lockOpts); + $.checkArgument(opts.emailOpts); var self = this; @@ -38,8 +40,20 @@ BlockchainMonitor.prototype.start = function(opts, cb) { self.messageBroker = new MessageBroker(opts.messageBrokerOpts); done(); }, - ], cb); - + function(done) { + self.lock = new Lock(opts.lockOpts); + }, + ], function(err) { + if (err) return cb(err); + + self.emailService = new EmailService({ + lock: self.lock, + storage: self.storage, + mailer: opts.mailer, + emailOpts: opts.emailOpts, + }); + return cb(); + }); }; BlockchainMonitor.prototype._initExplorer = function(provider, network, url) { @@ -106,7 +120,7 @@ BlockchainMonitor.prototype._handleIncommingTx = function(data) { BlockchainMonitor.prototype._createNotification = function(walletId, txid, address, amount, cb) { var self = this; - var n = Notification.create({ + var notification = Notification.create({ type: 'NewIncomingTx', data: { txid: txid, @@ -115,9 +129,13 @@ BlockchainMonitor.prototype._createNotification = function(walletId, txid, addre }, walletId: walletId, }); - self.storage.storeNotification(walletId, n, function() { - self.messageBroker.send(n) - return cb(); + self.storage.storeNotification(walletId, notification, function() { + self.messageBroker.send(notification) + if (self.emailService) { + self.emailService.sendEmail(notification, function() { + if (cb) return cb(); + }); + } }); }; diff --git a/lib/server.js b/lib/server.js index 6eae584..1daad58 100644 --- a/lib/server.js +++ b/lib/server.js @@ -82,7 +82,7 @@ WalletService.initialize = function(opts, cb) { }; function initEmailService(cb) { - if (!opts.mailer && !opts.email) return cb(); + if (!opts.mailer && !opts.emailOpts) return cb(); emailService = new EmailService({ lock: lock, storage: storage, From 87bba4651573f14e965a1983e14aa88d79580394 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 11 May 2015 15:09:33 -0300 Subject: [PATCH 23/25] send email on new copayer notification --- lib/emailservice.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/emailservice.js b/lib/emailservice.js index d5038b8..595a620 100644 --- a/lib/emailservice.js +++ b/lib/emailservice.js @@ -11,6 +11,10 @@ var nodemailer = require('nodemailer'); var Model = require('./model'); var EMAIL_TYPES = { + 'NewCopayer': { + filename: 'new_copayer', + notifyDoer: false, + }, 'NewTxProposal': { filename: 'new_tx_proposal', notifyDoer: false, From fce1d4d7aa2ecccf44abe21e36dd62e61b088934 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 11 May 2015 15:35:25 -0300 Subject: [PATCH 24/25] connect to locker server by default --- config.js | 8 ++++---- lib/blockchainmonitor.js | 3 ++- lib/templates/new_copayer.plain | 2 ++ messagebroker/{bws-messagebroker.js => messagebroker.js} | 0 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 lib/templates/new_copayer.plain rename messagebroker/{bws-messagebroker.js => messagebroker.js} (100%) diff --git a/config.js b/config.js index db5adf8..90b92b7 100644 --- a/config.js +++ b/config.js @@ -18,10 +18,10 @@ var config = { }, lockOpts: { // To use locker-server, uncomment this: - // lockerServer: { - // host: 'localhost', - // port: 3231, - // }, + lockerServer: { + host: 'localhost', + port: 3231, + }, }, messageBrokerOpts: { // To use message broker server, uncomment this: diff --git a/lib/blockchainmonitor.js b/lib/blockchainmonitor.js index f89edb6..a756b3f 100644 --- a/lib/blockchainmonitor.js +++ b/lib/blockchainmonitor.js @@ -9,6 +9,8 @@ log.debug = log.verbose; var BlockchainExplorer = require('./blockchainexplorer'); var Storage = require('./storage'); var MessageBroker = require('./messagebroker'); +var Lock = require('./lock'); +var EmailService = require('./emailservice'); var Notification = require('./model/notification'); @@ -84,7 +86,6 @@ BlockchainMonitor.prototype._initExplorer = function(provider, network, url) { BlockchainMonitor.prototype._handleIncommingTx = function(data) { var self = this; - var walletId = walletService.walletId; if (!data || !data.vout) return; diff --git a/lib/templates/new_copayer.plain b/lib/templates/new_copayer.plain new file mode 100644 index 0000000..2cc1e53 --- /dev/null +++ b/lib/templates/new_copayer.plain @@ -0,0 +1,2 @@ +<%= subjectPrefix %>New copayer +A new copayer just joined your wallet <%= walletName %>. diff --git a/messagebroker/bws-messagebroker.js b/messagebroker/messagebroker.js similarity index 100% rename from messagebroker/bws-messagebroker.js rename to messagebroker/messagebroker.js From f3a3e0ff108450bfb501a950df5e3e30a2c7a019 Mon Sep 17 00:00:00 2001 From: Ivan Socolsky Date: Mon, 11 May 2015 15:43:25 -0300 Subject: [PATCH 25/25] email validation using email-validator module --- lib/server.js | 3 ++- package.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index 1daad58..3e191f4 100644 --- a/lib/server.js +++ b/lib/server.js @@ -5,6 +5,7 @@ var async = require('async'); var log = require('npmlog'); log.debug = log.verbose; log.disableColor(); +var EmailValidator = require('email-validator'); var WalletUtils = require('bitcore-wallet-utils'); var Bitcore = WalletUtils.Bitcore; @@ -449,7 +450,7 @@ WalletService.prototype.savePreferences = function(opts, cb) { opts = opts || {}; if (opts.email) { - if (opts.email.length > 254 || opts.email.indexOf('@') == -1) { + if (!EmailValidator.validate(opts.email)) { return cb(new ClientError('Invalid email address')); } } diff --git a/package.json b/package.json index 9fbc3ea..b5d4bd3 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "bitcore-wallet-utils": "0.0.12", "body-parser": "^1.11.0", "coveralls": "^2.11.2", + "email-validator": "^1.0.1", "express": "^4.10.0", "inherits": "^2.0.1", "locker": "^0.1.0",