Compare commits

...

43 Commits

Author SHA1 Message Date
Tankred Hase 43729833a5 Merge pull request #412 from tanx/master
Minor fixes to master
2015-12-17 16:13:36 +07:00
Tankred Hase b05aeea342 Remove gitter link 2015-12-17 16:02:55 +07:00
Tankred Hase 137c8c7c24 Update links in readme 2015-12-17 16:00:49 +07:00
Tankred Hase 853be194d9 Fix dompurify build and upgrade to new version 2015-12-17 12:12:38 +07:00
Tankred Hase 8b36a719c3 Use faster container based travis builds, remove whiteout build email notification from travis 2015-12-14 10:30:25 +07:00
Tankred Hase 8165416c5d Rename repo to mail 2015-12-14 10:25:58 +07:00
Tankred Hase b375d81635 Update git repo name in readme 2015-12-11 19:10:10 +07:00
Felix Hammerl e56f8c2c28 Merge pull request #394 from whiteout-io/dev/WO-1026
[WO-1026] Fix broken key upload after mail server error
2015-08-19 12:36:59 +02:00
Felix Hammerl ad3691fae9 [WO-1026] Fix broken key upload after mail server error 2015-08-19 12:10:16 +02:00
Tankred Hase e0663ab8d8 Remove winstore-jscompat (not necessary in Windows 10 anymore) 2015-08-05 14:12:13 +02:00
Tankred Hase 263b5c13b0 Upgrade to grunt-mocha-phantomjs 0.7.0 2015-08-05 14:11:25 +02:00
Tankred Hase 8636ba201b Properly mock publickey requests 2015-08-05 14:10:29 +02:00
Tankred Hase aa24881efc Fix JS build by fixating transitive browserify dep 2015-07-10 12:39:47 +02:00
Tankred Hase b7b5c1bdf5 Update README.md 2015-07-08 17:55:05 +02:00
Tankred Hase 59d38f9b14 Bump mobile version code 2015-06-26 11:28:25 +02:00
Tankred Hase c44984b2f3 Bump mobile version code 2015-06-18 11:58:57 +02:00
Felix Hammerl c31c320e83 Merge pull request #381 from whiteout-io/WO-1000
Remove invite code from app
2015-06-18 11:52:27 +02:00
Tankred Hase 7f49e691db Remove invite code from app 2015-06-18 10:45:18 +02:00
Tankred Hase a346f1612e bump mobile version 2015-06-15 13:43:28 +02:00
Tankred Hase 6b0b71d4ff Merge pull request #377 from whiteout-io/dev/WO-997
[WO-997] Select multiple messages at once
2015-06-15 13:07:06 +02:00
Felix Hammerl 4683583a0a [WO-997] Select multiple messages at once 2015-06-15 13:02:28 +02:00
Tankred Hase b038ac2c16 Merge pull request #376 from whiteout-io/dev/WO-996
Add error handling for invalid PGP key user id
2015-06-15 12:59:59 +02:00
Tankred Hase f32863dc54 Add error handling for invalid PGP key user id 2015-06-12 16:50:03 +02:00
Felix Hammerl 39d19df187 Order folders by path instead of name to keep subfolder order 2015-06-12 15:15:44 +02:00
Tankred Hase 9bf8c758ec Merge pull request #356 from whiteout-io/dev/WO-927
[WO-927] Clear file input after key import
2015-06-12 12:20:10 +02:00
Tankred Hase 76f770a12b Merge pull request #357 from whiteout-io/audit/WO-03-014
[WO-03-014] Avoid unsinged content spoofing attack
2015-05-21 17:25:35 +02:00
Felix Hammerl 3af376b419 [WO-03-014] Avoid unsinged content spoofing attack 2015-05-21 16:25:50 +02:00
Felix Hammerl e9a8702b39 [WO-927] Clear file input after key import 2015-05-20 17:21:33 +02:00
Tankred Hase 25b9141a5f Merge pull request #353 from whiteout-io/dev/WO-891
[WO-891] Add logout to passphrase dialog
2015-05-19 18:31:16 +02:00
Tankred Hase 1d0efc02a2 Review text 2015-05-19 18:30:31 +02:00
Felix Hammerl c3362c193d Add missing mocks 2015-05-19 17:39:30 +02:00
Felix Hammerl 7f0235c9b2 [WO-981] Set style.display instead of style 2015-05-19 16:39:59 +02:00
Felix Hammerl 8e0dfacd51 [WO-891] Add logout to passphrase dialog 2015-05-19 16:21:52 +02:00
Tankred Hase 467d001483 Bump mobile version code 2015-05-18 17:05:30 +02:00
Tankred Hase e7fb3bcf6d Merge pull request #345 from whiteout-io/dev/WO-986
[WO-986] Use proper path for key download
2015-05-18 16:59:43 +02:00
Tankred Hase ce740b2109 Remove empty lines 2015-05-18 16:57:28 +02:00
Felix Hammerl 0bfaba3bd9 Merge pull request #352 from whiteout-io/dev/WO-944
[WO-03-011] Fix no Reliable Sender Indication is implemented (Medium)
2015-05-18 16:49:08 +02:00
Tankred Hase e7cbf9ed86 Fix browserify build 2015-05-18 16:14:12 +02:00
Tankred Hase c76a392abf [WO-03-011] Fix no Reliable Sender Indication is implemented (Medium)
Display sender email address next to full name
2015-05-18 15:56:31 +02:00
Felix Hammerl ca8c2d9a4f [WO-986] Use proper path for key download 2015-05-13 15:55:55 +02:00
Tankred Hase f287c4cddf Merge pull request #344 from whiteout-io/dev/WO-982
[WO-982] Add placeholder to writer
2015-05-13 11:46:26 +02:00
Felix Hammerl 59006a98d7 Remove anonymized from bug report body 2015-05-12 12:04:48 +02:00
Felix Hammerl 73fcfba2a9 [WO-982] Add placeholder to writer 2015-05-12 11:06:27 +02:00
26 changed files with 380 additions and 309 deletions

View File

@ -1,10 +1,10 @@
branch-defaults:
release/prod:
environment: mail-html5-prod
environment: mail-prod
release/test:
environment: mail-html5-test
environment: mail-test
global:
application_name: mail-html5
application_name: mail
default_ec2_keyname: null
default_platform: Node.js
default_region: eu-central-1

View File

@ -1,10 +1,7 @@
sudo: false
language: node_js
node_js:
- "0.10"
- "0.12"
before_install:
- gem install sass
- npm install -g grunt-cli
notifications:
email:
- build@whiteout.io

View File

@ -282,7 +282,6 @@ module.exports = function(grunt) {
},
app: {
src: [
'src/lib/winstore-jscompat.js',
'src/lib/underscore/underscore.js',
'node_modules/jquery/dist/jquery.min.js',
'src/lib/angular/angular.js',
@ -308,7 +307,7 @@ module.exports = function(grunt) {
},
readSandbox: {
src: [
'node_modules/dompurify/purify.js',
'node_modules/dompurify/src/purify.js',
'node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js',
'src/js/controller/app/read-sandbox.js'
],

View File

@ -1,4 +1,4 @@
Whiteout Mail [![Build Status](https://travis-ci.org/whiteout-io/mail-html5.svg?branch=master)](https://travis-ci.org/whiteout-io/mail-html5) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/whiteout-io/mail-html5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Whiteout Mail [![Build Status](https://travis-ci.org/whiteout-io/mail.svg?branch=master)](https://travis-ci.org/whiteout-io/mail)
==========
Whiteout Mail is an easy to use email client with integrated OpenPGP encryption written in pure JavaScript. Download the official version under [whiteout.io](https://whiteout.io/#product).
@ -7,15 +7,17 @@ Whiteout Mail is an easy to use email client with integrated OpenPGP encryption
### Features
You can read about product features and our future roadmap in our [FAQ](https://github.com/whiteout-io/mail-html5/wiki/FAQ).
You can read about product features and our future roadmap in our [FAQ](https://github.com/whiteout-io/mail/wiki/FAQ).
### Privacy and Security
We take the privacy of your data very seriously. Here are some of the technical details:
* The code has undergone a [full security audit](https://blog.whiteout.io/2015/06/11/whiteout-mail-1-0-and-security-audit-by-cure53/) by [Cure53](https://cure53.de).
* Messages are [encrypted end-to-end ](http://en.wikipedia.org/wiki/End-to-end_encryption) using the [OpenPGP](http://en.wikipedia.org/wiki/Pretty_Good_Privacy) standard. This means that only you and the recipient can read your mail. Your messages and private PGP key are stored only on your computer (in IndexedDB).
* Users have the option to use [encrypted private key sync](https://github.com/whiteout-io/mail-html5/wiki/Secure-OpenPGP-Key-Pair-Synchronization-via-IMAP) if they want to use Whiteout on multiple devices.
* Users have the option to use [encrypted private key sync](https://github.com/whiteout-io/mail/wiki/Secure-OpenPGP-Key-Pair-Synchronization-via-IMAP) if they want to use Whiteout on multiple devices.
* [Content Security Policy (CSP)](http://www.html5rocks.com/en/tutorials/security/content-security-policy/) is enforced to prevent injection attacks.
@ -25,7 +27,7 @@ We take the privacy of your data very seriously. Here are some of the technical
* Like most native email clients, whiteout mail uses raw [TCP sockets](http://developer.chrome.com/apps/socket.html) to communicate directly with your mail server via IMAP/SMTP. TLS is used to protect your password and message data in transit.
* The app is deployed as a signed [Chrome Packaged App](https://developer.chrome.com/apps/about_apps.html) with [auditable static versions](https://github.com/whiteout-io/mail-html5/releases) in order to prevent [problems with host-based security](https://blog.whiteout.io/2014/04/13/heartbleed-and-javascript-crypto/).
* The app is deployed as a signed [Chrome Packaged App](https://developer.chrome.com/apps/about_apps.html) with [auditable static versions](https://github.com/whiteout-io/mail/releases) in order to prevent [problems with host-based security](https://blog.whiteout.io/2014/04/13/heartbleed-and-javascript-crypto/).
* The app can also be used from any modern web browser in environments where installing an app is not possible (e.g. a locked down corporate desktop). The IMAP/SMTP TLS sessions are still terminated in the user's browser using JS crypto ([Forge](https://github.com/digitalbazaar/forge)), but the encrypted TLS payload is proxied via [socket.io](http://socket.io/), due to the lack of raw sockets in the browser. **Please keep in mind that this mode of operation is not as secure as using the signed packaged app, since users must trust the webserver to deliver the correct code. This mode will still protect user against passive attacks like wiretapping (since PGP and TLS are still applied in the user's browser), but not against active attacks from the webserver. So it's best to decide which threat model applies to you.**
@ -37,11 +39,11 @@ We take the privacy of your data very seriously. Here are some of the technical
* We will launch a bug bounty program later on for independent security researchers. If you find any security vulnerabilities, don't hesitate to contact us [security@whiteout.io](mailto:security@whiteout.io).
* You can also just create an [issue](https://github.com/whiteout-io/mail-html5/issues) on GitHub if you're missing a feature or just want to give us feedback. It would be much appreciated!
* You can also just create an [issue](https://github.com/whiteout-io/mail/issues) on GitHub if you're missing a feature or just want to give us feedback. It would be much appreciated!
### Testing
You can download a prebuilt bundle under [releases](https://github.com/whiteout-io/mail-html5/releases) or build your own from source (requires [node.js](http://nodejs.org/download/), [grunt](http://gruntjs.com/getting-started#installing-the-cli) and [sass](http://sass-lang.com/install)):
You can download a prebuilt bundle under [releases](https://github.com/whiteout-io/mail/releases) or build your own from source (requires [node.js](http://nodejs.org/download/), [grunt](http://gruntjs.com/getting-started#installing-the-cli) and [sass](http://sass-lang.com/install)):
npm install && npm test
@ -69,7 +71,7 @@ The App can be used either as a Chrome Packaged App or just by hosting it on you
Clone the git repository
git clone https://github.com/whiteout-io/mail-html5.git
git clone https://github.com/whiteout-io/mail.git
Build and generate the `dist/` directory:

View File

@ -3,21 +3,6 @@
"description": "Mail App with integrated OpenPGP encryption.",
"author": "Whiteout Networks",
"homepage": "https://whiteout.io",
"repository": {
"type": "git",
"url": "https://github.com/whiteout-io/mail-html5.git"
},
"keywords": [
"email",
"mail",
"client",
"app",
"openpgp",
"pgp",
"gpg",
"imap",
"smtp"
],
"engines": {
"node": ">=0.10"
},
@ -40,11 +25,12 @@
"browsersmtp": "https://github.com/whiteout-io/browsersmtp/tarball/master",
"chai": "~1.9.2",
"crypto-lib": "~0.2.1",
"dompurify": "~0.6.3",
"dompurify": "~0.7.3",
"grunt": "~0.4.1",
"grunt-angular-templates": "~0.5.7",
"grunt-autoprefixer": "~0.7.2",
"grunt-browserify": "^3.0.1",
"grunt-browserify": "3.7.0",
"insert-module-globals": "6.5.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-compress": "~0.5.2",
"grunt-contrib-concat": "^0.5.0",
@ -57,7 +43,7 @@
"grunt-csso": "~0.6.1",
"grunt-exorcise": "^0.2.0",
"grunt-manifest": "^0.4.0",
"grunt-mocha-phantomjs": "^0.6.0",
"grunt-mocha-phantomjs": "^0.7.0",
"grunt-shell": "~1.1.1",
"grunt-string-replace": "~1.0.0",
"grunt-svgmin": "~1.0.0",

View File

@ -99,7 +99,7 @@ appCfg.string = {
certificateFaqLink: 'https://github.com/whiteout-io/mail-html5/wiki/FAQ#what-does-the-ssl-certificate-for-the-mail-server--changed-mean',
bugReportTitle: 'Report a bug',
bugReportSubject: '[Bug] I want to report a bug',
bugReportBody: 'Steps to reproduce\n1. \n2. \n3. \n\nWhat happens?\n\n\nWhat do you expect to happen instead?\n\n\n\n== PLEASE DONT PUT ANY KEYS HERE! ==\n\n\n## Log\n\nBelow is the log. It includes your interactions with your email provider in an anonymized way from the point where you started the app for the last time. Any information provided by you will be used for the porpose of locating and fixing the bug you reported. It will be deleted subsequently. However, you can edit this log and/or remove log data in the event that something would show up.\n\nUser-Agent: {0}\nVersion: {1}\n\n',
bugReportBody: 'Steps to reproduce\n1. \n2. \n3. \n\nWhat happens?\n\n\nWhat do you expect to happen instead?\n\n\n\n== PLEASE DONT PUT ANY KEYS HERE! ==\n\n\n## Log\n\nBelow is the log. It includes your interactions with your email provider from the point where you started the app for the last time. Login data and email content has been stripped. Any information provided by you will be used for the purpose of locating and fixing the bug you reported. It will be deleted subsequently. However, you can edit this log and/or remove log data in the event that something would show up.\n\nUser-Agent: {0}\nVersion: {1}\n\n',
supportAddress: 'mail.support@whiteout.io',
connDocOffline: 'It appears that you are offline. Please retry when you are online.',
connDocTlsWrongCert: 'A connection to {0} was rejected because the TLS certificate is invalid. Please have a look at the FAQ for information on how to fix this error.',
@ -109,5 +109,7 @@ appCfg.string = {
connDocNoInbox: 'We could not detect an IMAP inbox folder on {0}. Please have a look at the FAQ for information on how to fix this error.',
connDocGenericError: 'There was an error connecting to {0}: {1}',
logoutTitle: 'Logout',
logoutMessage: 'Are you sure you want to log out? Please back up your encryption key before proceeding!'
};
logoutMessage: 'Are you sure you want to log out? Please back up your encryption key before proceeding!',
removePreAuthAccountTitle: 'Remove account',
removePreAuthAccountMessage: 'Are you sure you want to remove your account from this device?'
};

View File

@ -8,6 +8,42 @@ var ActionBarCtrl = function($scope, $q, email, dialog, status) {
// scope functions
//
$scope.CHECKNONE = 0;
$scope.CHECKALL = 1;
$scope.CHECKUNREAD = 2;
$scope.CHECKREAD = 3;
$scope.CHECKFLAGGED = 4;
$scope.CHECKUNFLAGGED = 5;
$scope.CHECKENCRYPTED = 6;
$scope.CHECKUNENCRYPTED = 7;
$scope.check = function(option) {
currentFolder().messages.forEach(function(email) {
if (!email.from) {
// only mark loaded messages, not the dummy messages
return;
}
if (option === $scope.CHECKNONE) {
email.checked = false;
} else if (option === $scope.CHECKALL) {
email.checked = true;
} else if (option === $scope.CHECKUNREAD) {
email.checked = !!email.unread;
} else if (option === $scope.CHECKREAD) {
email.checked = !email.unread;
} else if (option === $scope.CHECKFLAGGED) {
email.checked = !!email.flagged;
} else if (option === $scope.CHECKUNFLAGGED) {
email.checked = !email.flagged;
} else if (option === $scope.CHECKENCRYPTED) {
email.checked = !!email.encrypted;
} else if (option === $scope.CHECKUNENCRYPTED) {
email.checked = !email.encrypted;
}
});
};
/**
* Move a single message from the currently selected folder to another folder
* @param {Object} message The message that is to be moved

View File

@ -52,8 +52,7 @@ var CreateAccountCtrl = function($scope, $location, $routeParams, $q, auth, admi
return admin.createUser({
emailAddress: emailAddress,
password: $scope.pass,
phone: phone.internationalNumber,
betaCode: $scope.betaCode.toUpperCase()
phone: phone.internationalNumber
});
}).then(function() {

View File

@ -1,8 +1,10 @@
'use strict';
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, keychain) {
var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, auth, keychain, account, dialog, appConfig) {
!$routeParams.dev && !auth.isInitialized() && $location.path('/'); // init app
var str = appConfig.string;
$scope.confirmPassphrase = function() {
if ($scope.form.$invalid) {
$scope.errMsg = 'Please fill out all required fields!';
@ -38,6 +40,18 @@ var LoginExistingCtrl = function($scope, $location, $routeParams, $q, email, aut
}).catch(displayError);
};
$scope.logout = function() {
return dialog.confirm({
title: str.removePreAuthAccountTitle,
message: str.removePreAuthAccountMessage,
callback: function(confirm) {
if (confirm) {
account.logout().catch(dialog.error);
}
}
});
};
function displayError(err) {
$scope.busy = false;
$scope.incorrect = true;

View File

@ -106,7 +106,7 @@ PGP.prototype.getKeyId = function(keyArmored) {
* Read all relevant params of an armored key.
*/
PGP.prototype.getKeyParams = function(keyArmored) {
var key, packet, userIds;
var key, packet, userIds, emailAddress;
// process armored key input
if (keyArmored) {
@ -122,15 +122,24 @@ PGP.prototype.getKeyParams = function(keyArmored) {
// read user names and email addresses
userIds = [];
key.getUserIds().forEach(function(userId) {
if (!userId || userId.indexOf('<') < 0 || userId.indexOf('>') < 0) {
return;
}
userIds.push({
name: userId.split('<')[0].trim(),
emailAddress: userId.split('<')[1].split('>')[0].trim()
});
});
// check user ID
emailAddress = userIds[0] && userIds[0].emailAddress;
if (!emailAddress) {
throw new Error('Cannot parse PGP key user ID!');
}
return {
_id: packet.getKeyId().toHex().toUpperCase(),
userId: userIds[0].emailAddress, // the primary (first) email address of the key
userId: emailAddress, // the primary (first) email address of the key
userIds: userIds, // a dictonary of all the key's name/address pairs
fingerprint: packet.getFingerprint().toUpperCase(),
algorithm: packet.algorithm,

View File

@ -8,6 +8,7 @@ ngModule.directive('keyfileInput', function() {
for (var i = 0; i < e.target.files.length; i++) {
importKey(e.target.files.item(i));
}
elm.val(null); // clear input
});
function importKey(file) {

View File

@ -938,8 +938,10 @@ Email.prototype.onConnect = function(imap) {
Email.prototype.onDisconnect = function() {
// logout of imap-client
// ignore error, because it's not problem if logout fails
this._imapClient.stopListeningForChanges(function() {});
this._imapClient.logout(function() {});
if (this._imapClient) {
this._imapClient.stopListeningForChanges(function() {});
this._imapClient.logout(function() {});
}
// discard clients
this._account.online = false;
@ -1181,7 +1183,7 @@ Email.prototype._updateFolders = function() {
return 1;
} else {
// non-wellknown folders should be sorted case-insensitive
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
return a.path.toLowerCase().localeCompare(b.path.toLowerCase());
}
});
@ -1593,7 +1595,7 @@ Email.prototype._extractBody = function(message) {
}
// any content before/after the PGP block will be discarded, untrusted attachments and html is ignored
var clearSignedMatch = /^-{5}BEGIN PGP SIGNED MESSAGE-{5}\nHash:[ ][^\n]+\n(?:[A-Za-z]+:[ ][^\n]+\n)*\n([\s\S]*)\n-{5}BEGIN PGP SIGNATURE-{5}[\S\s]*-{5}END PGP SIGNATURE-{5}$/im.exec(body);
var clearSignedMatch = /^-{5}BEGIN PGP SIGNED MESSAGE-{5}\nHash:[ ][^\n]+\n(?:[A-Za-z]+:[ ][^\n]+\n)*\n([\s\S]*?)\n-{5}BEGIN PGP SIGNATURE-{5}[\S\s]*-{5}END PGP SIGNATURE-{5}$/im.exec(body);
if (clearSignedMatch) {
// PGP/INLINE signed
message.signed = true;

View File

@ -102,24 +102,39 @@ PrivateKey.prototype.upload = function(options) {
return new Promise(function(resolve) {
if (!options._id || !options.userId || !options.encryptedPrivateKey || !options.salt || !options.iv) {
throw new Error('Incomplete arguments!');
throw new Error('Incomplete arguments for key upload!');
}
resolve();
}).then(function() {
// create imap folder
return self._imap.createFolder({
path: IMAP_KEYS_FOLDER
}).then(function(fullPath) {
// Some servers (Exchange, Cyrus) error when creating an existing IMAP mailbox instead of
// responding with ALREADYEXISTS. Hence we search for the folder before uploading.
self._axe.debug('Searching imap folder for key upload...');
return self._getFolder().then(function(fullPath) {
path = fullPath;
self._axe.debug('Successfully created imap folder ' + path);
}).catch(function(err) {
var prettyErr = new Error('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed: ' + err.message);
self._axe.error(prettyErr);
throw prettyErr;
}).catch(function() {
// create imap folder
self._axe.debug('Folder not found, creating imap folder.');
return self._imap.createFolder({
path: IMAP_KEYS_FOLDER
}).then(function(fullPath) {
path = fullPath;
self._axe.debug('Successfully created imap folder ' + path);
}).catch(function(err) {
var prettyErr = new Error('Creating imap folder ' + IMAP_KEYS_FOLDER + ' failed: ' + err.message);
self._axe.error(prettyErr);
throw prettyErr;
});
});
}).then(createMessage).then(function(message) {
// upload to imap folder
self._axe.debug('Uploading key...');
return self._imap.uploadMessage({
path: path,
message: message
@ -165,9 +180,13 @@ PrivateKey.prototype.upload = function(options) {
* Check if matching private key is stored in IMAP.
*/
PrivateKey.prototype.isSynced = function() {
return this._fetchMessage({
userId: this._auth.emailAddress,
keyId: this._pgp.getKeyId()
var self = this;
return self._getFolder().then(function(path) {
return self._fetchMessage({
keyId: self._pgp.getKeyId(),
path: path
});
}).then(function(msg) {
return !!msg;
}).catch(function() {
@ -183,18 +202,24 @@ PrivateKey.prototype.isSynced = function() {
*/
PrivateKey.prototype.download = function(options) {
var self = this,
message;
path, message;
return self._fetchMessage(options).then(function(msg) {
if (!msg) {
throw new Error('Private key not synced!');
}
return self._getFolder().then(function(fullPath) {
path = fullPath;
return self._fetchMessage({
keyId: options.keyId,
path: path
}).then(function(msg) {
if (!msg) {
throw new Error('Private key not synced!');
}
message = msg;
message = msg;
});
}).then(function() {
// get the body for the message
return self._imap.getBodyParts({
path: IMAP_KEYS_FOLDER,
path: path,
uid: message.uid,
bodyParts: message.bodyParts
});
@ -282,17 +307,9 @@ PrivateKey.prototype.decrypt = function(options) {
});
};
PrivateKey.prototype._fetchMessage = function(options) {
PrivateKey.prototype._getFolder = function() {
var self = this;
if (!options.userId || !options.keyId) {
return new Promise(function() {
throw new Error('Incomplete arguments!');
});
}
// get the metadata for the message
return self._imap.listWellKnownFolders().then(function(wellKnownFolders) {
var paths = []; // gathers paths
@ -318,12 +335,21 @@ PrivateKey.prototype._fetchMessage = function(options) {
}
return paths[0];
});
};
}).then(function(path) {
return self._imap.listMessages({
path: path,
PrivateKey.prototype._fetchMessage = function(options) {
var self = this;
if (!options.keyId) {
return new Promise(function() {
throw new Error('Incomplete arguments!');
});
}
// get the metadata for the message
return self._imap.listMessages({
path: options.path
}).then(function(messages) {
if (!messages.length) {
// message has been deleted in the meantime

View File

@ -25,10 +25,10 @@ Download.prototype.createDownload = function(options) {
supportsBlob = !!new Blob();
} catch (e) {}
if (typeof a.download !== "undefined" && supportsBlob) {
if (typeof a.download !== 'undefined' && supportsBlob) {
// ff 30+, chrome 27+ (android: 37+)
document.body.appendChild(a);
a.style = "display: none";
a.style.display = 'none';
a.href = window.URL.createObjectURL(new Blob([content], {
type: contentType
}));
@ -52,15 +52,15 @@ Download.prototype.createDownload = function(options) {
var url = window.URL.createObjectURL(new Blob([content], {
type: contentType
}));
var newTab = window.open(url, "_blank");
var newTab = window.open(url, '_blank');
if (!newTab) {
window.location.href = url;
}
} else {
// anything else, where anything at all is better than nothing
if (typeof content !== "string" && content.buffer) {
if (typeof content !== 'string' && content.buffer) {
content = util.arrBuf2BinStr(content.buffer);
}
window.open('data:' + contentType + ';base64,' + btoa(content), "_blank");
window.open('data:' + contentType + ';base64,' + btoa(content), '_blank');
}
};

View File

@ -1,174 +0,0 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// JavaScript Dynamic Content shim for Windows Store apps
(function () {
if (window.MSApp && MSApp.execUnsafeLocalFunction) {
// Some nodes will have an "attributes" property which shadows the Node.prototype.attributes property
// and means we don't actually see the attributes of the Node (interestingly the VS debug console
// appears to suffer from the same issue).
//
var Element_setAttribute = Object.getOwnPropertyDescriptor(Element.prototype, "setAttribute").value;
var Element_removeAttribute = Object.getOwnPropertyDescriptor(Element.prototype, "removeAttribute").value;
var HTMLElement_insertAdjacentHTMLPropertyDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "insertAdjacentHTML");
var Node_get_attributes = Object.getOwnPropertyDescriptor(Node.prototype, "attributes").get;
var Node_get_childNodes = Object.getOwnPropertyDescriptor(Node.prototype, "childNodes").get;
var detectionDiv = document.createElement("div");
function getAttributes(element) {
return Node_get_attributes.call(element);
}
function setAttribute(element, attribute, value) {
try {
Element_setAttribute.call(element, attribute, value);
} catch (e) {
// ignore
}
}
function removeAttribute(element, attribute) {
Element_removeAttribute.call(element, attribute);
}
function childNodes(element) {
return Node_get_childNodes.call(element);
}
function empty(element) {
while (element.childNodes.length) {
element.removeChild(element.lastChild);
}
}
function insertAdjacentHTML(element, position, html) {
HTMLElement_insertAdjacentHTMLPropertyDescriptor.value.call(element, position, html);
}
function inUnsafeMode() {
var isUnsafe = true;
try {
detectionDiv.innerHTML = "<test/>";
}
catch (ex) {
isUnsafe = false;
}
return isUnsafe;
}
function cleanse(html, targetElement) {
var cleaner = document.implementation.createHTMLDocument("cleaner");
empty(cleaner.documentElement);
MSApp.execUnsafeLocalFunction(function () {
insertAdjacentHTML(cleaner.documentElement, "afterbegin", html);
});
var scripts = cleaner.documentElement.querySelectorAll("script");
Array.prototype.forEach.call(scripts, function (script) {
switch (script.type.toLowerCase()) {
case "":
script.type = "text/inert";
break;
case "text/javascript":
case "text/ecmascript":
case "text/x-javascript":
case "text/jscript":
case "text/livescript":
case "text/javascript1.1":
case "text/javascript1.2":
case "text/javascript1.3":
script.type = "text/inert-" + script.type.slice("text/".length);
break;
case "application/javascript":
case "application/ecmascript":
case "application/x-javascript":
script.type = "application/inert-" + script.type.slice("application/".length);
break;
default:
break;
}
});
function cleanseAttributes(element) {
var attributes = getAttributes(element);
if (attributes && attributes.length) {
// because the attributes collection is live it is simpler to queue up the renames
var events;
for (var i = 0, len = attributes.length; i < len; i++) {
var attribute = attributes[i];
var name = attribute.name;
if ((name[0] === "o" || name[0] === "O") &&
(name[1] === "n" || name[1] === "N")) {
events = events || [];
events.push({ name: attribute.name, value: attribute.value });
}
}
if (events) {
for (var i = 0, len = events.length; i < len; i++) {
var attribute = events[i];
removeAttribute(element, attribute.name);
setAttribute(element, "x-" + attribute.name, attribute.value);
}
}
}
var children = childNodes(element);
for (var i = 0, len = children.length; i < len; i++) {
cleanseAttributes(children[i]);
}
}
cleanseAttributes(cleaner.documentElement);
var cleanedNodes = [];
if (targetElement.tagName === 'HTML') {
cleanedNodes = Array.prototype.slice.call(document.adoptNode(cleaner.documentElement).childNodes);
} else {
if (cleaner.head) {
cleanedNodes = cleanedNodes.concat(Array.prototype.slice.call(document.adoptNode(cleaner.head).childNodes));
}
if (cleaner.body) {
cleanedNodes = cleanedNodes.concat(Array.prototype.slice.call(document.adoptNode(cleaner.body).childNodes));
}
}
return cleanedNodes;
}
function cleansePropertySetter(property, setter) {
var propertyDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, property);
var originalSetter = propertyDescriptor.set;
Object.defineProperty(HTMLElement.prototype, property, {
get: propertyDescriptor.get,
set: function (value) {
if(window.WinJS && window.WinJS._execUnsafe && inUnsafeMode()) {
originalSetter.call(this, value);
} else {
var that = this;
var nodes = cleanse(value, that);
MSApp.execUnsafeLocalFunction(function () {
setter(propertyDescriptor, that, nodes);
});
}
},
enumerable: propertyDescriptor.enumerable,
configurable: propertyDescriptor.configurable,
});
}
cleansePropertySetter("innerHTML", function (propertyDescriptor, target, elements) {
empty(target);
for (var i = 0, len = elements.length; i < len; i++) {
target.appendChild(elements[i]);
}
});
cleansePropertySetter("outerHTML", function (propertyDescriptor, target, elements) {
for (var i = 0, len = elements.length; i < len; i++) {
target.insertAdjacentElement("afterend", elements[i]);
}
target.parentNode.removeChild(target);
});
}
}());

View File

@ -1,6 +1,6 @@
{
"packageId": "io.whiteout.WhiteoutMail",
"versionCode": 24,
"versionCode": 28,
"CFBundleVersion": "1",
"ios": {

View File

@ -191,6 +191,9 @@
.mail-addresses__stripped {
display: inline;
}
.read__sender-address {
display: none;
}
}
}
}

View File

@ -1,5 +1,9 @@
<div class="action-bar" ng-controller="ActionBarCtrl">
<div class="action-bar__primary">
<button class="btn btn--light-dropdown" ng-hide="state.read.open" wo-dropdown="#dropdown-checkmessages">
<svg role="presentation"><use xlink:href="#icon-check" />Check messages</svg>
<svg class="btn__dropdown" role="presentation"><use xlink:href="#icon-dropdown" /></svg>
</button>
<button class="btn btn--light" wo-touch="state.read.open ? deleteMessage(state.mailList.selected) : deleteCheckedMessages()">Delete</button>
<button class="btn btn--light" wo-touch="state.read.open ? moveMessage(state.mailList.selected, getJunkFolder()) : moveCheckedMessages(getJunkFolder())">Spam</button>
<button class="btn btn--light-dropdown" wo-dropdown="#dropdown-folder">
@ -34,6 +38,17 @@
</li>
</ul><!--/dropdown-->
<ul id="dropdown-checkmessages" class="dropdown">
<li><button wo-touch="check(CHECKALL)">All</button></li>
<li><button wo-touch="check(CHECKNONE)">None</button></li>
<li><button wo-touch="check(CHECKENCRYPTED)">Encrypted</button></li>
<li><button wo-touch="check(CHECKUNENCRYPTED)">Unencrypted</button></li>
<li><button wo-touch="check(CHECKUNREAD)">Unread</button></li>
<li><button wo-touch="check(CHECKREAD)">Read</button></li>
<li><button wo-touch="check(CHECKFLAGGED)">Starred</button></li>
<li><button wo-touch="check(CHECKUNFLAGGED)">Unstarred</button></li>
</ul><!--/checkmessages-->
<ul id="dropdown-more" class="dropdown">
<li><button wo-touch="state.read.open ? markMessage(state.mailList.selected, false) : markCheckedMessages(false)">Mark as read</button></li>
<li><button wo-touch="state.read.open ? markMessage(state.mailList.selected, true) : markCheckedMessages(true)">Mark as unread</button></li>

View File

@ -287,9 +287,6 @@
<input class="input-text" type="tel" ng-model="dial" required placeholder="Mobile phone (for SMS validation)" tabindex="5">
</div>
</div>
<div class="form__row">
<input class="input-text" type="text" ng-model="betaCode" required placeholder="Invitation code" tabindex="6">
</div>
<div class="spinner-block" ng-show="busy">
<span class="spinner spinner--big"></span>
</div>
@ -297,9 +294,6 @@
<button class="btn" type="submit" ng-click="showConfirm()">Create</button>
</div>
</form>
<p class="typo-paragraph">
<a href="http://eepurl.com/ba09in" target="_blank" title="Sign up for mailbox access.">Need an invitation code?</a>
</p>
</main>
<div ng-include="'tpl/page-footer.html'"></div>
</div>

View File

@ -25,7 +25,7 @@
</div>
</form>
<p class="typo-paragraph">
<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Forgot your passphrase?</a>
<a href="#" wo-touch="$event.preventDefault(); logout()" title="Remove account from device">Forgot your passphrase?</a>
</p>
</main>
<div ng-include="'tpl/page-footer.html'"></div>

View File

@ -75,7 +75,7 @@
<label>From:</label>
<span ng-repeat="u in state.mailList.selected.from">
<span class="label" ng-class="{'label--invalid': u.secure === false, 'label--invalid-clickable': u.secure === false}" ng-mouseover="getKeyId(u.address)" wo-touch="invite(u)" wo-tooltip="#fingerprint-info">
{{u.name || u.address}}
{{u.name || u.address}} <span ng-show="u.name" ng-class="{'read__sender-address': !notStripped}">&#60; {{u.address}} &#62;</span>
<svg ng-show="u.secure === false"><use xlink:href="#icon-add_contact" /></svg>
</span>

View File

@ -72,7 +72,7 @@
</ul>
</header>
<textarea class="write__body" ng-model="body" spellcheck="true" wo-focus-me="state.lightbox === 'write' && writerTitle === 'Reply'" tabindex="3"></textarea>
<textarea class="write__body" ng-model="body" spellcheck="true" wo-focus-me="state.lightbox === 'write' && writerTitle === 'Reply'" tabindex="3" placeholder="Compose email"></textarea>
</div><!--/write-->
<footer class="lightbox__controls">

View File

@ -100,6 +100,10 @@ describe('Email DAO integration tests', function() {
}, {
raw: 'Message-id: <foobar>\r\nSubject: moveme\r\n\r\nmoveme!',
uid: 900
}, {
description: "Thunderbird (no attachment - PGP/INLINE): Signed w/ unsigned content spoofing attack",
raw: "Message-ID: <53A87B12.9010706@gmail.com>\r\nDate: Mon, 23 Jun 2014 21:08:02 +0200\r\nFrom: Andris Testbox2 <safewithme.testuser@gmail.com>\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:24.0) Gecko/20100101 Thunderbird/24.2.0\r\nMIME-Version: 1.0\r\nTo: safewithme.testuser@gmail.com\r\nSubject: test6\r\nX-Enigmail-Version: 1.6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Transfer-Encoding: 7bit\r\n\r\n\r\n-----BEGIN PGP SIGNED MESSAGE-----\r\nHash: SHA512\r\n\r\ntest6\r\n-----BEGIN PGP SIGNATURE-----\r\nVersion: GnuPG v1.4.13 (Darwin)\r\nComment: GPGTools - https://gpgtools.org\r\nComment: Using GnuPG with Thunderbird - http://www.enigmail.net/\r\n\r\niQEcBAEBCgAGBQJTqHsSAAoJENf7k/zfv8I8wz4H/RWo1qJvvJtMl7GyqGGbaByX\r\n/D7/yWJzMdE0Y7J/tHIexQ/sZnmcDlHG0mtJKgI7EOh2EyV+r+78vF71Mlc+bg8g\r\n3B4TKyp0QU1Pb6SETG//FtKrU7SnkjKujHvRMpzcOcm0ZLBDpmftyWLvp9Dg3KOF\r\n5sMBGpJRn1pqX2DxXZtc1rYOmSAaxFI5jewPws0DCDkLDGp9gLyusNeDHkmAT4AG\r\nDqsDPQvW0R4Sy7aQFT7GjrdnCiLyikynkocUpR95fDnjHJ6Xbyj2Yj9/ofewPQ//\r\nMq39sIYbcqlDBAhsOlii3ekdzLS4xEOkvtFoD4pufyLj3pYY60FG4bPygcccYkI=\r\n=IkRV\r\n-----END PGP SIGNATURE-----\r\n\r\nTHIS IS UNSINGED CONTENT AND MUST NOT BE SHOWN\r\n\r\n-----BEGIN PGP SIGNATURE-----\r\n-----END PGP SIGNATURE-----\r\n",
uid: 910
}];
imapFolders = {
@ -245,6 +249,7 @@ describe('Email DAO integration tests', function() {
function onCleaned() {
userStorage = accountService._accountStore;
auth = accountService._auth;
emailDao = accountService._emailDao;
auth.setCredentials({
emailAddress: testAccount.user,
@ -253,19 +258,17 @@ describe('Email DAO integration tests', function() {
imap: {} // a preconfigured smtpclient with mocked tcp sockets
});
// stub rest request to key server
sinon.stub(emailDao._keychain._publicKeyDao, 'get').returns(resolves(mockKeyPair.publicKey));
sinon.stub(emailDao._keychain._publicKeyDao, 'getByUserId').returns(resolves(mockKeyPair.publicKey));
auth.init().then(function() {
accountService.init({
emailAddress: testAccount.user
}).then(function() {
emailDao = accountService._emailDao;
// retrieve the pgpbuilder from the emaildao and initialize the pgpmailer with the existing pgpbuilder
pgpMailer = new PgpMailer({}, emailDao._pgpbuilder);
// stub rest request to key server
sinon.stub(emailDao._keychain._publicKeyDao, 'get').returns(resolves(mockKeyPair.publicKey));
sinon.stub(emailDao._keychain._publicKeyDao, 'getByUserId').returns(resolves(mockKeyPair.publicKey));
emailDao.unlock({
passphrase: testAccount.pass,
keypair: mockKeyPair
@ -563,6 +566,14 @@ describe('Email DAO integration tests', function() {
done();
});
});
it('should parse Thunderbird (no attachment): Signed w/ PGP/INLINE including unsigned content spoofing attack', function() {
expect(inbox.messages[19].encrypted).to.be.false;
expect(inbox.messages[19].signed).to.be.true;
expect(inbox.messages[19].signaturesValid).to.be.true;
expect(inbox.messages[19].attachments.length).to.equal(0);
expect(inbox.messages[19].body).to.equal('test6');
});
});
});

View File

@ -24,9 +24,20 @@ describe('Action Bar Controller unit test', function() {
type: 'Inbox',
path: 'INBOX',
messages: [{
from: [],
checked: true
}, {
from: [],
checked: false
}, {
from: [],
flagged: true
}, {
from: [],
encrypted: true
}, {
from: [],
unread: true
}]
}
};
@ -43,6 +54,83 @@ describe('Action Bar Controller unit test', function() {
afterEach(function() {});
describe('check', function() {
it('should check all', function() {
scope.check(scope.CHECKALL);
expect(scope.state.nav.currentFolder.messages[0].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[1].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[2].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[3].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[4].checked).to.be.true;
});
it('should check none', function() {
scope.check(scope.CHECKNONE);
expect(scope.state.nav.currentFolder.messages[0].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[1].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[2].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[3].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[4].checked).to.be.false;
});
it('should check encrypted', function() {
scope.check(scope.CHECKENCRYPTED);
expect(scope.state.nav.currentFolder.messages[0].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[1].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[2].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[3].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[4].checked).to.be.false;
});
it('should check unencrypted', function() {
scope.check(scope.CHECKUNENCRYPTED);
expect(scope.state.nav.currentFolder.messages[0].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[1].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[2].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[3].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[4].checked).to.be.true;
});
it('should check unread', function() {
scope.check(scope.CHECKUNREAD);
expect(scope.state.nav.currentFolder.messages[0].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[1].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[2].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[3].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[4].checked).to.be.true;
});
it('should check read', function() {
scope.check(scope.CHECKREAD);
expect(scope.state.nav.currentFolder.messages[0].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[1].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[2].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[3].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[4].checked).to.be.false;
});
it('should check starred', function() {
scope.check(scope.CHECKFLAGGED);
expect(scope.state.nav.currentFolder.messages[0].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[1].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[2].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[3].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[4].checked).to.be.false;
});
it('should check unstarred', function() {
scope.check(scope.CHECKUNFLAGGED);
expect(scope.state.nav.currentFolder.messages[0].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[1].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[2].checked).to.be.false;
expect(scope.state.nav.currentFolder.messages[3].checked).to.be.true;
expect(scope.state.nav.currentFolder.messages[4].checked).to.be.true;
});
});
describe('deleteMessage', function() {
it('should not delete without a selected mail', function() {
scope.deleteMessage();

View File

@ -1,12 +1,14 @@
'use strict';
var Auth = require('../../../../src/js/service/auth'),
Account = require('../../../../src/js/email/account'),
Dialog = require('../../../../src/js/util/dialog'),
LoginExistingCtrl = require('../../../../src/js/controller/login/login-existing'),
EmailDAO = require('../../../../src/js/email/email'),
KeychainDAO = require('../../../../src/js/service/keychain');
describe('Login (existing user) Controller unit test', function() {
var scope, location, ctrl, emailDaoMock, authMock,
var scope, location, ctrl, emailDaoMock, authMock, accountMock, dialogMock,
emailAddress = 'fred@foo.com',
passphrase = 'asd',
keychainMock;
@ -15,6 +17,8 @@ describe('Login (existing user) Controller unit test', function() {
emailDaoMock = sinon.createStubInstance(EmailDAO);
authMock = sinon.createStubInstance(Auth);
keychainMock = sinon.createStubInstance(KeychainDAO);
accountMock = sinon.createStubInstance(Account);
dialogMock = sinon.createStubInstance(Dialog);
authMock.emailAddress = emailAddress;
@ -31,7 +35,10 @@ describe('Login (existing user) Controller unit test', function() {
$q: window.qMock,
email: emailDaoMock,
auth: authMock,
keychain: keychainMock
keychain: keychainMock,
account: accountMock,
dialog: dialogMock,
appConfig: {}
});
});
});

View File

@ -68,6 +68,14 @@ describe('Private Key DAO unit tests', function() {
});
describe('upload', function() {
beforeEach(function() {
sinon.stub(privkeyDao, '_getFolder');
});
afterEach(function() {
privkeyDao._getFolder.restore();
});
it('should fail due to invalid args', function(done) {
privkeyDao.upload({}).catch(function(err) {
expect(err.message).to.match(/Incomplete/);
@ -75,10 +83,11 @@ describe('Private Key DAO unit tests', function() {
});
});
it('should work', function(done) {
it('should work without existing folder', function(done) {
var IMAP_KEYS_FOLDER = 'openpgp_keys';
var fullPath = 'INBOX.' + IMAP_KEYS_FOLDER;
privkeyDao._getFolder.returns(rejects(new Error()));
imapClientStub.createFolder.withArgs({
path: IMAP_KEYS_FOLDER
}).returns(resolves(fullPath));
@ -95,44 +104,96 @@ describe('Private Key DAO unit tests', function() {
salt: salt,
iv: iv
}).then(function() {
expect(privkeyDao._getFolder.calledOnce).to.be.true;
expect(imapClientStub.createFolder.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
done();
});
});
it('should work with existing folder', function(done) {
var IMAP_KEYS_FOLDER = 'openpgp_keys';
var fullPath = 'INBOX.' + IMAP_KEYS_FOLDER;
privkeyDao._getFolder.returns(resolves(fullPath));
imapClientStub.uploadMessage.withArgs(sinon.match(function(arg) {
expect(arg.path).to.equal(fullPath);
expect(arg.message).to.exist;
return true;
})).returns(resolves());
privkeyDao.upload({
_id: keyId,
userId: emailAddress,
encryptedPrivateKey: encryptedPrivateKey,
salt: salt,
iv: iv
}).then(function() {
expect(privkeyDao._getFolder.calledOnce).to.be.true;
expect(imapClientStub.createFolder.called).to.be.false;
expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
done();
});
});
});
describe('isSynced', function() {
beforeEach(function() {
sinon.stub(privkeyDao, '_getFolder');
sinon.stub(privkeyDao, '_fetchMessage');
});
afterEach(function() {
privkeyDao._getFolder.restore();
privkeyDao._fetchMessage.restore();
});
it('should be synced', function(done) {
privkeyDao._getFolder.returns(resolves('foo'));
privkeyDao._fetchMessage.returns(resolves({}));
privkeyDao.isSynced().then(function(synced) {
expect(synced).to.be.true;
expect(privkeyDao._getFolder.calledOnce).to.be.true;
expect(privkeyDao._fetchMessage.calledOnce).to.be.true;
done();
});
});
it('should not be synced', function(done) {
privkeyDao._getFolder.returns(resolves());
privkeyDao._fetchMessage.returns(resolves());
privkeyDao.isSynced().then(function(synced) {
expect(synced).to.be.false;
expect(privkeyDao._getFolder.calledOnce).to.be.true;
expect(privkeyDao._fetchMessage.calledOnce).to.be.true;
done();
});
});
it('should not be synced in case of error', function(done) {
privkeyDao._getFolder.returns(rejects(new Error()));
privkeyDao.isSynced().then(function(synced) {
expect(synced).to.be.false;
expect(privkeyDao._getFolder.calledOnce).to.be.true;
done();
});
});
it('should not be synced in case of error', function(done) {
privkeyDao._getFolder.returns(resolves('foo'));
privkeyDao._fetchMessage.returns(rejects(new Error()));
privkeyDao.isSynced().then(function(synced) {
expect(synced).to.be.false;
expect(privkeyDao._getFolder.calledOnce).to.be.true;
expect(privkeyDao._fetchMessage.calledOnce).to.be.true;
done();
});
});
@ -146,15 +207,18 @@ describe('Private Key DAO unit tests', function() {
}];
beforeEach(function() {
sinon.stub(privkeyDao, '_getFolder');
sinon.stub(privkeyDao, '_fetchMessage');
sinon.stub(privkeyDao, '_parse');
});
afterEach(function() {
privkeyDao._getFolder.restore();
privkeyDao._fetchMessage.restore();
privkeyDao._parse.restore();
});
it('should fail if key not synced', function(done) {
privkeyDao._getFolder.returns(resolves('foo'));
privkeyDao._fetchMessage.returns(resolves());
privkeyDao.download({
@ -167,6 +231,7 @@ describe('Private Key DAO unit tests', function() {
});
it('should work', function(done) {
privkeyDao._getFolder.returns(resolves('foo'));
privkeyDao._fetchMessage.returns(resolves({}));
imapClientStub.getBodyParts.returns(resolves());
privkeyDao._parse.returns(resolves(root));
@ -253,14 +318,7 @@ describe('Private Key DAO unit tests', function() {
});
});
describe('_fetchMessage', function() {
it('should fail due to invalid args', function(done) {
privkeyDao._fetchMessage({}).catch(function(err) {
expect(err.message).to.match(/Incomplete/);
done();
});
});
describe('_getFolder', function() {
it('should fail if imap folder does not exist', function(done) {
imapClientStub.listWellKnownFolders.returns(resolves({
Inbox: [{
@ -270,13 +328,10 @@ describe('Private Key DAO unit tests', function() {
path: 'foo'
}]
}));
imapClientStub.listMessages.returns(rejects(new Error()));
privkeyDao._fetchMessage({
userId: emailAddress,
keyId: keyId
}).catch(function(err) {
privkeyDao._getFolder().catch(function(err) {
expect(err.message).to.match(/Imap folder/);
expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true;
done();
});
});
@ -290,6 +345,25 @@ describe('Private Key DAO unit tests', function() {
path: 'openpgp_keys'
}]
}));
privkeyDao._getFolder().then(function(path) {
expect(path).to.equal('openpgp_keys');
expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true;
done();
});
});
});
describe('_fetchMessage', function() {
it('should fail due to invalid args', function(done) {
privkeyDao._fetchMessage({}).catch(function(err) {
expect(err.message).to.match(/Incomplete/);
done();
});
});
it('should work', function(done) {
imapClientStub.listMessages.returns(resolves([{
subject: keyId
}]));
@ -299,21 +373,12 @@ describe('Private Key DAO unit tests', function() {
keyId: keyId
}).then(function(msg) {
expect(msg.subject).to.equal(keyId);
expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true;
expect(imapClientStub.listMessages.calledOnce).to.be.true;
done();
});
});
it('should work with path prefix', function(done) {
imapClientStub.listWellKnownFolders.returns(resolves({
Inbox: [{
path: 'INBOX'
}],
Other: [{
path: 'INBOX.openpgp_keys'
}]
}));
imapClientStub.listMessages.returns(resolves([{
subject: keyId
}]));
@ -323,18 +388,12 @@ describe('Private Key DAO unit tests', function() {
keyId: keyId
}).then(function(msg) {
expect(msg.subject).to.equal(keyId);
expect(imapClientStub.listWellKnownFolders.calledOnce).to.be.true;
expect(imapClientStub.listMessages.calledOnce).to.be.true;
done();
});
});
it('should work for not matching message', function(done) {
imapClientStub.listWellKnownFolders.returns(resolves({
Other: [{
path: 'INBOX.openpgp_keys'
}]
}));
imapClientStub.listMessages.returns(resolves([{
subject: '7890'
}]));
@ -349,11 +408,6 @@ describe('Private Key DAO unit tests', function() {
});
it('should work for no messages', function(done) {
imapClientStub.listWellKnownFolders.returns(resolves({
Other: [{
path: 'INBOX.openpgp_keys'
}]
}));
imapClientStub.listMessages.returns(resolves([]));
privkeyDao._fetchMessage({