mirror of
https://github.com/moparisthebest/mail
synced 2024-11-26 02:42:17 -05:00
623 lines
26 KiB
JavaScript
623 lines
26 KiB
JavaScript
define(function(require) {
|
|
'use strict';
|
|
|
|
var KeychainDAO = require('js/dao/keychain-dao'),
|
|
EmailDAO = require('js/dao/email-dao'),
|
|
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
|
|
SmtpClient = require('smtp-client'),
|
|
ImapClient = require('imap-client'),
|
|
PGP = require('js/crypto/pgp'),
|
|
app = require('js/app-config'),
|
|
expect = chai.expect;
|
|
|
|
var emaildaoTest = {
|
|
user: "whiteout.test@t-online.de",
|
|
passphrase: 'asdf',
|
|
asymKeySize: 512
|
|
};
|
|
|
|
var dummyMail;
|
|
|
|
var publicKey = "-----BEGIN PUBLIC KEY-----\r\n" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxy+Te5dyeWd7g0P+8LNO7fZDQ\r\n" + "g96xTb1J6pYE/pPTMlqhB6BRItIYjZ1US5q2vk5Zk/5KasBHAc9RbCqvh9v4XFEY\r\n" + "JVmTXC4p8ft1LYuNWIaDk+R3dyYXmRNct/JC4tks2+8fD3aOvpt0WNn3R75/FGBt\r\n" + "h4BgojAXDE+PRQtcVQIDAQAB\r\n" + "-----END PUBLIC KEY-----";
|
|
|
|
describe('Email DAO unit tests', function() {
|
|
|
|
var emailDao, account,
|
|
keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub;
|
|
|
|
beforeEach(function() {
|
|
// init dummy object
|
|
dummyMail = {
|
|
from: [{
|
|
name: 'Whiteout Test',
|
|
address: 'whiteout.test@t-online.de'
|
|
}], // sender address
|
|
to: [{
|
|
address: 'safewithme.testuser@gmail.com'
|
|
}], // list of receivers
|
|
subject: "Hello", // Subject line
|
|
body: "Hello world" // plaintext body
|
|
};
|
|
|
|
account = {
|
|
emailAddress: emaildaoTest.user,
|
|
symKeySize: app.config.symKeySize,
|
|
symIvSize: app.config.symIvSize,
|
|
asymKeySize: emaildaoTest.asymKeySize
|
|
};
|
|
|
|
keychainStub = sinon.createStubInstance(KeychainDAO);
|
|
imapClientStub = sinon.createStubInstance(ImapClient);
|
|
smtpClientStub = sinon.createStubInstance(SmtpClient);
|
|
pgpStub = sinon.createStubInstance(PGP);
|
|
devicestorageStub = sinon.createStubInstance(DeviceStorageDAO);
|
|
|
|
emailDao = new EmailDAO(keychainStub, imapClientStub, smtpClientStub, pgpStub, devicestorageStub);
|
|
emailDao._account = account;
|
|
});
|
|
|
|
afterEach(function() {});
|
|
|
|
describe('init', function() {
|
|
beforeEach(function() {
|
|
delete emailDao._account;
|
|
});
|
|
|
|
it('should fail due to error in getUserKeyPair', function(done) {
|
|
devicestorageStub.init.yields();
|
|
keychainStub.getUserKeyPair.yields(42);
|
|
|
|
emailDao.init(account, function(err) {
|
|
expect(devicestorageStub.init.calledOnce).to.be.true;
|
|
expect(err).to.equal(42);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should init', function(done) {
|
|
var mockKeyPair = {};
|
|
|
|
devicestorageStub.init.yields();
|
|
keychainStub.getUserKeyPair.yields(null, mockKeyPair);
|
|
|
|
emailDao.init(account, function(err, keyPair) {
|
|
expect(err).to.not.exist;
|
|
|
|
expect(keyPair === mockKeyPair).to.be.true;
|
|
expect(devicestorageStub.init.calledOnce).to.be.true;
|
|
expect(keychainStub.getUserKeyPair.calledOnce).to.be.true;
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('unlock', function() {
|
|
it('should unlock with new key', function(done) {
|
|
pgpStub.generateKeys.yields(null, {});
|
|
pgpStub.importKeys.yields();
|
|
keychainStub.putUserKeyPair.yields();
|
|
|
|
emailDao.unlock({}, emaildaoTest.passphrase, function(err) {
|
|
expect(err).to.not.exist;
|
|
|
|
expect(pgpStub.generateKeys.calledOnce).to.be.true;
|
|
expect(pgpStub.importKeys.calledOnce).to.be.true;
|
|
expect(keychainStub.putUserKeyPair.calledOnce).to.be.true;
|
|
expect(pgpStub.generateKeys.calledWith({
|
|
emailAddress: account.emailAddress,
|
|
keySize: account.asymKeySize,
|
|
passphrase: emaildaoTest.passphrase
|
|
})).to.be.true;
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should unlock with existing key pair', function(done) {
|
|
pgpStub.importKeys.yields();
|
|
|
|
emailDao.unlock({
|
|
privateKey: {
|
|
encryptedKey: 'cryptocrypto'
|
|
},
|
|
publicKey: {
|
|
publicKey: 'omgsocrypto'
|
|
}
|
|
}, emaildaoTest.passphrase, function(err) {
|
|
expect(err).to.not.exist;
|
|
|
|
expect(pgpStub.importKeys.calledOnce).to.be.true;
|
|
expect(pgpStub.importKeys.calledWith({
|
|
passphrase: emaildaoTest.passphrase,
|
|
privateKeyArmored: 'cryptocrypto',
|
|
publicKeyArmored: 'omgsocrypto'
|
|
})).to.be.true;
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not unlock with error during keygen', function(done) {
|
|
pgpStub.generateKeys.yields(new Error('fubar'));
|
|
|
|
emailDao.unlock({}, emaildaoTest.passphrase, function(err) {
|
|
expect(err).to.exist;
|
|
|
|
expect(pgpStub.generateKeys.calledOnce).to.be.true;
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not unloch with error during key import', function(done) {
|
|
pgpStub.generateKeys.yields(null, {});
|
|
pgpStub.importKeys.yields(new Error('fubar'));
|
|
|
|
emailDao.unlock({}, emaildaoTest.passphrase, function(err) {
|
|
expect(err).to.exist;
|
|
|
|
expect(pgpStub.generateKeys.calledOnce).to.be.true;
|
|
expect(pgpStub.importKeys.calledOnce).to.be.true;
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not unlock with error during key store', function(done) {
|
|
pgpStub.generateKeys.yields(null, {});
|
|
pgpStub.importKeys.yields();
|
|
keychainStub.putUserKeyPair.yields(new Error('omgwtf'));
|
|
|
|
emailDao.unlock({}, emaildaoTest.passphrase, function(err) {
|
|
expect(err).to.exist;
|
|
|
|
expect(pgpStub.generateKeys.calledOnce).to.be.true;
|
|
expect(pgpStub.importKeys.calledOnce).to.be.true;
|
|
expect(keychainStub.putUserKeyPair.calledOnce).to.be.true;
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('login', function() {
|
|
it('should fail due to error in imap login', function(done) {
|
|
imapClientStub.login.yields(42);
|
|
|
|
emailDao.imapLogin(function(err) {
|
|
expect(err).to.equal(42);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should work', function(done) {
|
|
imapClientStub.login.yields();
|
|
|
|
emailDao.imapLogin(function(err) {
|
|
expect(err).to.not.exist;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('IMAP/SMTP tests', function() {
|
|
beforeEach(function(done) {
|
|
devicestorageStub.init.yields();
|
|
keychainStub.getUserKeyPair.yields();
|
|
pgpStub.generateKeys.yields(null, {});
|
|
pgpStub.importKeys.yields();
|
|
keychainStub.putUserKeyPair.yields();
|
|
|
|
emailDao.init(account, function(err, keyPair) {
|
|
emailDao.unlock(keyPair, emaildaoTest.passphrase, function(err) {
|
|
expect(devicestorageStub.init.calledOnce).to.be.true;
|
|
expect(keychainStub.getUserKeyPair.calledOnce).to.be.true;
|
|
expect(pgpStub.generateKeys.calledOnce).to.be.true;
|
|
expect(pgpStub.importKeys.calledOnce).to.be.true;
|
|
expect(keychainStub.putUserKeyPair.calledOnce).to.be.true;
|
|
expect(err).to.not.exist;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
afterEach(function(done) {
|
|
imapClientStub.logout.yields();
|
|
emailDao.destroy(function(err) {
|
|
expect(imapClientStub.logout.calledOnce).to.be.true;
|
|
expect(err).to.not.exist;
|
|
done();
|
|
});
|
|
});
|
|
|
|
describe('SMTP: send email', function() {
|
|
it('should fail due to bad input', function(done) {
|
|
emailDao.smtpSend({}, function(err) {
|
|
expect(smtpClientStub.send.called).to.be.false;
|
|
expect(keychainStub.getReceiverPublicKey.called).to.be.false;
|
|
expect(err).to.exist;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should fail due to invalid email address input', function(done) {
|
|
dummyMail.to = [{
|
|
address: 'asfd'
|
|
}];
|
|
emailDao.smtpSend(dummyMail, function(err) {
|
|
expect(smtpClientStub.send.called).to.be.false;
|
|
expect(keychainStub.getReceiverPublicKey.called).to.be.false;
|
|
expect(err).to.exist;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should work for a new user', function(done) {
|
|
keychainStub.getReceiverPublicKey.yields(null, null);
|
|
smtpClientStub.send.yields();
|
|
|
|
emailDao.smtpSend(dummyMail, function(err) {
|
|
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
|
|
// expect(smtpClientStub.send.called).to.be.true;
|
|
// smtpClientStub.send.calledWith(sinon.match(function(o) {
|
|
// return typeof o.attachments === 'undefined';
|
|
// }));
|
|
expect(err).to.exist;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should work without attachments', function(done) {
|
|
keychainStub.getReceiverPublicKey.yields(null, {
|
|
_id: "fcf8b4aa-5d09-4089-8b4f-e3bc5091daf3",
|
|
userId: "safewithme.testuser@gmail.com",
|
|
publicKey: publicKey
|
|
});
|
|
pgpStub.exportKeys.yields(null, {});
|
|
pgpStub.encrypt.yields(null, 'asdfasfd');
|
|
smtpClientStub.send.yields();
|
|
|
|
emailDao.smtpSend(dummyMail, function(err) {
|
|
expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
|
|
expect(pgpStub.exportKeys.calledOnce).to.be.true;
|
|
expect(pgpStub.encrypt.calledOnce).to.be.true;
|
|
expect(smtpClientStub.send.calledOnce).to.be.true;
|
|
expect(smtpClientStub.send.calledWith(sinon.match(function(o) {
|
|
return typeof o.attachments === 'undefined';
|
|
}))).to.be.true;
|
|
expect(err).to.not.exist;
|
|
done();
|
|
});
|
|
});
|
|
|
|
// it('should work with attachments', function(done) {
|
|
// dummyMail.attachments = [{
|
|
// fileName: 'bar.txt',
|
|
// contentType: 'text/plain',
|
|
// binStr: 'barbarbarbarbar'
|
|
// }];
|
|
// keychainStub.getReceiverPublicKey.yields(null, {
|
|
// _id: "fcf8b4aa-5d09-4089-8b4f-e3bc5091daf3",
|
|
// userId: "safewithme.testuser@gmail.com",
|
|
// publicKey: publicKey
|
|
// });
|
|
// pgpStub.exportKeys.yields(null, {});
|
|
// pgpStub.encrypt.yields(null, 'asdfasfd');
|
|
// smtpClientStub.send.yields();
|
|
|
|
// emailDao.smtpSend(dummyMail, function(err) {
|
|
// expect(keychainStub.getReceiverPublicKey.calledOnce).to.be.true;
|
|
// expect(pgpStub.exportKeys.calledOnce).to.be.true;
|
|
// expect(pgpStub.encrypt.calledOnce).to.be.true;
|
|
// expect(smtpClientStub.send.calledOnce).to.be.true;
|
|
// expect(smtpClientStub.send.calledWith(sinon.match(function(o) {
|
|
// var ptAt = dummyMail.attachments[0];
|
|
// var ctAt = o.attachments[0];
|
|
// return ctAt.uint8Array && !ctAt.binStr && ctAt.fileName && ctAt.fileName !== ptAt.fileName;
|
|
// }))).to.be.true;
|
|
// expect(err).to.not.exist;
|
|
// done();
|
|
// });
|
|
// });
|
|
});
|
|
|
|
describe('IMAP: list folders', function() {
|
|
var dummyFolders = [{
|
|
type: 'Inbox',
|
|
path: 'INBOX'
|
|
}, {
|
|
type: 'Outbox',
|
|
path: 'OUTBOX'
|
|
}];
|
|
|
|
it('should work on empty local db', function(done) {
|
|
devicestorageStub.listItems.yields(null, [dummyFolders]);
|
|
|
|
emailDao.imapListFolders(function(err, folders) {
|
|
expect(err).to.not.exist;
|
|
expect(devicestorageStub.listItems.calledOnce).to.be.true;
|
|
expect(folders[0].type).to.equal('Inbox');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should work with local cache', function(done) {
|
|
devicestorageStub.listItems.yields(null, []);
|
|
imapClientStub.listWellKnownFolders.yields(null, {
|
|
inbox: dummyFolders[0]
|
|
});
|
|
devicestorageStub.storeList.yields();
|
|
|
|
emailDao.imapListFolders(function(err, folders) {
|
|
expect(err).to.not.exist;
|
|
expect(devicestorageStub.listItems.calledOnce).to.be.true;
|
|
expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true;
|
|
expect(devicestorageStub.storeList.calledOnce).to.be.true;
|
|
expect(folders[0].type).to.equal('Inbox');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('IMAP: get unread message count for folder', function() {
|
|
it('should work', function(done) {
|
|
imapClientStub.unreadMessages.yields();
|
|
emailDao.unreadMessages(function(err) {
|
|
expect(imapClientStub.unreadMessages.calledOnce).to.be.true;
|
|
expect(err).to.not.exist;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('IMAP: list messages from folder', function() {
|
|
it('should work', function(done) {
|
|
imapClientStub.listMessages.yields();
|
|
emailDao.imapListMessages({
|
|
folder: 'INBOX',
|
|
offset: 0,
|
|
num: 10
|
|
}, function(err) {
|
|
expect(imapClientStub.listMessages.calledOnce).to.be.true;
|
|
expect(err).to.not.exist;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('IMAP: get message preview', function() {
|
|
it('should parse message body without attachement', function(done) {
|
|
var uid = 415;
|
|
|
|
imapClientStub.getMessagePreview.yields(null, {
|
|
uid: uid,
|
|
body: ''
|
|
});
|
|
emailDao.imapGetMessage({
|
|
folder: 'INBOX',
|
|
uid: uid
|
|
}, function(err, message) {
|
|
expect(imapClientStub.getMessagePreview.calledOnce).to.be.true;
|
|
expect(err).to.not.exist;
|
|
expect(message.uid).to.equal(uid);
|
|
expect(message.attachments).to.not.exist;
|
|
done();
|
|
});
|
|
});
|
|
|
|
// it('should parse message body and attachement', function(done) {
|
|
// var uid = 415,
|
|
// newImapClientStub = {
|
|
// getMessage: function() {}
|
|
// };
|
|
// sinon.stub(newImapClientStub, 'getMessage', function(options) {
|
|
// options.onMessageBody(null, {
|
|
// uid: uid,
|
|
// body: '',
|
|
// attachments: ['file.txt']
|
|
// });
|
|
// options.onAttachment(null, {
|
|
// uint8Array: new Uint8Array(42)
|
|
// });
|
|
// });
|
|
// emailDao._imapClient = newImapClientStub;
|
|
|
|
// emailDao.imapGetMessage({
|
|
// folder: 'INBOX',
|
|
// uid: uid
|
|
// }, function(err, message) {
|
|
// expect(newImapClientStub.getMessagePreview.calledOnce).to.be.true;
|
|
// expect(err).to.not.exist;
|
|
// expect(message.uid).to.equal(uid);
|
|
// expect(message.attachments[0].uint8Array).to.exist;
|
|
// emailDao._imapClient = imapClientStub;
|
|
// done();
|
|
// });
|
|
// });
|
|
});
|
|
|
|
describe('IMAP: move messages', function() {
|
|
it('should move messages and remove from local storage', function(done) {
|
|
imapClientStub.moveMessage.yields();
|
|
devicestorageStub.removeList.yields();
|
|
|
|
emailDao.imapMoveMessage({
|
|
folder: 'ORIGIN',
|
|
uid: 1234,
|
|
destination: 'DESTINATION'
|
|
}, function(err) {
|
|
expect(err).to.not.exist;
|
|
expect(imapClientStub.moveMessage.calledWith({
|
|
path: 'ORIGIN',
|
|
uid: 1234,
|
|
destination: 'DESTINATION'
|
|
})).to.be.true;
|
|
expect(imapClientStub.moveMessage.calledOnce).to.be.true;
|
|
expect(devicestorageStub.removeList.calledOnce).to.be.true;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not remove from local storage after imap error', function(done) {
|
|
imapClientStub.moveMessage.yields(new Error('tis a silly place...'));
|
|
|
|
emailDao.imapMoveMessage({
|
|
folder: 'ORIGIN',
|
|
uid: 1234,
|
|
destination: 'DESTINATION'
|
|
}, function(err) {
|
|
expect(err).to.exist;
|
|
expect(imapClientStub.moveMessage.calledWith({
|
|
path: 'ORIGIN',
|
|
uid: 1234,
|
|
destination: 'DESTINATION'
|
|
})).to.be.true;
|
|
expect(imapClientStub.moveMessage.calledOnce).to.be.true;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('IMAP: delete messages', function() {
|
|
it('should delete messages and remove from local storage', function(done) {
|
|
imapClientStub.deleteMessage.yields();
|
|
devicestorageStub.removeList.yields();
|
|
|
|
emailDao.imapDeleteMessage({
|
|
folder: 'FOLDAAAA',
|
|
uid: 1234
|
|
}, function(err) {
|
|
expect(err).to.not.exist;
|
|
expect(imapClientStub.deleteMessage.calledWith({
|
|
path: 'FOLDAAAA',
|
|
uid: 1234
|
|
})).to.be.true;
|
|
expect(imapClientStub.deleteMessage.calledOnce).to.be.true;
|
|
expect(devicestorageStub.removeList.calledOnce).to.be.true;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not remove from local storage after imap error', function(done) {
|
|
imapClientStub.deleteMessage.yields(new Error('tis a silly place...'));
|
|
|
|
emailDao.imapDeleteMessage({
|
|
folder: 'FOLDAAAA',
|
|
uid: 1234
|
|
}, function(err) {
|
|
expect(err).to.exist;
|
|
expect(imapClientStub.deleteMessage.calledWith({
|
|
path: 'FOLDAAAA',
|
|
uid: 1234
|
|
})).to.be.true;
|
|
expect(imapClientStub.deleteMessage.calledOnce).to.be.true;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('IMAP: sync messages to local storage', function() {
|
|
it('should not list unencrypted messages', function(done) {
|
|
imapClientStub.listMessages.yields(null, [{
|
|
uid: 413,
|
|
}, {
|
|
uid: 414,
|
|
}]);
|
|
imapClientStub.getMessagePreview.yields(null, {
|
|
body: 'asdf'
|
|
});
|
|
devicestorageStub.removeList.yields();
|
|
devicestorageStub.storeList.yields();
|
|
|
|
emailDao.imapSync({
|
|
folder: 'INBOX',
|
|
offset: 0,
|
|
num: 2
|
|
}, function(err) {
|
|
expect(err).to.not.exist;
|
|
expect(imapClientStub.listMessages.calledOnce).to.be.true;
|
|
expect(imapClientStub.getMessagePreview.called).to.be.false;
|
|
expect(devicestorageStub.removeList.calledOnce).to.be.true;
|
|
expect(devicestorageStub.storeList.calledOnce).to.be.true;
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should work', function(done) {
|
|
imapClientStub.listMessages.yields(null, [{
|
|
uid: 413,
|
|
subject: app.string.subject
|
|
}, {
|
|
uid: 414,
|
|
subject: app.string.subject
|
|
}]);
|
|
imapClientStub.getMessagePreview.yields(null, {
|
|
body: 'asdf'
|
|
});
|
|
devicestorageStub.removeList.yields();
|
|
devicestorageStub.storeList.yields();
|
|
|
|
emailDao.imapSync({
|
|
folder: 'INBOX',
|
|
offset: 0,
|
|
num: 2
|
|
}, function(err) {
|
|
expect(err).to.not.exist;
|
|
expect(imapClientStub.listMessages.calledOnce).to.be.true;
|
|
expect(imapClientStub.getMessagePreview.calledTwice).to.be.true;
|
|
expect(devicestorageStub.removeList.calledOnce).to.be.true;
|
|
expect(devicestorageStub.storeList.calledOnce).to.be.true;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('IMAP: list messages from local storage', function() {
|
|
it('should work', function(done) {
|
|
dummyMail.body = app.string.cryptPrefix + btoa('asdf') + app.string.cryptSuffix;
|
|
devicestorageStub.listItems.yields(null, [dummyMail, dummyMail]);
|
|
keychainStub.getReceiverPublicKey.yields(null, {
|
|
_id: "fcf8b4aa-5d09-4089-8b4f-e3bc5091daf3",
|
|
userId: "safewithme.testuser@gmail.com",
|
|
publicKey: publicKey
|
|
});
|
|
pgpStub.decrypt.yields(null, JSON.stringify({
|
|
body: 'test body',
|
|
subject: 'test subject'
|
|
}));
|
|
|
|
emailDao.listMessages({
|
|
folder: 'INBOX',
|
|
offset: 0,
|
|
num: 2
|
|
}, function(err, emails) {
|
|
expect(devicestorageStub.listItems.calledOnce).to.be.true;
|
|
expect(keychainStub.getReceiverPublicKey.calledTwice).to.be.true;
|
|
expect(pgpStub.decrypt.calledTwice).to.be.true;
|
|
expect(err).to.not.exist;
|
|
expect(emails.length).to.equal(2);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('IMAP: mark message as read', function() {
|
|
it('should work', function(done) {
|
|
imapClientStub.updateFlags.yields();
|
|
|
|
emailDao.imapMarkMessageRead({
|
|
folder: 'asdf',
|
|
uid: 1
|
|
}, function(err) {
|
|
expect(imapClientStub.updateFlags.calledOnce).to.be.true;
|
|
expect(err).to.not.exist;
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
}); |