[WO-188][WO-421] introduce password-based login

* add custom login ability
* encrypt email password
* allow arbitrary tcp connections
* add db migration script v3 -> v4
* add bug reporting capability with the axe logger
* fix nonexistent folder handling
* enable message upload after sent
* use plain text alternative body when replying to html mails
* Move set-credentials.* -> login-set-credentials.*
* Use login styles for set-credentials
* Make OAuth optional
This commit is contained in:
Felix Hammerl 2014-07-01 19:49:19 +02:00
parent 26f62668c6
commit 3e80138a10
37 changed files with 1814 additions and 549 deletions

View File

@ -11,9 +11,9 @@
},
"dependencies": {
"crypto-lib": "https://github.com/whiteout-io/crypto-lib/tarball/v0.2.0",
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.3.5",
"imap-client": "https://github.com/whiteout-io/imap-client/tarball/v0.3.6",
"mailreader": "https://github.com/whiteout-io/mailreader/tarball/v0.3.4",
"pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.3.6",
"pgpmailer": "https://github.com/whiteout-io/pgpmailer/tarball/v0.3.7",
"pgpbuilder": "https://github.com/whiteout-io/pgpbuilder/tarball/v0.3.5",
"requirejs": "2.1.14",
"axe": "https://github.com/whiteout-io/axe/tarball/v0.0.2",
@ -41,4 +41,4 @@
"grunt-contrib-compress": "~0.5.2",
"grunt-node-webkit-builder": "~0.1.17"
}
}
}

View File

@ -1,24 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
YWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG
EwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy
bmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP
VaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv
h8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE
ahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ
EASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC
DTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7
qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD
VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g
K4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI
KwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n
ZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB
BQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY
/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/
zG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza
HFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto
WHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6
yuGnBXj8ytqU0CwIPX4WecigUCAkVDNx
-----END CERTIFICATE-----

View File

@ -37,24 +37,62 @@ define(function(require) {
symIvSize: 96,
asymKeySize: 2048,
workerPath: 'js',
reconnectInterval: 10000,
gmail: {
clientId: clientId || '440907777130.apps.googleusercontent.com',
imap: {
secure: true,
host: 'imap.gmail.com',
port: 993,
host: 'imap.gmail.com'
secure: true,
ca: '-----BEGIN CERTIFICATE-----\r\nMIIEdjCCA16gAwIBAgIIOCAMKXEOvgcwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE\r\nBhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl\r\ncm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwNzAyMTMxMTUwWhcNMTQwOTMwMDAwMDAw\r\nWjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN\r\nTW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOaW1h\r\ncC5nbWFpbC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQdG3a\r\nCqhUvm2qrrzjONFWWGNyuqO60DfuvMfoysUmfMuenZskA9GADbZzFFhpXaj9CgIR\r\nNyXxMb1JiQ2h/0t3t94OvEBDcfZvG2aSYDWnsXClVzwMXO0PN6Sy1B/dsMv2v+DZ\r\n5mnkCV8vGiJzimHv2lI+fpAoDrDZkpEZ5KMNbck7FWAVTOogdvQhlRYLijjDXFRS\r\nudbr1HDZIA3n24EbEiXTxkhrovKB3kvyYwkSPOeAD36Ct5B0DebEqnXiwnoYe5Cv\r\nz+twRdb1vnKFfeF5aJEvED1MidD7wkmZuVQoakCv4bJt+Prk+LTwFoUGBPio+3zs\r\nEdIxUbhuXPPevnODAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI\r\nKwYBBQUHAwIwGQYDVR0RBBIwEIIOaW1hcC5nbWFpbC5jb20waAYIKwYBBQUHAQEE\r\nXDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0\r\nMCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G\r\nA1UdDgQWBBRGBrwFZ4atafE46eC+bikqv+yFkjAMBgNVHRMBAf8EAjAAMB8GA1Ud\r\nIwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW\r\neQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB\r\nRzIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQAM+EmMM+dBjufjzV/4gK56ppI/BuBy\r\n7NT/AjTu+eNt/WRKouAqdtt9+jd3WRAskH1g/UEtfuh9lJhdJo7mJuWjeyHWQvLb\r\ndRzrp43FnADabNxJUFstddnqJoiGnKk90elLx8WJ5JQtP023oZpysunGUnhZNlpA\r\nqcqJW9TrwwTo3s1VQbuTsybglDG8eiNhgp+tBcDfcGUSY/HMXjerehr0nqP0cg5m\r\n1zGMsh5QEJ96MfOiRnP6urpzEYDQTMi2fahJpmqcyytfiazQexIC43fk+8rtgKWr\r\nO4BejBviN1FmTYn/H9qgDeAuxn94ldY5/5DuX1sgcvSZCmgKjOd/EpKv\r\n-----END CERTIFICATE-----',
pinned: true
},
smtp: {
secure: true,
host: 'smtp.gmail.com',
port: 465,
host: 'smtp.gmail.com'
secure: true,
ca: '-----BEGIN CERTIFICATE-----\r\nMIIEdjCCA16gAwIBAgIIOuQOXm7sFPMwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE\r\nBhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl\r\ncm5ldCBBdXRob3JpdHkgRzIwHhcNMTMwOTEwMDc1NDQ3WhcNMTQwOTEwMDc1NDQ3\r\nWjBoMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN\r\nTW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEXMBUGA1UEAwwOc210\r\ncC5nbWFpbC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpMKDa\r\nE9bW18yuVMulny5K5YLwf7ebEpINUVPZXvp7cO6vNjl+MCHjhbB2Rkg7QVJE8eNS\r\nV0Hpq3vOuz+RQ2rPKfaeM3MFBZJ+tKscC39XmlVtmyBW5AVWy5dlO7718MQCN/L5\r\nkpYSY6RinFrf5pIlf5XSGRCo3WYndguPP1A+X4gsDKjMaWhCP5KfczLHGTY+4T+d\r\n31lDSah8CbFeMvKav0SFnyRYM36YAvAk2HH1/64Tolbx9tMAW6e6q8dU1U6W5u6+\r\nBt7WjW1iYwwfML+ZorKR9p+V070nDDN42ZE8HVZw+hOl9eMl48L/eX0eKbSGZBC2\r\n1IK16eISmcFKML1bAgMBAAGjggFBMIIBPTAdBgNVHSUEFjAUBggrBgEFBQcDAQYI\r\nKwYBBQUHAwIwGQYDVR0RBBIwEIIOc210cC5nbWFpbC5jb20waAYIKwYBBQUHAQEE\r\nXDBaMCsGCCsGAQUFBzAChh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lBRzIuY3J0\r\nMCsGCCsGAQUFBzABhh9odHRwOi8vY2xpZW50czEuZ29vZ2xlLmNvbS9vY3NwMB0G\r\nA1UdDgQWBBQEQ01ljbiIzNcZdvg6hhkpxvAmujAMBgNVHRMBAf8EAjAAMB8GA1Ud\r\nIwQYMBaAFErdBhYbvPZotXb1gba7Yhq6WoEvMBcGA1UdIAQQMA4wDAYKKwYBBAHW\r\neQIFATAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vcGtpLmdvb2dsZS5jb20vR0lB\r\nRzIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQAA3n1AtYa8ES0KDhRGEsXsWQEQp3m8\r\nX3gXB6Rfg1mDRFqap67XYZTgYtGdeUOkbmXvfUYbljyTeSIdTN6iD/tzzaiJUzPl\r\nSwCT/ylI2kSo/0Km34rA5/D84Ja/1SSdCzxx4HFU0FlOERNg7RxSsW6F+f/QmTmZ\r\nJ/3lYLI71meuut7O7G+BcFlXVphs5XSy65LkziTXikR+MRERjCKhv3AwP0oGB2+q\r\nAPMUqxtH6K6hmFE5ELtYjS4rKLbH08s8gy65y/EiaBaWKBlKG6s+r22uyxu2xmgo\r\nLFf94N1gVJXuaZXlCgVwThCtbekh8wxjHtcVw2HCZfzQemEr7oshVOX2\r\n-----END CERTIFICATE-----',
pinned: true
},
ignoreUploadOnSent: true
},
yahoo: {
imap: {
host: 'imap.mail.yahoo.com',
port: 993,
secure: true,
ca: '-----BEGIN CERTIFICATE-----\r\nMIIFUjCCBDqgAwIBAgIQdThnkckQvgouzHOsQA7ctTANBgkqhkiG9w0BAQUFADCB\r\ntTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\r\nExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2Ug\r\nYXQgaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDEvMC0GA1UEAxMm\r\nVmVyaVNpZ24gQ2xhc3MgMyBTZWN1cmUgU2VydmVyIENBIC0gRzMwHhcNMTQwNDIy\r\nMDAwMDAwWhcNMTUwNDIzMjM1OTU5WjCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\r\nCkNhbGlmb3JuaWExEjAQBgNVBAcUCVN1bm55dmFsZTETMBEGA1UEChQKWWFob28g\r\nSW5jLjEfMB0GA1UECxQWSW5mb3JtYXRpb24gVGVjaG5vbG9neTEeMBwGA1UEAxQV\r\nKi5pbWFwLm1haWwueWFob28uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\r\nCgKCAQEAw36HN2sgtMNQ0TZlqGgfqInK/UT6y3ZgqDdmFRU9D5D5675hfxcwJoS+\r\nb0hIn/UixbZDpSLhNkjAkOTAbFEFWar7628D2dU5WtCUlFMiwg2TA0Un8B9EbUi5\r\nwDrqzXDyABVnBVR5I2eKwr5cuB9ldjxAabcCyqQhVKdH0+IskRpUrvxAb84uQtJg\r\nJyNieOZAWdxg9fkubk1YKw/MgJHnaY8P4lUlYY8fIY39d6gW6My8oT0IersrH1X1\r\n/oCmqUIGM1PawXBvvpPKYdI4fCH75/UaEQ41BFSUn1NsinFYZUPlVcBCOvLFEOQi\r\nuU+4Tjybq3x7NNhd3uBxfm4jo4h5zQIDAQABo4IBgzCCAX8wNQYDVR0RBC4wLIIV\r\nKi5pbWFwLm1haWwueWFob28uY29tghNpbWFwLm1haWwueWFob28uY29tMAkGA1Ud\r\nEwQCMAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF\r\nBQcDAjBlBgNVHSAEXjBcMFoGCmCGSAGG+EUBBzYwTDAjBggrBgEFBQcCARYXaHR0\r\ncHM6Ly9kLnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGRoXaHR0cHM6Ly9kLnN5\r\nbWNiLmNvbS9ycGEwHwYDVR0jBBgwFoAUDURcFlNEwYJ+HSCrJfQBY9i+eaUwKwYD\r\nVR0fBCQwIjAgoB6gHIYaaHR0cDovL3NkLnN5bWNiLmNvbS9zZC5jcmwwVwYIKwYB\r\nBQUHAQEESzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc2Quc3ltY2QuY29tMCYGCCsG\r\nAQUFBzAChhpodHRwOi8vc2Quc3ltY2IuY29tL3NkLmNydDANBgkqhkiG9w0BAQUF\r\nAAOCAQEAVxsglXJTtBTCoTwOd6j0iJQ+P9cxFVqHcmbshEfEQBlPwr4Sp9tLJ4kj\r\nfVi0XorWU6e6e57dtYtxpcPz+6WNSNKT0B0IBOTUTIBwSLJMHxEZI6gSS/fo1agt\r\n81B06rB8Rhn4yHwyDO/9uRvXbNYiEgpa5e6gIpXY6h6p1HscQMcuROaUA9ETvGd8\r\nDKG4XSZE7QAF9iB9WSLa/IQUD4sGMDaMp2q4XkoWZTnyL1bEDKwUvw9Z17PxVmrF\r\n8c7S5HTNU+1kyZw2LJRu3SgtsYXSWA88WFiKUPuqU+EBXmbrwLAwLAJ6mVc2bGFC\r\ng5fLGbtTscaARBlb1u3Iee2Fd419jg==\r\n-----END CERTIFICATE-----',
pinned: true
},
smtp: {
host: 'smtp.mail.yahoo.com',
port: 465,
secure: true,
ca: '-----BEGIN CERTIFICATE-----\r\nMIIHITCCBgmgAwIBAgIQBlVSvxlwsqw8Kc8eVV5EKTANBgkqhkiG9w0BAQUFADBm\r\nMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\nd3cuZGlnaWNlcnQuY29tMSUwIwYDVQQDExxEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\r\nZSBDQS0zMB4XDTE0MDYwNTAwMDAwMFoXDTE1MDYyMzEyMDAwMFowYjELMAkGA1UE\r\nBhMCVVMxCzAJBgNVBAgTAkNBMRIwEAYDVQQHEwlTdW5ueXZhbGUxFDASBgNVBAoM\r\nC1lhaG9vISBJbmMuMRwwGgYDVQQDExNzbXRwLm1haWwueWFob28uY29tMIIBIjAN\r\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA339OSxTIuW6NVqKwYHFBbg4/tmEx\r\nMYpzohtObPaWgvLTsYdce+WlQFBU5rxaCMUtVsVAGJWs5uQRDVHXRLPnOWe+foqZ\r\n5ItgjYIQc1XeMv6BZEeaF3Bum5ehUyLB3y48cjbWxma1QlkZ8XAK0f9AS7ySWAen\r\no5OXJlMFAqXGtKMAhY46dzSY0wjgdrvgiomFRy0iQKV1DxGsXoXSMEszlUTQoNQ3\r\nWTsiA3O//cdWE06wWeA3/90cb7QkU3KflSoyfi878BQGpPR1L+rLNsqnc8QuucbD\r\nz4Q++rxGqgg4QYrOtmZfAn96TXhPWCwKld6FN+f3uV5ITOBFx44M5v1ILQIDAQAB\r\no4IDzTCCA8kwHwYDVR0jBBgwFoAUUOpzidsp+xCPnuUBINTeeZlIg/cwHQYDVR0O\r\nBBYEFEhOpSFQjO/QrSNfVwyInZUhyzhsMIICJAYDVR0RBIICGzCCAheCE3NtdHAu\r\nbWFpbC55YWhvby5jb22CFnNtdHAubWFpbC55YWhvby5jb20uYXKCFnNtdHAubWFp\r\nbC55YWhvby5jb20uYXWCFnNtdHAubWFpbC55YWhvby5jb20uYnKCFnNtdHAubWFp\r\nbC55YWhvby5jb20uY26CFnNtdHAubWFpbC55YWhvby5jb20uaGuCFnNtdHAubWFp\r\nbC55YWhvby5jb20ubXmCFnNtdHAubWFpbC55YWhvby5jb20ucGiCFnNtdHAubWFp\r\nbC55YWhvby5jb20uc2eCFnNtdHAubWFpbC55YWhvby5jb20udHeCFnNtdHAubWFp\r\nbC55YWhvby5jb20udm6CFXNtdHAubWFpbC55YWhvby5jby5pZIIVc210cC5tYWls\r\nLnlhaG9vLmNvLmlughVzbXRwLm1haWwueWFob28uY28ua3KCFXNtdHAubWFpbC55\r\nYWhvby5jby50aIIVc210cC5tYWlsLnlhaG9vLmNvLnVrghJzbXRwLm1haWwueWFo\r\nb28uY2GCEnNtdHAubWFpbC55YWhvby5jboISc210cC5tYWlsLnlhaG9vLmRlghJz\r\nbXRwLm1haWwueWFob28uZXOCEnNtdHAubWFpbC55YWhvby5mcoISc210cC5tYWls\r\nLnlhaG9vLml0gg9zbXRwLnk3bWFpbC5jb22CFHNtdHAuY29ycmVvLnlhaG9vLmVz\r\nMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\r\nYQYDVR0fBFowWDAqoCigJoYkaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL2NhMy1n\r\nMjguY3JsMCqgKKAmhiRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vY2EzLWcyOC5j\r\ncmwwQgYDVR0gBDswOTA3BglghkgBhv1sAQEwKjAoBggrBgEFBQcCARYcaHR0cHM6\r\nLy93d3cuZGlnaWNlcnQuY29tL0NQUzB7BggrBgEFBQcBAQRvMG0wJAYIKwYBBQUH\r\nMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBFBggrBgEFBQcwAoY5aHR0cDov\r\nL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUNBLTMu\r\nY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEFBQADggEBABshjUND4hADHGfe\r\ncV9XGXcr9h3u7mT5kgdcgGFhcrFAlwkzt0NUCbuN0X8DWHE72Tpb3zRE25CmUUJe\r\nktBbq9PQb4b5/Wt1htAEw0qUs3BsUbqejK9OHJ/11Jn2ek4+SLJuYlijzc7KM3F/\r\nyz7ZTZtKR0PglkXfqbpvWYGabYpfL2FRLoJ7alTLsMJcFxbSLYcAIMxufj7RyTBJ\r\nbKgRJl4wmP4+Zc2Q1p59mENY0u5HqVAAOmWc0jNb0/31+tRr5f6EgXxK++7TQOpF\r\n0TOaFsXlzRlpKfmIbzVr2nfwghV5/bRZj96TK3g1OoOz4C8ksK4INHnUdTAqZ18M\r\nvHpnJw4=\r\n-----END CERTIFICATE-----',
pinned: true
}
},
tonline: {
imap: {
host: 'secureimap.t-online.de',
port: 993,
secure: true,
ca: '-----BEGIN CERTIFICATE-----\r\nMIIHnjCCBoagAwIBAgIJAJ9peA0rvlFEMA0GCSqGSIb3DQEBBQUAMIHJMQswCQYD\r\nVQQGEwJERTElMCMGA1UEChMcVC1TeXN0ZW1zIEludGVybmF0aW9uYWwgR21iSDEf\r\nMB0GA1UECxMWVC1TeXN0ZW1zIFRydXN0IENlbnRlcjEMMAoGA1UECBMDTlJXMQ4w\r\nDAYDVQQREwU1NzI1MDEQMA4GA1UEBxMHTmV0cGhlbjEgMB4GA1UECRMXVW50ZXJl\r\nIEluZHVzdHJpZXN0ci4gMjAxIDAeBgNVBAMTF1RlbGVTZWMgU2VydmVyUGFzcyBE\r\nRS0xMB4XDTE0MDQxNzEyMDUzNFoXDTE2MDQyMjIzNTk1OVowga4xCzAJBgNVBAYT\r\nAkRFMRwwGgYDVQQKExNEZXV0c2NoZSBUZWxla29tIEFHMRMwEQYDVQQLDApQJkkg\r\nQU0vRENTMQ8wDQYDVQQIEwZIZXNzZW4xEjAQBgNVBAcTCURhcm1zdGFkdDEmMCQG\r\nCSqGSIb3DQEJARYXY2VydGFkbWluX3BpQHRlbGVrb20uZGUxHzAdBgNVBAMTFnNl\r\nY3VyZWltYXAudC1vbmxpbmUuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\r\nAoIBAQDcJrmKStOpx5r0uzT+FkSY951qgeBi5hwo+XOir1TOTAjFSDvWPcgLwNRV\r\nDWlg/iCy8KkVJY3jUz6a/JOf4ovBOubKOyeH/UH5GYOQPy6ph9xCEF9f2QYT4uYT\r\nu5OYpfSBALNQxa37E3KYHq4n8GNTtQD+XQ7bO34Re5njCB4mAIEpxaEMTzRr79q0\r\n4G5LCzWXdDuobaojm1uFnPmei9tJBEVF3Bb5sGj+3n7GhsaEAHMyIlP4M0oiAEL0\r\n0FjqmQLoZwDv9J2+PoKg4wwuHjeXDF5zdd1btM0DK0F0fyxarxOfBA/e62lUvvrp\r\nUT+Lg4rHSfVWTgcTz6Fn2jHS+TCXAgMBAAGjggOgMIIDnDAfBgNVHSMEGDAWgBRi\r\nTxPONmeEzRn8oE8Zi+8VVAEyHDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI\r\nKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBRCC+Q6QBJOFkh7mq9vPMPuilfi\r\nMTBZBgNVHSAEUjBQMEQGCSsGAQQBvUcNAjA3MDUGCCsGAQUFBwIBFilodHRwOi8v\r\nd3d3LnRlbGVzZWMuZGUvc2VydmVycGFzcy9jcHMuaHRtbDAIBgZngQwBAgIwggEh\r\nBgNVHR8EggEYMIIBFDBFoEOgQYY/aHR0cDovL2NybC5zZXJ2ZXJwYXNzLnRlbGVz\r\nZWMuZGUvcmwvVGVsZVNlY19TZXJ2ZXJQYXNzX0RFLTEuY3JsMIHKoIHHoIHEhoHB\r\nbGRhcDovL2xkYXAuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NuPVRlbGVTZWMlMjBT\r\nZXJ2ZXJQYXNzJTIwREUtMSxvdT1ULVN5c3RlbXMlMjBUcnVzdCUyMENlbnRlcixv\r\nPVQtU3lzdGVtcyUyMEludGVybmF0aW9uYWwlMjBHbWJILGM9ZGU/Y2VydGlmaWNh\r\ndGVSZXZvY2F0aW9ubGlzdD9iYXNlP2NlcnRpZmljYXRlUmV2b2NhdGlvbmxpc3Q9\r\nKjCCATkGCCsGAQUFBwEBBIIBKzCCAScwMwYIKwYBBQUHMAGGJ2h0dHA6Ly9vY3Nw\r\nLnNlcnZlcnBhc3MudGVsZXNlYy5kZS9vY3NwcjBMBggrBgEFBQcwAoZAaHR0cDov\r\nL2NybC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvY3J0L1RlbGVTZWNfU2VydmVyUGFz\r\nc19ERS0xLmNlcjCBoQYIKwYBBQUHMAKGgZRsZGFwOi8vbGRhcC5zZXJ2ZXJwYXNz\r\nLnRlbGVzZWMuZGUvY249VGVsZVNlYyUyMFNlcnZlclBhc3MlMjBERS0xLG91PVQt\r\nU3lzdGVtcyUyMFRydXN0JTIwQ2VudGVyLG89VC1TeXN0ZW1zJTIwSW50ZXJuYXRp\r\nb25hbCUyMEdtYkgsYz1kZT9jQUNlcnRpZmljYXRlMAwGA1UdEwEB/wQCMAAwYAYD\r\nVR0RBFkwV4IWc2VjdXJlaW1hcC50LW9ubGluZS5kZYIUaW1hcG1haWwudC1vbmxp\r\nbmUuZGWCFWltYXAtbWFpbC50LW9ubGluZS5kZYIQaW1hcC50LW9ubGluZS5kZTAN\r\nBgkqhkiG9w0BAQUFAAOCAQEAB7fRFmLSpT5qMcCL4V4gf7/cOrNZK/7F/X9aF6T9\r\n9xFbzWhq7tti0rsrfvOgWk6It3YMEc/04OBoXnh43xg3tBWE16cz2ptr6YxAyoj8\r\nhn9p0x9coW1Cw2qFMPZi1IkwQpOm2pF/bXgqNEerhXKTZYnrko2mVcPnSoKcpRwL\r\n/55qVH88DmYQXnwhVgwzJYK0TP8E1m9MlUvtIPVudSOwkDS0tRlEOwnYXQOP1CCk\r\nYo3nSt3N1yjQEe8scx+4miF4Y7WQ4cKx0w2huwV9snp32h8kdtvvRxHSlMsQKxFu\r\nPQZk2EDE5pHTgzojgQkG7YTep+hh2DQl7piv+jbjM+Sxbw==\r\n-----END CERTIFICATE-----',
pinned: true
},
smtp: {
host: 'securesmtp.t-online.de',
port: 465,
secure: true,
ca: '-----BEGIN CERTIFICATE-----\r\nMIIH+zCCBuOgAwIBAgIJAPW7+G3hGuGWMA0GCSqGSIb3DQEBBQUAMIHJMQswCQYD\r\nVQQGEwJERTElMCMGA1UEChMcVC1TeXN0ZW1zIEludGVybmF0aW9uYWwgR21iSDEf\r\nMB0GA1UECxMWVC1TeXN0ZW1zIFRydXN0IENlbnRlcjEMMAoGA1UECBMDTlJXMQ4w\r\nDAYDVQQREwU1NzI1MDEQMA4GA1UEBxMHTmV0cGhlbjEgMB4GA1UECRMXVW50ZXJl\r\nIEluZHVzdHJpZXN0ci4gMjAxIDAeBgNVBAMTF1RlbGVTZWMgU2VydmVyUGFzcyBE\r\nRS0xMB4XDTE0MDQxNzA5MTk0NloXDTE2MDQyMjIzNTk1OVowga4xCzAJBgNVBAYT\r\nAkRFMRwwGgYDVQQKExNEZXV0c2NoZSBUZWxla29tIEFHMRMwEQYDVQQLDApQJkkg\r\nQU0vRENTMQ8wDQYDVQQIEwZIZXNzZW4xEjAQBgNVBAcTCURhcm1zdGFkdDEmMCQG\r\nCSqGSIb3DQEJARYXY2VydGFkbWluX3BpQHRlbGVrb20uZGUxHzAdBgNVBAMTFnNl\r\nY3VyZXNtdHAudC1vbmxpbmUuZGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\r\nAoIBAQC7LE0RYPww4qKM0rO4BPKU50cr7/VpPXVv5Z+sq1r+qQ4hTjm/zqtWSmJU\r\nP45/Akp8EPBuqf2Kx0KBRhP/SNxDdlvy68JiLID2WNhKA6t1q0njJoxx8FVOiEqU\r\nQYgk1BNk+MbTFT4bJpnoTE7Nz+KW07XMVlGzm2femojPwxw+Hvbfi/6dMMGbCyU6\r\n2rQVihCFdE7jaZdPnsP1Y6+0B42+siP8iIBoJtHac9tT14J6sOZ8f3P4GQuVI/Ky\r\nO7SBgoqDilgQvRFJLsRqyGE1X4DqFV2Po4DbriRaHVOZtM0CfU5cVuesX7fCE35x\r\nyYyYXJiw4AU794rjR/nHy+0xwot5AgMBAAGjggP9MIID+TAfBgNVHSMEGDAWgBRi\r\nTxPONmeEzRn8oE8Zi+8VVAEyHDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI\r\nKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBQEF2eCJX54KC3LnK1hkboZHtgU\r\ndjBZBgNVHSAEUjBQMEQGCSsGAQQBvUcNAjA3MDUGCCsGAQUFBwIBFilodHRwOi8v\r\nd3d3LnRlbGVzZWMuZGUvc2VydmVycGFzcy9jcHMuaHRtbDAIBgZngQwBAgIwggEh\r\nBgNVHR8EggEYMIIBFDBFoEOgQYY/aHR0cDovL2NybC5zZXJ2ZXJwYXNzLnRlbGVz\r\nZWMuZGUvcmwvVGVsZVNlY19TZXJ2ZXJQYXNzX0RFLTEuY3JsMIHKoIHHoIHEhoHB\r\nbGRhcDovL2xkYXAuc2VydmVycGFzcy50ZWxlc2VjLmRlL2NuPVRlbGVTZWMlMjBT\r\nZXJ2ZXJQYXNzJTIwREUtMSxvdT1ULVN5c3RlbXMlMjBUcnVzdCUyMENlbnRlcixv\r\nPVQtU3lzdGVtcyUyMEludGVybmF0aW9uYWwlMjBHbWJILGM9ZGU/Y2VydGlmaWNh\r\ndGVSZXZvY2F0aW9ubGlzdD9iYXNlP2NlcnRpZmljYXRlUmV2b2NhdGlvbmxpc3Q9\r\nKjCCATkGCCsGAQUFBwEBBIIBKzCCAScwMwYIKwYBBQUHMAGGJ2h0dHA6Ly9vY3Nw\r\nLnNlcnZlcnBhc3MudGVsZXNlYy5kZS9vY3NwcjBMBggrBgEFBQcwAoZAaHR0cDov\r\nL2NybC5zZXJ2ZXJwYXNzLnRlbGVzZWMuZGUvY3J0L1RlbGVTZWNfU2VydmVyUGFz\r\nc19ERS0xLmNlcjCBoQYIKwYBBQUHMAKGgZRsZGFwOi8vbGRhcC5zZXJ2ZXJwYXNz\r\nLnRlbGVzZWMuZGUvY249VGVsZVNlYyUyMFNlcnZlclBhc3MlMjBERS0xLG91PVQt\r\nU3lzdGVtcyUyMFRydXN0JTIwQ2VudGVyLG89VC1TeXN0ZW1zJTIwSW50ZXJuYXRp\r\nb25hbCUyMEdtYkgsYz1kZT9jQUNlcnRpZmljYXRlMAwGA1UdEwEB/wQCMAAwgbwG\r\nA1UdEQSBtDCBsYIWc2VjdXJlc210cC50LW9ubGluZS5kZYIUc210cG1haWwudC1v\r\nbmxpbmUuZGWCFXNtdHAtbWFpbC50LW9ubGluZS5kZYIXc2VjdXJlLXNtdHAudC1v\r\nbmxpbmUuZGWCE3Vtc2dhdGUudC1vbmxpbmUuZGWCEWFzbXRwLnQtb25saW5lLmRl\r\nghdzZWN1cmVhc210cC50LW9ubGluZS5kZYIQc210cC50LW9ubGluZS5kZTANBgkq\r\nhkiG9w0BAQUFAAOCAQEAaed1t1h37qvROLO0M4UoOFkUvK3p+YrE89A+p/RCddGP\r\nCL7L1ywQXmo7VLyfdeF0X3yjWDvVq4Mm4QABZfhc5I3I282qICpv8bIGCefrZ5dU\r\n3zAkKVtRjn3BD4dbYCW/sIWnccl7HEiTZvTF1F1T+5Pev1N2YYMVshu7FJ8obXEa\r\n5q2xG/BLFr73AHrqpCq6JNp9nnZCCpu/IuZqfHcrPrQbpjxMk9K05XPBkRRqW8o8\r\n8cdmM+4zQSxCvDv7tYAIGKAX5vM3YTIv0Rah92/BFv8K7uc2w8aFnVDYglJ9j5Yj\r\nZwEQiXacs/agdmKuAKer/YPKM1mf1bZDzd6gWZpFJA==\r\n-----END CERTIFICATE-----',
pinned: true
}
},
checkOutboxInterval: 5000,
iconPath: '/img/icon.png',
verificationUrl: '/verify/',
verificationUuidLength: 36,
dbVersion: 3,
dbVersion: 4,
appVersion: appVersion,
outboxMailboxPath: 'OUTBOX',
outboxMailboxType: 'Outbox'
@ -78,6 +116,12 @@ define(function(require) {
updatePublicKeyMsgRemovedKey: '{0} revoked his key and may no longer be able to read encrypted messages. Remove the key?',
updatePublicKeyPosBtn: 'Yes',
updatePublicKeyNegBtn: 'No',
outdatedCertificateTitle: 'Warning',
outdatedCertificateMessage: 'The SSL certificate for the mail server {0} changed, the connection was refused.',
updateCertificateTitle: 'Warning',
updateCertificateMessage: 'The SSL certificate for the mail server {0} changed. Do you want to proceed?',
updateCertificatePosBtn: 'Yes',
updateCertificateNegBtn: 'No',
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\n',

View File

@ -4,7 +4,8 @@
define(function(require) {
'use strict';
var Auth = require('js/bo/auth'),
var axe = require('axe'),
Auth = require('js/bo/auth'),
PGP = require('js/crypto/pgp'),
PgpMailer = require('pgpmailer'),
OAuth = require('js/util/oauth'),
@ -13,19 +14,18 @@ define(function(require) {
mailreader = require('mailreader'),
ImapClient = require('imap-client'),
Crypto = require('js/crypto/crypto'),
axe = require('axe'),
RestDAO = require('js/dao/rest-dao'),
EmailDAO = require('js/dao/email-dao'),
appConfig = require('js/app-config'),
config = appConfig.config,
str = appConfig.string,
EmailDAO = require('js/dao/email-dao'),
KeychainDAO = require('js/dao/keychain-dao'),
PublicKeyDAO = require('js/dao/publickey-dao'),
LawnchairDAO = require('js/dao/lawnchair-dao'),
PrivateKeyDAO = require('js/dao/privatekey-dao'),
InvitationDAO = require('js/dao/invitation-dao'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
UpdateHandler = require('js/util/update/update-handler');
UpdateHandler = require('js/util/update/update-handler'),
config = appConfig.config,
str = appConfig.string;
var self = {};
@ -33,6 +33,13 @@ define(function(require) {
* Start the application
*/
self.start = function(options, callback) {
if (self.started) {
return callback();
}
self.started = true;
self.onError = options.onError;
// are we running in a cordova app or in a browser environment?
if (window.cordova) {
// wait for 'deviceready' event to make sure plugins are loaded
@ -47,18 +54,18 @@ define(function(require) {
function onDeviceReady() {
axe.debug('Starting app.');
self.buildModules(options);
self.buildModules();
// Handle offline and online gracefully
window.addEventListener('online', self.onConnect.bind(self, options.onError));
window.addEventListener('online', self.onConnect.bind(self, self.onError));
window.addEventListener('offline', self.onDisconnect.bind(self));
self._appConfigStore.init('app-config', callback);
}
};
self.buildModules = function(options) {
var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore;
self.buildModules = function() {
var lawnchairDao, restDao, pubkeyDao, privkeyDao, crypto, emailDao, keychain, pgp, userStorage, pgpbuilder, oauth, appConfigStore, auth;
// start the mailreader's worker thread
mailreader.startWorker(config.workerPath + '/../lib/mailreader-parser-worker.js');
@ -77,7 +84,7 @@ define(function(require) {
var message = params.newKey ? str.updatePublicKeyMsgNewKey : str.updatePublicKeyMsgRemovedKey;
message = message.replace('{0}', params.userId);
options.onError({
self.onError({
title: str.updatePublicKeyTitle,
message: message,
positiveBtnStr: str.updatePublicKeyPosBtn,
@ -88,15 +95,15 @@ define(function(require) {
};
self._appConfigStore = appConfigStore = new DeviceStorageDAO(new LawnchairDAO());
self._auth = new Auth(appConfigStore, oauth, new RestDAO('/ca'));
self._auth = auth = new Auth(appConfigStore, oauth, pgp);
self._userStorage = userStorage = new DeviceStorageDAO(lawnchairDao);
self._invitationDao = new InvitationDAO(restDao);
self._pgpbuilder = pgpbuilder = new PgpBuilder();
self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader);
self._outboxBo = new OutboxBO(emailDao, keychain, userStorage);
self._updateHandler = new UpdateHandler(appConfigStore, userStorage);
self._updateHandler = new UpdateHandler(appConfigStore, userStorage, auth);
emailDao.onError = options.onError;
emailDao.onError = self.onError;
};
self.isOnline = function() {
@ -114,8 +121,7 @@ define(function(require) {
return;
}
// fetch pinned local ssl certificate
self._auth.getCredentials({}, function(err, credentials) {
self._auth.getCredentials(function(err, credentials) {
if (err) {
callback(err);
return;
@ -125,33 +131,19 @@ define(function(require) {
});
function initClients(credentials) {
var auth, imapOptions, imapClient, smtpOptions, pgpMailer;
auth = {
user: credentials.emailAddress,
xoauth2: credentials.oauthToken
};
imapOptions = {
secure: config.gmail.imap.secure,
port: config.gmail.imap.port,
host: config.gmail.imap.host,
auth: auth,
ca: [credentials.sslCert]
};
smtpOptions = {
secureConnection: config.gmail.smtp.secure,
port: config.gmail.smtp.port,
host: config.gmail.smtp.host,
auth: auth,
tls: {
ca: [credentials.sslCert]
}
};
pgpMailer = new PgpMailer(smtpOptions, self._pgpbuilder);
imapClient = new ImapClient(imapOptions);
var pgpMailer = new PgpMailer(credentials.smtp, self._pgpbuilder);
var imapClient = new ImapClient(credentials.imap);
imapClient.onError = onImapError;
// certificate update handling
imapClient.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'imap', self.onConnect, self.onError);
pgpMailer.onCert = self._auth.handleCertificateUpdate.bind(self._auth, 'smtp', self.onConnect, self.onError);
// after-setup configuration depending on the provider:
// gmail does not require you to upload to the sent items folder
// after successful sending, whereas most other providers do
self._emailDao.ignoreUploadOnSent = !!(config[self._auth.provider] && config[self._auth.provider].ignoreUploadOnSent);
// connect to clients
self._emailDao.onConnect({
imapClient: imapClient,
@ -160,17 +152,20 @@ define(function(require) {
}
function onImapError(error) {
axe.debug('IMAP error.' + (error.errMsg || error.message) + (error.stack ? ('\n' + error.stack) : ''));
axe.debug('IMAP reconnecting...');
// re-init client modules on error
self.onConnect(function(err) {
if (err) {
axe.error('IMAP reconnect failed! ' + (err.errMsg || err.message) + (err.stack ? ('\n' + err.stack) : ''));
return;
}
axe.debug('IMAP connection error. Attempting reconnect in ' + config.reconnectInterval + ' ms. Error: ' + (error.errMsg || error.message) + (error.stack ? ('\n' + error.stack) : ''));
axe.debug('IMAP reconnect attempt complete.');
});
setTimeout(function() {
axe.debug('IMAP reconnecting...');
// re-init client modules on error
self.onConnect(function(err) {
if (err) {
axe.error('IMAP reconnect attempt failed! ' + (err.errMsg || err.message) + (err.stack ? ('\n' + err.stack) : ''));
return;
}
axe.debug('IMAP reconnect attempt complete.');
});
}, config.reconnectInterval);
}
};
@ -221,6 +216,7 @@ define(function(require) {
// account information for the email dao
var account = {
realname: options.realname,
emailAddress: options.emailAddress,
asymKeySize: config.asymKeySize
};

View File

@ -16,6 +16,7 @@ requirejs([
'js/controller/login-new-device',
'js/controller/login-existing',
'js/controller/login-privatekey-download',
'js/controller/login-set-credentials',
'js/controller/mail-list',
'js/controller/read',
'js/controller/write',
@ -41,6 +42,7 @@ requirejs([
LoginNewDeviceCtrl,
LoginExistingCtrl,
LoginPrivateKeyDownloadCtrl,
LoginSetCredentialsCtrl,
MailListCtrl,
ReadCtrl,
WriteCtrl,
@ -78,6 +80,10 @@ requirejs([
templateUrl: 'tpl/login.html',
controller: LoginCtrl
});
$routeProvider.when('/login-set-credentials', {
templateUrl: 'tpl/login-set-credentials.html',
controller: LoginSetCredentialsCtrl
});
$routeProvider.when('/login-existing', {
templateUrl: 'tpl/login-existing.html',
controller: LoginExistingCtrl

View File

@ -1,49 +1,215 @@
define(function() {
define(function(require) {
'use strict';
var emailItemKey = 'emailaddress';
var axe = require('axe'),
str = require('js/app-config').string;
var Auth = function(appConfigStore, oauth, ca) {
var EMAIL_ADDR_DB_KEY = 'emailaddress';
var USERNAME_DB_KEY = 'username';
var REALNAME_DB_KEY = 'realname';
var PASSWD_DB_KEY = 'password';
var PROVIDER_DB_KEY = 'provider';
var IMAP_DB_KEY = 'imap';
var SMTP_DB_KEY = 'smtp';
/**
* The Auth BO handles the rough edges and gaps between user/password authentication
* and OAuth via Chrome Identity API.
* Typical usage:
* var auth = new Auth(...);
* auth.setCredentials(...); // during the account setup
* auth.getEmailAddress(...); // called from the login controller to determine if there is already a user present on the device
* auth.getCredentials(...); // called to gather all the information to connect to IMAP/SMTP, e.g. pinned intermediate certificates,
* username, password / oauth token, IMAP/SMTP server host names, ...
*/
var Auth = function(appConfigStore, oauth, pgp) {
this._appConfigStore = appConfigStore;
this._oauth = oauth;
this._ca = ca;
this._pgp = pgp;
};
Auth.prototype.getCredentials = function(options, callback) {
/**
* Retrieves credentials and IMAP/SMTP settings:
* 1) Fetches the credentials from disk, then...
* 2 a) ... in an oauth setting, retrieves a fresh oauth token from the Chrome Identity API.
* 2 b) ... in a user/passwd setting, does not need to do additional work.
* 3) Loads the intermediate certs from the configuration.
*
* @param {Function} callback(err, credentials)
*/
Auth.prototype.getCredentials = function(callback) {
var self = this;
// fetch pinned local ssl certificate
self.getCertificate(function(err, certificate) {
if (err) {
callback(err);
if (!self.provider || !self.emailAddress) {
// we're not yet initialized, so let's load our stuff from disk
self._loadCredentials(function(err) {
if (err) {
return callback(err);
}
chooseLogin();
});
return;
}
chooseLogin();
function chooseLogin() {
if (self.provider === 'gmail' && !self.password) {
// oauth login for gmail
self.getOAuthToken(function(err) {
if (err) {
return callback(err);
}
done();
});
return;
}
// try reading email address from local storage
self.getEmailAddressFromConfig(function(err, emailAddress) {
if (err) {
callback(err);
return;
}
// get a fresh oauth token
self._oauth.getOAuthToken(emailAddress, function(err, token) {
if (self.passwordNeedsDecryption) {
// decrypt password
self._pgp.decrypt(self.password, undefined, function(err, cleartext) {
if (err) {
callback(err);
return;
return callback(err);
}
// get email address for the token
self.queryEmailAddress(token, function(err, emailAddress) {
self.passwordNeedsDecryption = false;
self.password = cleartext;
done();
});
return;
}
done();
}
function done() {
var credentials = {
imap: {
secure: self.imap.secure,
port: self.imap.port,
host: self.imap.host,
ca: self.imap.ca,
pinned: self.imap.pinned,
auth: {
user: self.username,
xoauth2: self.oauthToken, // password or oauthToken is undefined
pass: self.password
}
},
smtp: {
secure: self.smtp.secure,
port: self.smtp.port,
host: self.smtp.host,
ca: self.smtp.ca,
pinned: self.smtp.pinned,
auth: {
user: self.username,
xoauth2: self.oauthToken,
pass: self.password // password or oauthToken is undefined
}
}
};
callback(null, credentials);
}
};
/**
* Set the credentials
*
* @param {String} options.provider The service provider, e.g. 'gmail', 'yahoo', 'tonline'. Matches the entry in the app-config.
* @param {String} options.emailAddress The email address
* @param {String} options.username The user name
* @param {String} options.realname The user's real name
* @param {String} options.password The password, only in user/passwd setting
* @param {String} options.smtp The smtp settings (host, port, secure)
* @param {String} options.imap The imap settings (host, port, secure)
*/
Auth.prototype.setCredentials = function(options) {
this.credentialsDirty = true;
this.provider = options.provider;
this.emailAddress = options.emailAddress;
this.username = options.username;
this.realname = options.realname ? options.realname : '';
this.password = options.password;
this.smtp = options.smtp; // host, port, secure, ca, pinned
this.imap = options.imap; // host, port, secure, ca, pinned
};
Auth.prototype.storeCredentials = function(callback) {
var self = this;
if (!self.credentialsDirty) {
return callback();
}
// persist the provider
self._appConfigStore.storeList([self.smtp], SMTP_DB_KEY, function(err) {
if (err) {
return callback(err);
}
self._appConfigStore.storeList([self.imap], IMAP_DB_KEY, function(err) {
if (err) {
return callback(err);
}
self._appConfigStore.storeList([self.provider], PROVIDER_DB_KEY, function(err) {
if (err) {
return callback(err);
}
self._appConfigStore.storeList([self.emailAddress], EMAIL_ADDR_DB_KEY, function(err) {
if (err) {
callback(err);
return;
return callback(err);
}
callback(null, {
emailAddress: emailAddress,
oauthToken: token,
sslCert: certificate
self._appConfigStore.storeList([self.username], USERNAME_DB_KEY, function(err) {
if (err) {
return callback(err);
}
self._appConfigStore.storeList([self.realname], REALNAME_DB_KEY, function(err) {
if (err) {
return callback(err);
}
if (!self.password) {
self.credentialsDirty = false;
return callback();
}
if (self.passwordNeedsDecryption) {
// password is not decrypted yet, so no need to re-encrypt it before storing...
self._appConfigStore.storeList([self.password], PASSWD_DB_KEY, function(err) {
if (err) {
return callback(err);
}
self.credentialsDirty = false;
callback();
});
return;
}
self._pgp.encrypt(self.password, undefined, function(err, ciphertext) {
if (err) {
return callback(err);
}
self._appConfigStore.storeList([ciphertext], PASSWD_DB_KEY, function(err) {
if (err) {
return callback(err);
}
self.credentialsDirty = false;
callback();
});
});
});
});
});
});
@ -52,96 +218,201 @@ define(function() {
};
/**
* Get the pinned ssl certificate for the corresponding mail server.
*/
Auth.prototype.getCertificate = function(callback) {
this._ca.get({
uri: '/Google_Internet_Authority_G2.pem',
type: 'text'
}, function(err, cert) {
if (err || !cert) {
callback({
errMsg: 'Could not fetch pinned certificate!'
});
return;
}
callback(null, cert);
});
};
/**
* Gracefully try to fetch the user's email address from local storage.
* If not yet stored, handle online/offline cases on first use.
* Returns the email address. Loads it from disk, if necessary
*/
Auth.prototype.getEmailAddress = function(callback) {
// try to fetch email address from local storage
this.getEmailAddressFromConfig(function(err, cachedEmailAddress) {
if (err) {
callback(err);
return;
}
callback(null, cachedEmailAddress);
});
};
/**
* Get the user's email address from local storage
*/
Auth.prototype.getEmailAddressFromConfig = function(callback) {
this._appConfigStore.listItems(emailItemKey, 0, null, function(err, cachedItems) {
if (err) {
callback(err);
return;
}
// no email address is cached yet
if (!cachedItems || cachedItems.length < 1) {
callback();
return;
}
callback(null, cachedItems[0]);
});
};
/**
* Lookup the user's email address. Check local cache if available
* otherwise query google's token info api to learn the user's email address
*/
Auth.prototype.queryEmailAddress = function(token, callback) {
var self = this;
self.getEmailAddressFromConfig(function(err, cachedEmailAddress) {
if (self.emailAddress) {
return callback(null, {
emailAddress: self.emailAddress,
realname: self.realname
});
}
self._loadCredentials(function(err) {
if (err) {
callback(err);
return;
return callback(err);
}
// do roundtrip to google api if no email address is cached yet
if (!cachedEmailAddress) {
queryOAuthApi();
return;
}
callback(null, cachedEmailAddress);
callback(null, {
emailAddress: self.emailAddress,
realname: self.realname
});
});
};
function queryOAuthApi() {
self._oauth.queryEmailAddress(token, function(err, emailAddress) {
/**
* READ FIRST b/c usage of the oauth api is weird.
* the chrome identity api will let you query an oauth token for an email account without knowing
* the corresponding email address. also, android has multiple accounts whereas desktop chrome only
* has one user logged in.
* 1) try to read the email address from the configuration (see above)
* 2) fetch the oauth token. if we already HAVE an email address at this point, we can spare
* popping up the account picker on android! if not, the account picker will pop up. this
* is android only, since the desktop chrome will query the user that is logged into chrome
* 3) fetch the email address for the oauth token from the chrome identity api
*/
Auth.prototype.getOAuthToken = function(callback) {
var self = this;
// get a fresh oauth token
self._oauth.getOAuthToken(self.emailAddress, function(err, oauthToken) {
if (err) {
return callback(err);
}
// shortcut if the email address is already known
if (self.emailAddress) {
self.oauthToken = oauthToken;
return callback();
}
// query the email address
self._oauth.queryEmailAddress(oauthToken, function(err, emailAddress) {
if (err) {
callback(err);
return;
return callback(err);
}
// cache the email address on the device
self._appConfigStore.storeList([emailAddress], emailItemKey, function(err) {
callback(err, emailAddress);
self.oauthToken = oauthToken;
self.emailAddress = emailAddress;
callback();
});
});
};
/**
* Loads email address, password, provider, ... from disk and sets them on `this`
*/
Auth.prototype._loadCredentials = function(callback) {
var self = this;
if (self.initialized) {
callback();
}
loadFromDB(SMTP_DB_KEY, function(err, smtp) {
if (err) {
return callback(err);
}
loadFromDB(IMAP_DB_KEY, function(err, imap) {
if (err) {
return callback(err);
}
loadFromDB(USERNAME_DB_KEY, function(err, username) {
if (err) {
return callback(err);
}
loadFromDB(REALNAME_DB_KEY, function(err, realname) {
if (err) {
return callback(err);
}
loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) {
if (err) {
return callback(err);
}
loadFromDB(PASSWD_DB_KEY, function(err, password) {
if (err) {
return callback(err);
}
loadFromDB(PROVIDER_DB_KEY, function(err, provider) {
if (err) {
return callback(err);
}
self.emailAddress = emailAddress;
self.password = password;
self.passwordNeedsDecryption = !!password;
self.provider = provider;
self.username = username;
self.realname = realname;
self.smtp = smtp;
self.imap = imap;
self.initialized = true;
callback();
});
});
});
});
});
});
});
function loadFromDB(key, callback) {
self._appConfigStore.listItems(key, 0, null, function(err, cachedItems) {
callback(err, (!err && cachedItems && cachedItems[0]));
});
}
};
/**
* Handles certificate updates and errors by notifying the user.
* @param {String} component Either imap or smtp
* @param {Function} callback The error handler
* @param {[type]} pemEncodedCert The PEM encoded SSL certificate
*/
Auth.prototype.handleCertificateUpdate = function(component, onConnect, callback, pemEncodedCert) {
var self = this;
axe.debug('new ssl certificate received: ' + pemEncodedCert);
if (!self[component].ca) {
// no previous ssl cert, trust on first use
self[component].ca = pemEncodedCert;
self.credentialsDirty = true;
self.storeCredentials(callback);
return;
}
if (self[component].ca === pemEncodedCert) {
// ignore multiple successive tls handshakes, e.g. for gmail
return;
}
if (self[component].ca && self[component].pinned) {
// do not update the pinned certificates!
callback({
title: str.outdatedCertificateTitle,
message: str.outdatedCertificateMessage.replace('{0}', self[component].host)
});
return;
}
// previous ssl cert known, does not match: query user and certificate
callback({
title: str.updateCertificateTitle,
message: str.updateCertificateMessage.replace('{0}', self[component].host),
positiveBtnStr: str.updateCertificatePosBtn,
negativeBtnStr: str.updateCertificateNegBtn,
showNegativeBtn: true,
callback: function(granted) {
if (!granted) {
return;
}
self[component].ca = pemEncodedCert;
self.storeCredentials(function(err) {
if (err) {
callback(err);
return;
}
onConnect(callback);
});
}
});
};
return Auth;
});

View File

@ -1,24 +1,48 @@
define(function(require) {
'use strict';
var appController = require('js/app-controller');
var appCtrl = require('js/app-controller');
var AddAccountCtrl = function($scope, $location) {
$scope.connectToGoogle = function() {
appController._auth.getCredentials({}, function(err) {
if (err) {
$scope.onError(err);
return;
}
// test for oauth support
if (appCtrl._auth._oauth.isSupported()) {
// fetches the email address from the chrome identity api
appCtrl._auth.getOAuthToken(function(err) {
if (err) {
return $scope.onError(err);
}
$location.path('/login-set-credentials').search({
provider: 'gmail'
});
$scope.$apply();
});
return;
}
redirect();
// use normal user/password login
$location.path('/login-set-credentials').search({
provider: 'gmail'
});
};
function redirect() {
$location.path('/login');
$scope.$apply();
}
$scope.connectToYahoo = function() {
$location.path('/login-set-credentials').search({
provider: 'yahoo'
});
};
$scope.connectToTonline = function() {
$location.path('/login-set-credentials').search({
provider: 'tonline'
});
};
$scope.connectOther = function() {
$location.path('/login-set-credentials').search({
provider: 'custom'
});
};
};
return AddAccountCtrl;

View File

@ -108,8 +108,14 @@ define(function(require) {
return;
}
$location.path('/desktop');
$scope.$apply();
appController._auth.storeCredentials(function(err) {
if (err) {
return $scope.onError(err);
}
$location.path('/desktop');
$scope.$apply();
});
});
}, 500);
};

View File

@ -86,8 +86,14 @@ define(function(require) {
return;
}
$location.path('/desktop');
$scope.$apply();
appController._auth.storeCredentials(function(err) {
if (err) {
return $scope.onError(err);
}
$location.path('/desktop');
$scope.$apply();
});
}
};

View File

@ -73,7 +73,13 @@ define(function(require) {
}
// passphrase is corrent ... go to main app
$scope.goTo('/desktop');
appController._auth.storeCredentials(function(err) {
if (err) {
return $scope.onError(err);
}
$scope.goTo('/desktop');
});
});
});
};

View File

@ -0,0 +1,157 @@
define(function(require) {
'use strict';
var appCtrl = require('js/app-controller'),
config = require('js/app-config').config,
ImapClient = require('imap-client'),
SmtpClient = require('smtpclient');
var SetCredentialsCtrl = function($scope, $location) {
var auth = appCtrl._auth;
var provider;
provider = $location.search().provider;
$scope.hasProviderPreset = !!config[provider];
$scope.useOAuth = !!auth.oauthToken;
$scope.showDetails = (provider === 'custom');
if ($scope.useOAuth) {
$scope.emailAddress = auth.emailAddress;
}
if ($scope.hasProviderPreset) {
// use non-editable smtp and imap presets for provider
$scope.smtpHost = config[provider].smtp.host;
$scope.smtpPort = config[provider].smtp.port;
$scope.smtpSecure = config[provider].smtp.secure;
$scope.smtpCert = config[provider].smtp.ca;
$scope.smtpPinned = config[provider].smtp.pinned;
$scope.imapHost = config[provider].imap.host;
$scope.imapPort = config[provider].imap.port;
$scope.imapSecure = config[provider].imap.secure;
$scope.imapCert = config[provider].imap.ca;
$scope.imapPinned = config[provider].imap.pinned;
}
$scope.test = function(imapClient, smtpClient) {
$scope.credentialsIncomplete = false;
$scope.connectionError = false;
$scope.smtpOk = undefined;
$scope.imapOk = undefined;
if (!(($scope.username || $scope.emailAddress) && ($scope.password || $scope.useOAuth))) {
$scope.credentialsIncomplete = true;
return;
}
var imap = imapClient || new ImapClient({
host: $scope.imapHost.toLowerCase(),
port: $scope.imapPort,
secure: $scope.imapSecure,
ca: $scope.imapCert,
auth: {
user: $scope.username || $scope.emailAddress,
pass: $scope.password,
xoauth2: auth.oauthToken
}
});
imap.onCert = function(pemEncodedCert) {
if (!$scope.imapPinned) {
$scope.imapCert = pemEncodedCert;
}
};
imap.onError = function(err) {
$scope.imapOk = !err;
$scope.connectionError = err;
done();
};
var smtp = smtpClient || new SmtpClient($scope.smtpHost.toLowerCase(), $scope.smtpPort, {
useSecureTransport: $scope.smtpSecure,
ca: $scope.smtpCert,
auth: {
user: $scope.username || $scope.emailAddress,
pass: $scope.password,
xoauth2: auth.oauthToken
}
});
smtp.oncert = function(pemEncodedCert) {
if (!$scope.smtpPinned) {
$scope.smtpCert = pemEncodedCert;
}
};
smtp.onerror = function(err) {
$scope.smtpOk = !err;
$scope.connectionError = $scope.connectionError || err;
done();
};
smtp.onidle = function() {
smtp.onerror = function() {}; // don't care about errors after discarding connection
$scope.smtpOk = true;
smtp.quit();
done();
};
$scope.busy = 2;
// fire away
imap.login(function(err) {
$scope.connectionError = $scope.connectionError || err;
$scope.imapOk = !err;
imap.logout(function() {}); // don't care about errors after discarding connection
done();
});
smtp.connect();
};
function done() {
if ($scope.busy > 0) {
$scope.busy--;
}
if ($scope.smtpOk && $scope.imapOk) {
login();
}
$scope.$apply();
}
function login() {
auth.setCredentials({
provider: provider,
emailAddress: $scope.emailAddress,
username: $scope.username || $scope.emailAddress,
realname: $scope.realname,
password: $scope.password,
imap: {
host: $scope.imapHost.toLowerCase(),
port: $scope.imapPort,
secure: $scope.imapSecure,
ca: $scope.imapCert,
pinned: !!$scope.imapPinned
},
smtp: {
host: $scope.smtpHost.toLowerCase(),
port: $scope.smtpPort,
secure: $scope.smtpSecure,
ca: $scope.smtpCert,
pinned: !!$scope.smtpPinned
}
});
redirect();
}
function redirect() {
$location.path('/login');
}
};
return SetCredentialsCtrl;
});

View File

@ -21,21 +21,22 @@ define(function(require) {
function initializeUser() {
// get OAuth token from chrome
appController._auth.getEmailAddress(function(err, emailAddress) {
appController._auth.getEmailAddress(function(err, info) {
if (err) {
$scope.onError(err);
return;
}
// check if account needs to be selected
if (!emailAddress) {
if (!info.emailAddress) {
goTo('/add-account');
return;
}
// initiate controller by creating email dao
appController.init({
emailAddress: emailAddress
emailAddress: info.emailAddress,
realname: info.realname
}, function(err, availableKeys) {
if (err) {
$scope.onError(err);

View File

@ -197,6 +197,11 @@ define(function(require) {
* display more items (for infinite scrolling)
*/
$scope.displayMore = function() {
if (!currentFolder() || !$scope.displayMessages) {
// folders not yet initialized
return;
}
var len = currentFolder().messages.length,
dLen = $scope.displayMessages.length;

View File

@ -8,7 +8,7 @@ define(function(require) {
aes = require('js/crypto/aes-gcm'),
util = require('js/crypto/util'),
str = require('js/app-config').string,
pgp, emailDao, outbox, keychainDao;
pgp, emailDao, outbox, keychainDao, auth;
//
// Controller
@ -16,7 +16,8 @@ define(function(require) {
var WriteCtrl = function($scope, $filter) {
pgp = appController._pgp;
emailDao = appController._emailDao,
auth = appController._auth;
emailDao = appController._emailDao;
outbox = appController._outboxBo;
keychainDao = appController._keychain;
@ -112,6 +113,7 @@ define(function(require) {
$scope.writerTitle = str.bugReportTitle;
$scope.subject = str.bugReportSubject;
$scope.body = str.bugReportBody + dump;
}
function fillFields(re, replyAll, forward) {
@ -131,13 +133,18 @@ define(function(require) {
address: replyTo
});
$scope.to.forEach($scope.verify);
if ((re.references || []).indexOf(re.id) < 0) {
// references might not exist yet, so use the double concat
$scope.references = [].concat(re.references || []).concat(re.id);
} else {
$scope.references = re.references;
if (re.references) {
}
$scope.references = (re.references || []);
if (re.id && $scope.references.indexOf(re.id) < 0) {
// references might not exist yet, so use the double concat
$scope.references = $scope.references.concat(re.id);
}
if (re.id) {
$scope.inReplyTo = re.id;
}
$scope.inReplyTo = re.id;
}
if (replyAll) {
re.to.concat(re.cc).forEach(function(recipient) {
@ -164,7 +171,9 @@ define(function(require) {
// create a new array, otherwise removing an attachment will also
// remove it from the original in the mail list as a side effect
$scope.attachments = [].concat(re.attachments);
$scope.references = [re.id];
if (re.id) {
$scope.references = [re.id];
}
}
// fill subject
@ -201,8 +210,7 @@ define(function(require) {
body = '\n\n' + sentDate + ' ' + from + ' wrote:\n> ';
}
// only display non html mails in reply part
if (!re.html) {
if (re.body) {
body += re.body.trim().split('\n').join('\n> ').replace(/ >/g, '>');
$scope.body = body;
}
@ -357,6 +365,7 @@ define(function(require) {
// build email model for smtp-client
email = {
from: [{
name: emailDao._account.realname,
address: emailDao._account.emailAddress
}],
to: $scope.to.filter(filterEmptyAddresses),
@ -653,4 +662,4 @@ define(function(require) {
});
return WriteCtrl;
});
});

View File

@ -982,9 +982,26 @@ define(function(require) {
smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage
mail: options.email,
publicKeysArmored: options.email.publicKeysArmored
}, function(err) {
self.done();
callback(err);
}, function(err, rfcText) {
if (err) {
return callback(err);
}
var sentFolder = _.findWhere(self._account.folders, {
type: 'Sent'
});
if (self.ignoreUploadOnSent || !sentFolder || !rfcText) {
return callback();
}
self._imapClient.uploadMessage({
path: sentFolder.path,
message: rfcText
}, function(err) {
self.done();
callback(err);
});
});
};
@ -1009,9 +1026,26 @@ define(function(require) {
self._pgpMailer.send({
smtpclient: options.smtpclient, // filled solely in the integration test, undefined in normal usage
mail: options.email
}, function(err) {
self.done();
callback(err);
}, function(err, rfcText) {
if (err) {
return callback(err);
}
var sentFolder = _.findWhere(self._account.folders, {
type: 'Sent'
});
if (self.ignoreUploadOnSent || !sentFolder || !rfcText) {
return callback();
}
self._imapClient.uploadMessage({
path: sentFolder.path,
message: rfcText
}, function(err) {
self.done();
callback(err);
});
});
};
@ -1115,8 +1149,7 @@ define(function(require) {
});
if (!inbox) {
callback();
return;
return callback();
}
self._imapClient.listenForChanges({
@ -1268,15 +1301,23 @@ define(function(require) {
}
// this array is dropped directly into the ui to create the folder list
var folders = [
wellKnownFolders.inbox,
wellKnownFolders.sent, {
type: 'Outbox',
path: config.outboxMailboxPath
},
wellKnownFolders.drafts,
wellKnownFolders.trash
];
var folders = [];
if (wellKnownFolders.inbox) {
folders.push(wellKnownFolders.inbox);
}
if (wellKnownFolders.sent) {
folders.push(wellKnownFolders.sent);
}
folders.push({
type: 'Outbox',
path: config.outboxMailboxPath
});
if (wellKnownFolders.drafts) {
folders.push(wellKnownFolders.drafts);
}
if (wellKnownFolders.trash) {
folders.push(wellKnownFolders.trash);
}
var foldersChanged = false; // indicates if are there any new/removed folders?

View File

@ -4,15 +4,17 @@ define(function(require) {
var cfg = require('js/app-config').config,
updateV1 = require('js/util/update/update-v1'),
updateV2 = require('js/util/update/update-v2'),
updateV3 = require('js/util/update/update-v3');
updateV3 = require('js/util/update/update-v3'),
updateV4 = require('js/util/update/update-v4');
/**
* Handles database migration
*/
var UpdateHandler = function(appConfigStorage, userStorage) {
var UpdateHandler = function(appConfigStorage, userStorage, auth) {
this._appConfigStorage = appConfigStorage;
this._userStorage = userStorage;
this._updateScripts = [updateV1, updateV2, updateV3];
this._updateScripts = [updateV1, updateV2, updateV3, updateV4];
this._auth = auth;
};
/**
@ -48,7 +50,7 @@ define(function(require) {
*/
UpdateHandler.prototype._applyUpdate = function(options, callback) {
var self = this,
storage,
scriptOptions,
queue = [];
if (options.currentVersion >= options.targetVersion) {
@ -57,9 +59,10 @@ define(function(require) {
return;
}
storage = {
scriptOptions = {
appConfigStorage: self._appConfigStorage,
userStorage: self._userStorage
userStorage: self._userStorage,
auth: self._auth
};
// add all the necessary database updates to the queue
@ -82,7 +85,7 @@ define(function(require) {
// process next update
var script = queue.shift();
script(storage, executeNextUpdate);
script(scriptOptions, executeNextUpdate);
}
executeNextUpdate();

View File

@ -7,7 +7,7 @@ define(function() {
* In database version 3, we introduced new flags to the messages, also
* the outbox uses artificial uids
*/
function updateV2(options, callback) {
function update(options, callback) {
var emailDbType = 'email_',
versionDbType = 'dbVersion',
postUpdateDbVersion = 3;
@ -24,5 +24,5 @@ define(function() {
});
}
return updateV2;
return update;
});

View File

@ -0,0 +1,105 @@
define(function(require) {
'use strict';
var config = require('js/app-config').config;
/**
* Update handler for transition database version 3 -> 4
*
* In database version 4, we need to add a "provider" flag to the
* indexeddb. only gmail was allowed as a mail service provider before,
* so let's add this...
*/
function update(options, callback) {
var VERSION_DB_TYPE = 'dbVersion',
EMAIL_ADDR_DB_KEY = 'emailaddress',
USERNAME_DB_KEY = 'username',
PROVIDER_DB_KEY = 'provider',
IMAP_DB_KEY = 'imap',
SMTP_DB_KEY = 'smtp',
REALNAME_DB_KEY = 'realname',
POST_UPDATE_DB_VERSION = 4;
var imap = config.gmail.imap,
smtp = config.gmail.smtp;
// load the email address (if existing)
loadFromDB(EMAIL_ADDR_DB_KEY, function(err, emailAddress) {
if (err) {
return callback(err);
}
// load the provider (if existing)
loadFromDB(PROVIDER_DB_KEY, function(err, provider) {
if (err) {
return callback(err);
}
// if there is an email address without a provider, we need to add the missing provider entry
// for any other situation, we're good.
if (!(emailAddress && !provider)) {
// update the database version to POST_UPDATE_DB_VERSION
return options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback);
}
// add the missing provider key
options.appConfigStorage.storeList(['gmail'], PROVIDER_DB_KEY, function(err) {
if (err) {
return callback(err);
}
// add the missing user name key
options.appConfigStorage.storeList([emailAddress], USERNAME_DB_KEY, function(err) {
if (err) {
return callback(err);
}
// add the missing imap host info key
options.appConfigStorage.storeList([imap], IMAP_DB_KEY, function(err) {
if (err) {
return callback(err);
}
// add the missing empty real name
options.appConfigStorage.storeList([''], REALNAME_DB_KEY, function(err) {
if (err) {
return callback(err);
}
// add the missing smtp host info key
options.appConfigStorage.storeList([smtp], SMTP_DB_KEY, function(err) {
if (err) {
return callback(err);
}
// reload the credentials
options.auth.initialized = false;
options.auth._loadCredentials(function(err) {
if (err) {
return callback(err);
}
// update the database version to POST_UPDATE_DB_VERSION
options.appConfigStorage.storeList([POST_UPDATE_DB_VERSION], VERSION_DB_TYPE, callback);
});
});
});
});
});
});
});
});
function loadFromDB(key, callback) {
options.appConfigStorage.listItems(key, 0, null, function(err, cachedItems) {
callback(err, (!err && cachedItems && cachedItems[0]));
});
}
}
return update;
});

File diff suppressed because one or more lines are too long

View File

@ -17,8 +17,7 @@
"https://www.googleapis.com/",
"identity", {
"socket": [
"tcp-connect:imap.gmail.com:993",
"tcp-connect:smtp.gmail.com:465"
"tcp-connect:*"
]
}
],

View File

@ -12,6 +12,8 @@
-webkit-appearance: none;
}
:invalid,
.ng-invalid,
.input-text-error {
border-color: red;
}

View File

@ -29,10 +29,25 @@
position: relative;
height: 68px;
cursor: pointer;
&:hover,
&:focus {
opacity: 0.6;
}
&:active {
opacity: 1;
top: 1px;
left: 1px;
}
&:nth-child(odd) {
background-color: white;
}
&:nth-child(even) {
background-color: $color-grey-lightest;
}
&.google {
background-color: white;
div {
width: 164px;
height: 58px;
@ -47,8 +62,6 @@
}
&.whiteout {
background-color: $color-grey-lightest;
div {
width: 210px;
margin: 0 auto;
@ -60,23 +73,7 @@
}
}
&.outlook {
background-color: white;
div {
width: 256px;
margin: 0 auto;
padding: 13px 0;
img {
width: 100%;
}
}
}
&.yahoo {
background-color: $color-grey-lightest;
div {
width: 181px;
margin: 0 auto;
@ -88,8 +85,6 @@
}
&.tonline {
background-color: white;
div {
width: 271px;
margin: 0 auto;
@ -108,23 +103,6 @@
text-align: center;
}
}
&.disabled {
opacity: 0.2;
}
&.enabled {
&:hover,
&:focus {
opacity: 0.6;
}
&:active {
opacity: 1;
top: 1px;
left: 1px;
}
}
}
}
}

View File

@ -1,10 +1,11 @@
.view-login {
position: relative;
min-height: 100%;
height: 100%;
background-color: $color-grey-lightest;
color: $color-grey-dark;
text-align: center;
padding: 20px;
overflow-y: auto;
.logo {
max-width: 700px;
@ -38,7 +39,7 @@
color: $color-blue;
}
p {
p, label {
line-height: 150%;
}
@ -49,7 +50,10 @@
input {
margin-right: 10px;
}
label,
input[type="text"],
input[type="email"],
input[type="number"],
input[type="password"] {
margin-bottom: 10px;
width: 100%;
@ -130,4 +134,39 @@
width: auto;
}
}
}
.view-login-set-credentials {
.content {
max-width: 450px;
b, a {
text-decoration: none;
}
@include respond-to(desktop) {
input[type="text"],
input[type="password"],
input[type="file"] {
width: 100%;
}
}
.details {
div {
margin: 0;
}
label,
input[type="text"],
input[type="number"] {
width: auto;
display: inline-block;
&.username {
width: 100%;
}
}
}
}
}

View File

@ -6,19 +6,16 @@
<li class="google enabled" popover="#google-info" wo-touch="connectToGoogle()">
<div><img src="img/google_logo.png" alt="Google Mail"></div>
</li>
<li class="whiteout disabled" popover="#whiteout-info">
<li class="whiteout disabled" popover="#whiteout-info" wo-touch="onError({message:'Whiteout Mailbox coming soon!', sync:true})">
<div><img src="img/whiteout_logo.svg" alt="Whiteout Mailbox"></div>
</li>
<li class="outlook disabled" popover="#outlook-info">
<div><img src="img/outlook_logo.jpg" alt="Outlook.com"></div>
</li>
<li class="yahoo disabled" popover="#yahoo-info">
<div><img src="img/yahoo_logo.png" alt="Yahoo! Mail"></div>
</li>
<li class="tonline disabled" popover="#tonline-info">
<li class="tonline" popover="#tonline-info" wo-touch="connectToTonline()">
<div><img src="img/tonline_logo.jpg" alt="T-Online"></div>
</li>
<li class="other disabled" popover="#custom-info">
<li class="yahoo" popover="#yahoo-info" wo-touch="connectToYahoo()">
<div><img src="img/yahoo_logo.png" alt="Yahoo! Mail"></div>
</li>
<li class="other" popover="#custom-info" wo-touch="connectOther()">
<h3>Custom server...</h3>
</li>
</ul>
@ -32,7 +29,7 @@
<div class="popover-content">
<p>Connect Whiteout Mail to your Gmail or Google Apps account.</p>
<p>Encrypted and cleartext messages are stored on Google's servers.</p>
<p>No data is sent to our servers.</p>
<p>Your emails and password remain on your device and are never sent to our servers.</p>
</div>
</div><!--/.popover-->
@ -40,48 +37,38 @@
<div class="arrow"></div>
<div class="popover-title"><b>Whiteout Mailbox (coming soon)</b></div>
<div class="popover-content">
<p>Connect Whiteout Mail to your secure Whiteout Mailbox.</p>
<p>All incoming messages are encrypted at rest and can only be read on your device.</p>
<p>Whiteout Mailbox is an email service hosted in Europe.</p>
</div>
</div><!--/.popover-->
<div id="outlook-info" class="popover right desktop-only" ng-controller="PopoverCtrl">
<div class="arrow"></div>
<div class="popover-title"><b>Outlook.com Account (coming soon)</b></div>
<div class="popover-content">
<p>Connect Whiteout Mail to your Outlook.com account.</p>
<p>Encrypted and cleartext messages are stored on Microsoft's servers.</p>
<p>No data is sent to our servers.</p>
<p>Connect Whiteout Mail to your fully encrypted Whiteout Mailbox (hosted in Europe).</p>
<p>Incoming cleartext messages are encrypted with your public PGP key before being stored in your inbox.</p>
<p>This way your email is protected against hackers and can only be read on your device.</p>
</div>
</div><!--/.popover-->
<div id="yahoo-info" class="popover right desktop-only" ng-controller="PopoverCtrl">
<div class="arrow"></div>
<div class="popover-title"><b>Yahoo Mail (coming soon)</b></div>
<div class="popover-title"><b>Yahoo Mail</b></div>
<div class="popover-content">
<p>Connect Whiteout Mail to your Yahoo Mail account.</p>
<p>Encrypted and cleartext messages are stored on Yahoo's servers.</p>
<p>No data is sent to our servers.</p>
<p>Your emails and password remain on your device and are never sent to our servers.</p>
</div>
</div><!--/.popover-->
<div id="tonline-info" class="popover right desktop-only" ng-controller="PopoverCtrl">
<div class="arrow"></div>
<div class="popover-title"><b>T-Online Account (coming soon)</b></div>
<div class="popover-title"><b>T-Online Account</b></div>
<div class="popover-content">
<p>Connect Whiteout Mail to your T-Online account.</p>
<p>Encrypted and cleartext messages are stored on T-Online's servers.</p>
<p>No data is sent to our servers.</p>
<p>Your emails and password remain on your device and are never sent to our servers.</p>
</div>
</div><!--/.popover-->
<div id="custom-info" class="popover right desktop-only" ng-controller="PopoverCtrl">
<div class="arrow"></div>
<div class="popover-title"><b>Custom server (coming soon)</b></div>
<div class="popover-title"><b>Custom server</b></div>
<div class="popover-content">
<p>Connect Whiteout Mail to your own email server.</p>
<p>Encrypted and cleartext messages are stored on your server.</p>
<p>No data is sent to our servers.</p>
<p>Your emails and password remain on your device and are never sent to our servers.</p>
</div>
</div><!--/.popover-->

View File

@ -0,0 +1,49 @@
<div class="view-login view-login-set-credentials">
<div class="logo">
<img src="img/whiteout_logo.svg" alt="whiteout.io">
</div><!--/logo-->
<div class="content">
<p><b>Login.</b> Please enter your email address and password. The password is used to authenticate directly with your mail provider and is not sent to our servers.</p>
<form name="form">
<div>
<label class="input-error-message" ng-show="connectionError">Connection failed. Please check your credentials!</label>
<label class="input-error-message" ng-show="form.$invalid || credentialsIncomplete">Please fill out all required fields!</label>
<br>
<input class="input-text" type="email" required ng-model="emailAddress" placeholder="Email address" focus-me="true" tabindex="1" spellcheck="false"></input>
<input class="input-text" type="text" ng-model="realname" placeholder="Full name (optional)" tabindex="2"></input>
<input ng-hide="useOAuth" ng-required="!useOAuth" class="input-text" ng-class="{'input-text-error': connectionError}" type="password" ng-model="password" placeholder="Password" tabindex="3">
</div><!--/credentials-->
<a href="#" wo-touch='showDetails = !showDetails; $event.preventDefault()'>{{showDetails ? "Hide Options" : "Show Options"}}</a>
<div class='details' ng-show='showDetails'>
<input ng-hide="useOAuth" class="input-text username" type="text" ng-model="username" placeholder="User (optional)"></input>
<div class="settings">
<input required ng-disabled="hasProviderPreset" class="input-text" type="text" ng-model="imapHost" placeholder="IMAP server" pattern="^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$"></input>
<input required ng-disabled="hasProviderPreset" class="input-text" type="number" ng-model="imapPort" placeholder="Port" min="0" max="65535" step="1" pattern="\d+"></input>
<label for="imapSecure"><input ng-disabled="hasProviderPreset" class="input-checkbox" type="checkbox" ng-model="imapSecure" name="imapSecure" checked>TLS</label>
</div><!--/.settings-->
<div class="settings">
<input required ng-disabled="hasProviderPreset" class="input-text" type="text" ng-model="smtpHost" placeholder="SMTP server" pattern="^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$"></input>
<input required ng-disabled="hasProviderPreset" class="input-text" type="number" ng-model="smtpPort" placeholder="Port" min="0" max="65535" step="1" pattern="\d+"></input>
<label for="smtpSecure"><input ng-disabled="hasProviderPreset" class="input-checkbox" type="checkbox" ng-model="smtpSecure" name="smtpSecure" checked>TLS</label>
</div><!--/.settings-->
</div><!--/.details-->
<div ng-show="busy">
<span class="spinner"></span>
</div>
<div>
<button type="submit" ng-disabled="form.$invalid" ng-click="test()" class="btn">Login</button>
</div>
</form>
</div><!--/.content-->
</div>

View File

@ -12,39 +12,19 @@ define(function(require) {
var scope, location, ctrl, authStub;
describe('connectToGoogle', function() {
var origAuth;
beforeEach(function() {
// remember original module to restore later, then replace it
origAuth = appController._auth;
appController._auth = authStub = sinon.createStubInstance(Auth);
});
afterEach(function() {
// restore the app controller module
location && location.path && location.path.restore && location.path.restore();
appController._auth = origAuth;
});
it('should fail on fetchOAuthToken error', function(done) {
angular.module('addaccounttest', []);
mocks.module('addaccounttest');
mocks.inject(function($controller, $rootScope) {
scope = $rootScope.$new();
scope.state = {};
ctrl = $controller(AddAccountCtrl, {
$location: location,
$scope: scope
});
});
scope.onError = function(err) {
expect(err).to.equal(42);
expect(authStub.getCredentials.calledOnce).to.be.true;
done();
};
authStub.getCredentials.yields(42);
scope.connectToGoogle();
});
it('should forward to login', function(done) {
it('should forward to login', function() {
angular.module('addaccounttest', []);
mocks.module('addaccounttest');
mocks.inject(function($controller, $rootScope, $location) {
@ -52,15 +32,8 @@ define(function(require) {
scope = $rootScope.$new();
scope.state = {};
sinon.stub(location, 'path', function(path) {
expect(path).to.equal('/login');
expect(authStub.getCredentials.calledOnce).to.be.true;
location.path.restore();
scope.$apply.restore();
done();
});
sinon.stub(location, 'path').returns(location);
sinon.stub(location, 'search').returns(location);
sinon.stub(scope, '$apply', function() {});
ctrl = $controller(AddAccountCtrl, {
@ -69,10 +42,200 @@ define(function(require) {
});
});
authStub.getCredentials.yields();
authStub._oauth = {
isSupported: function() {
return true;
}
};
authStub.getOAuthToken.yields();
scope.connectToGoogle();
expect(location.path.calledWith('/login-set-credentials')).to.be.true;
expect(location.search.calledWith({
provider: 'gmail'
})).to.be.true;
expect(authStub.getOAuthToken.calledOnce).to.be.true;
location.path.restore();
location.search.restore();
scope.$apply.restore();
});
it('should not use oauth for gmail', function() {
angular.module('addaccounttest', []);
mocks.module('addaccounttest');
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
scope = $rootScope.$new();
scope.state = {};
sinon.stub(location, 'path').returns(location);
sinon.stub(location, 'search').returns(location);
sinon.stub(scope, '$apply', function() {});
ctrl = $controller(AddAccountCtrl, {
$location: location,
$scope: scope
});
});
authStub._oauth = {
isSupported: function() {
return false;
}
};
scope.connectToGoogle();
expect(location.path.calledWith('/login-set-credentials')).to.be.true;
expect(location.search.calledWith({
provider: 'gmail'
})).to.be.true;
expect(authStub.getOAuthToken.called).to.be.false;
location.path.restore();
location.search.restore();
scope.$apply.restore();
});
it('should not forward to login when oauth fails', function(done) {
angular.module('addaccounttest', []);
mocks.module('addaccounttest');
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
scope = $rootScope.$new();
scope.state = {};
sinon.stub(location, 'path').returns(location);
sinon.stub(location, 'search').returns(location);
sinon.stub(scope, '$apply', function() {});
ctrl = $controller(AddAccountCtrl, {
$location: location,
$scope: scope
});
});
authStub._oauth = {
isSupported: function() {
return true;
}
};
authStub.getOAuthToken.yields(new Error());
scope.onError = function(err) {
expect(err).to.exist;
expect(location.path.called).to.be.false;
expect(location.search.called).to.be.false;
location.path.restore();
location.search.restore();
scope.$apply.restore();
done();
};
scope.connectToGoogle();
});
});
describe('connectToYahoo', function() {
it('should forward to login', function() {
angular.module('addaccounttest', []);
mocks.module('addaccounttest');
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
scope = $rootScope.$new();
scope.state = {};
sinon.stub(location, 'path').returns(location);
sinon.stub(location, 'search').returns(location);
sinon.stub(scope, '$apply', function() {});
ctrl = $controller(AddAccountCtrl, {
$location: location,
$scope: scope
});
});
scope.connectToYahoo();
expect(location.path.calledWith('/login-set-credentials')).to.be.true;
expect(location.search.calledWith({
provider: 'yahoo'
})).to.be.true;
location.path.restore();
location.search.restore();
scope.$apply.restore();
});
});
describe('connectToTonline', function() {
it('should forward to login', function() {
angular.module('addaccounttest', []);
mocks.module('addaccounttest');
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
scope = $rootScope.$new();
scope.state = {};
sinon.stub(location, 'path').returns(location);
sinon.stub(location, 'search').returns(location);
sinon.stub(scope, '$apply', function() {});
ctrl = $controller(AddAccountCtrl, {
$location: location,
$scope: scope
});
});
scope.connectToTonline();
expect(location.path.calledWith('/login-set-credentials')).to.be.true;
expect(location.search.calledWith({
provider: 'tonline'
})).to.be.true;
location.path.restore();
location.search.restore();
scope.$apply.restore();
});
});
describe('connectOther', function() {
it('should forward to login', function() {
angular.module('addaccounttest', []);
mocks.module('addaccounttest');
mocks.inject(function($controller, $rootScope, $location) {
location = $location;
scope = $rootScope.$new();
scope.state = {};
sinon.stub(location, 'path').returns(location);
sinon.stub(location, 'search').returns(location);
sinon.stub(scope, '$apply', function() {});
ctrl = $controller(AddAccountCtrl, {
$location: location,
$scope: scope
});
});
scope.connectOther();
expect(location.path.calledWith('/login-set-credentials')).to.be.true;
expect(location.search.calledWith({
provider: 'custom'
})).to.be.true;
location.path.restore();
location.search.restore();
scope.$apply.restore();
});
});
});
});

View File

@ -89,7 +89,7 @@ define(function(require) {
it('should fail due to error in auth.getCredentials', function(done) {
isOnlineStub.returns(true);
authStub.getCredentials.withArgs({}).yields(new Error());
authStub.getCredentials.yields(new Error());
controller.onConnect(function(err) {
expect(err).to.exist;
@ -100,10 +100,12 @@ define(function(require) {
it('should work', function(done) {
isOnlineStub.returns(true);
authStub.getCredentials.withArgs({}).yields(null, {
authStub.getCredentials.yields(null, {
emailAddress: 'asdf@example.com',
oauthToken: 'token',
sslCert: 'cert'
sslCert: 'cert',
imap: {},
smtp: {}
});
emailDaoStub.onConnect.yields();

View File

@ -3,226 +3,332 @@ define(function(require) {
var Auth = require('js/bo/auth'),
OAuth = require('js/util/oauth'),
RestDAO = require('js/dao/rest-dao'),
PGP = require('js/crypto/pgp'),
DeviceStorageDAO = require('js/dao/devicestorage-dao'),
expect = chai.expect;
describe('Auth unit tests', function() {
var auth, appConfigStoreStub, oauthStub, caStub;
// Constancts
var EMAIL_ADDR_DB_KEY = 'emailaddress';
var USERNAME_DB_KEY = 'username';
var REALNAME_DB_KEY = 'realname';
var PASSWD_DB_KEY = 'password';
var PROVIDER_DB_KEY = 'provider';
var IMAP_DB_KEY = 'imap';
var SMTP_DB_KEY = 'smtp';
// SUT
var auth;
// Dependencies
var storageStub, oauthStub, pgpStub;
// test data
var emailAddress = 'bla@blubb.com';
var password = 'passwordpasswordpassword';
var encryptedPassword = 'pgppasswordpgppassword';
var oauthToken = 'tokentokentokentoken';
var provider = 'gmail';
var realname = 'Bla Blubb';
var username = 'bla';
var imap = {
host: 'mail.blablubb.com',
port: 123,
secure: true,
ca: 'PEMPEMPEMPEMPEMPEMPEMPEMPEMPEM'
};
var smtp = {
host: 'mail.blablubb.com',
port: 456,
secure: true,
ca: 'PEMPEMPEMPEMPEMPEMPEMPEMPEMPEM'
};
beforeEach(function() {
appConfigStoreStub = sinon.createStubInstance(DeviceStorageDAO);
storageStub = sinon.createStubInstance(DeviceStorageDAO);
oauthStub = sinon.createStubInstance(OAuth);
caStub = sinon.createStubInstance(RestDAO);
auth = new Auth(appConfigStoreStub, oauthStub, caStub);
pgpStub = sinon.createStubInstance(PGP);
auth = new Auth(storageStub, oauthStub, pgpStub);
});
afterEach(function() {});
describe('#getCredentials', function() {
it('should load credentials and retrieve credentials from cfg', function(done) {
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).yieldsAsync(null, [emailAddress]);
storageStub.listItems.withArgs(PASSWD_DB_KEY, 0, null).yieldsAsync(null, [encryptedPassword]);
storageStub.listItems.withArgs(PROVIDER_DB_KEY, 0, null).yieldsAsync(null, [provider]);
storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).yieldsAsync(null, [username]);
storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).yieldsAsync(null, [realname]);
storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).yieldsAsync(null, [imap]);
storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).yieldsAsync(null, [smtp]);
pgpStub.decrypt.withArgs(encryptedPassword, undefined).yields(null, password);
describe('getCredentials', function() {
var getCertificateStub, queryEmailAddressStub, getEmailAddressFromConfigStub;
beforeEach(function() {
getCertificateStub = sinon.stub(auth, 'getCertificate');
queryEmailAddressStub = sinon.stub(auth, 'queryEmailAddress');
getEmailAddressFromConfigStub = sinon.stub(auth, 'getEmailAddressFromConfig');
});
it('should work', function(done) {
getCertificateStub.yields(null, 'cert');
getEmailAddressFromConfigStub.yields(null, 'asdf@example.com');
queryEmailAddressStub.withArgs('token').yields(null, 'asdf@example.com');
oauthStub.getOAuthToken.withArgs('asdf@example.com').yields(null, 'token');
auth.getCredentials({}, function(err, credentials) {
auth.getCredentials(function(err, cred) {
expect(err).to.not.exist;
expect(credentials.emailAddress).to.equal('asdf@example.com');
expect(credentials.oauthToken).to.equal('token');
expect(credentials.sslCert).to.equal('cert');
done();
});
});
it('should fail due to error in getCertificate', function(done) {
getCertificateStub.yields(new Error());
expect(auth.provider).to.equal(provider);
expect(auth.emailAddress).to.equal(emailAddress);
expect(auth.password).to.equal(password);
auth.getCredentials({}, function(err, credentials) {
expect(err).to.exist;
expect(credentials).to.not.exist;
done();
});
});
expect(cred.imap.host).to.equal(imap.host);
expect(cred.imap.port).to.equal(imap.port);
expect(cred.imap.secure).to.equal(imap.secure);
expect(cred.imap.ca).to.equal(imap.ca);
expect(cred.imap.auth.user).to.equal(username);
expect(cred.imap.auth.pass).to.equal(password);
it('should fail due to error in getOAuthToken', function(done) {
getCertificateStub.yields(null, 'cert');
getEmailAddressFromConfigStub.yields(null, 'asdf@example.com');
oauthStub.getOAuthToken.yields(new Error());
expect(cred.smtp.host).to.equal(smtp.host);
expect(cred.smtp.port).to.equal(smtp.port);
expect(cred.smtp.secure).to.equal(smtp.secure);
expect(cred.smtp.ca).to.equal(smtp.ca);
expect(cred.smtp.auth.user).to.equal(username);
expect(cred.smtp.auth.pass).to.equal(password);
auth.getCredentials({}, function(err, credentials) {
expect(err).to.exist;
expect(credentials).to.not.exist;
done();
});
});
expect(storageStub.listItems.callCount).to.equal(7);
expect(pgpStub.decrypt.calledOnce).to.be.true;
it('should fail due to error in queryEmailAddress', function(done) {
getCertificateStub.yields(null, 'cert');
getEmailAddressFromConfigStub.yields(null, 'asdf@example.com');
queryEmailAddressStub.withArgs('token').yields(new Error());
oauthStub.getOAuthToken.yields(null, 'token');
auth.getCredentials({}, function(err, credentials) {
expect(err).to.exist;
expect(credentials).to.not.exist;
done();
});
});
});
describe('getCertificate', function() {
it('should work', function(done) {
caStub.get.yields(null, 'cert');
describe('#setCredentials', function() {
it('should set the credentials', function() {
auth.setCredentials({
provider: 'albhsvadlbvsdalbsadflb',
emailAddress: emailAddress,
username: username,
realname: realname,
password: password,
imap: imap,
smtp: smtp
});
auth.getCertificate(function(err, cert) {
expect(auth.provider).to.equal('albhsvadlbvsdalbsadflb');
expect(auth.emailAddress).to.equal(emailAddress);
expect(auth.username).to.equal(username);
expect(auth.realname).to.equal(realname);
expect(auth.password).to.equal(password);
expect(auth.smtp).to.equal(smtp);
expect(auth.imap).to.equal(imap);
expect(auth.credentialsDirty).to.be.true;
});
});
describe('#storeCredentials', function() {
it('should persist ALL the things!', function(done) {
auth.credentialsDirty = true;
auth.emailAddress = emailAddress;
auth.username = username;
auth.realname = realname;
auth.password = password;
auth.smtp = smtp;
auth.imap = imap;
auth.provider = provider;
storageStub.storeList.withArgs([encryptedPassword], PASSWD_DB_KEY).yieldsAsync();
storageStub.storeList.withArgs([emailAddress], EMAIL_ADDR_DB_KEY).yieldsAsync();
storageStub.storeList.withArgs([provider], PROVIDER_DB_KEY).yieldsAsync();
storageStub.storeList.withArgs([username], USERNAME_DB_KEY).yieldsAsync();
storageStub.storeList.withArgs([realname], REALNAME_DB_KEY).yieldsAsync();
storageStub.storeList.withArgs([imap], IMAP_DB_KEY).yieldsAsync();
storageStub.storeList.withArgs([smtp], SMTP_DB_KEY).yieldsAsync();
pgpStub.encrypt.withArgs(password).yields(null, encryptedPassword);
auth.storeCredentials(function(err) {
expect(err).to.not.exist;
expect(cert).to.equal('cert');
expect(storageStub.storeList.callCount).to.equal(7);
expect(pgpStub.encrypt.calledOnce).to.be.true;
done();
});
});
});
describe('#getOAuthToken', function() {
it('should fetch token with known email address', function(done) {
auth.emailAddress = emailAddress;
oauthStub.getOAuthToken.withArgs(emailAddress).yieldsAsync(null, oauthToken);
auth.getOAuthToken(function(err) {
expect(err).to.not.exist;
expect(auth.emailAddress).to.equal(emailAddress);
expect(auth.oauthToken).to.equal(oauthToken);
expect(oauthStub.getOAuthToken.calledOnce).to.be.true;
done();
});
});
it('should fetch token with unknown email address', function(done) {
oauthStub.getOAuthToken.withArgs(undefined).yieldsAsync(null, oauthToken);
oauthStub.queryEmailAddress.withArgs(oauthToken).yieldsAsync(null, emailAddress);
auth.getOAuthToken(function(err) {
expect(err).to.not.exist;
expect(auth.emailAddress).to.equal(emailAddress);
expect(auth.oauthToken).to.equal(oauthToken);
expect(oauthStub.getOAuthToken.calledOnce).to.be.true;
expect(oauthStub.queryEmailAddress.calledOnce).to.be.true;
done();
});
});
it('should fail when email address fetch fails', function(done) {
oauthStub.getOAuthToken.yieldsAsync(null, oauthToken);
oauthStub.queryEmailAddress.yieldsAsync(new Error());
auth.getOAuthToken(function(err) {
expect(err).to.exist;
expect(auth.emailAddress).to.not.exist;
expect(auth.oauthToken).to.not.exist;
expect(oauthStub.getOAuthToken.calledOnce).to.be.true;
expect(oauthStub.queryEmailAddress.calledOnce).to.be.true;
done();
});
});
it('should fail when oauth fetch fails', function(done) {
oauthStub.getOAuthToken.yieldsAsync(new Error());
auth.getOAuthToken(function(err) {
expect(err).to.exist;
expect(auth.emailAddress).to.not.exist;
expect(auth.oauthToken).to.not.exist;
expect(oauthStub.getOAuthToken.calledOnce).to.be.true;
expect(oauthStub.queryEmailAddress.called).to.be.false;
done();
});
});
});
describe('#_loadCredentials', function() {
it('should work', function(done) {
storageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY, 0, null).yieldsAsync(null, [emailAddress]);
storageStub.listItems.withArgs(PASSWD_DB_KEY, 0, null).yieldsAsync(null, [encryptedPassword]);
storageStub.listItems.withArgs(PROVIDER_DB_KEY, 0, null).yieldsAsync(null, [provider]);
storageStub.listItems.withArgs(USERNAME_DB_KEY, 0, null).yieldsAsync(null, [username]);
storageStub.listItems.withArgs(REALNAME_DB_KEY, 0, null).yieldsAsync(null, [realname]);
storageStub.listItems.withArgs(IMAP_DB_KEY, 0, null).yieldsAsync(null, [imap]);
storageStub.listItems.withArgs(SMTP_DB_KEY, 0, null).yieldsAsync(null, [smtp]);
auth._loadCredentials(function(err) {
expect(err).to.not.exist;
expect(auth.emailAddress).to.equal(emailAddress);
expect(auth.password).to.equal(encryptedPassword);
expect(auth.provider).to.equal(provider);
expect(auth.imap).to.equal(imap);
expect(auth.smtp).to.equal(smtp);
expect(auth.username).to.equal(username);
expect(auth.realname).to.equal(realname);
expect(auth.passwordNeedsDecryption).to.be.true;
expect(storageStub.listItems.callCount).to.equal(7);
done();
});
});
it('should fail', function(done) {
caStub.get.yields(null, '');
storageStub.listItems.yieldsAsync(new Error());
auth.getCertificate(function(err, cert) {
auth._loadCredentials(function(err) {
expect(err).to.exist;
expect(cert).to.not.exist;
expect(auth.emailAddress).to.not.exist;
expect(auth.password).to.not.exist;
expect(auth.provider).to.not.exist;
expect(auth.imap).to.not.exist;
expect(auth.smtp).to.not.exist;
expect(auth.username).to.not.exist;
expect(auth.realname).to.not.exist;
expect(storageStub.listItems.calledOnce).to.be.true;
done();
});
});
});
describe('getEmailAddress', function() {
var getEmailAddressFromConfigStub;
describe('#handleCertificateUpdate', function() {
var storeCredentialsStub;
var dummyCert = 'cert';
function onConnectDummy() {}
beforeEach(function() {
getEmailAddressFromConfigStub = sinon.stub(auth, 'getEmailAddressFromConfig');
storeCredentialsStub = sinon.stub(auth, 'storeCredentials');
});
it('should work', function(done) {
getEmailAddressFromConfigStub.yields(null, 'asdf@example.com');
it('should work for Trust on first use', function(done) {
auth.imap = {};
storeCredentialsStub.yields();
auth.getEmailAddress(function(err, emailAddress) {
function callback(err) {
expect(err).to.not.exist;
expect(emailAddress).to.equal('asdf@example.com');
expect(storeCredentialsStub.callCount).to.equal(1);
done();
});
}
auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert);
});
it('should fail', function(done) {
getEmailAddressFromConfigStub.yields(new Error());
it('should work for stored cert', function() {
auth.imap = {
ca: dummyCert
};
storeCredentialsStub.yields();
auth.getEmailAddress(function(err, emailAddress) {
auth.handleCertificateUpdate('imap', onConnectDummy, onConnectDummy, dummyCert);
expect(storeCredentialsStub.callCount).to.equal(0);
});
it('should work for pinned cert', function(done) {
auth.imap = {
ca: 'other',
pinned: true
};
storeCredentialsStub.yields();
function callback(err) {
expect(err).to.exist;
expect(emailAddress).to.not.exist;
expect(err.message).to.exist;
expect(storeCredentialsStub.callCount).to.equal(0);
done();
});
}
auth.handleCertificateUpdate('imap', onConnectDummy, callback, dummyCert);
});
it('should work for updated cert', function(done) {
auth.imap = {
ca: 'other'
};
storeCredentialsStub.yields();
function callback(err) {
if (err && err.callback) {
expect(err).to.exist;
expect(err.message).to.exist;
expect(storeCredentialsStub.callCount).to.equal(0);
err.callback(true);
} else {
expect(storeCredentialsStub.callCount).to.equal(1);
done();
}
}
function onConnect(callback) {
callback();
}
auth.handleCertificateUpdate('imap', onConnect, callback, dummyCert);
});
});
describe('getEmailAddressFromConfig', function() {
it('should work', function(done) {
appConfigStoreStub.listItems.withArgs('emailaddress', 0, null).yields(null, ['asdf@example.com']);
auth.getEmailAddressFromConfig(function(err, emailAddress) {
expect(err).to.not.exist;
expect(emailAddress).to.equal('asdf@example.com');
done();
});
});
it('should return empty result', function(done) {
appConfigStoreStub.listItems.withArgs('emailaddress', 0, null).yields(null, []);
auth.getEmailAddressFromConfig(function(err, emailAddress) {
expect(err).to.not.exist;
expect(emailAddress).to.not.exist;
done();
});
});
it('should fail', function(done) {
appConfigStoreStub.listItems.withArgs('emailaddress', 0, null).yields(new Error());
auth.getEmailAddressFromConfig(function(err, emailAddress) {
expect(err).to.exist;
expect(emailAddress).to.not.exist;
done();
});
});
});
describe('queryEmailAddress', function() {
var getEmailAddressFromConfigStub;
beforeEach(function() {
getEmailAddressFromConfigStub = sinon.stub(auth, 'getEmailAddressFromConfig');
});
it('should if already cached', function(done) {
getEmailAddressFromConfigStub.yields(null, 'asdf@example.com');
auth.queryEmailAddress('token', function(err, emailAddress) {
expect(err).to.not.exist;
expect(emailAddress).to.equal('asdf@example.com');
done();
});
});
it('should when querying oauth api', function(done) {
getEmailAddressFromConfigStub.yields();
oauthStub.queryEmailAddress.withArgs('token').yields(null, 'asdf@example.com');
appConfigStoreStub.storeList.withArgs(['asdf@example.com'], 'emailaddress').yields();
auth.queryEmailAddress('token', function(err, emailAddress) {
expect(err).to.not.exist;
expect(emailAddress).to.equal('asdf@example.com');
done();
});
});
it('should fail due to error in cache lookup', function(done) {
getEmailAddressFromConfigStub.yields(new Error());
auth.queryEmailAddress('token', function(err, emailAddress) {
expect(err).to.exist;
expect(emailAddress).to.not.exist;
done();
});
});
it('should fail due to error in oauth api', function(done) {
getEmailAddressFromConfigStub.yields();
oauthStub.queryEmailAddress.withArgs('token').yields(new Error());
auth.queryEmailAddress('token', function(err, emailAddress) {
expect(err).to.exist;
expect(emailAddress).to.not.exist;
done();
});
});
it('should fail due to error in oauth api', function(done) {
getEmailAddressFromConfigStub.yields();
oauthStub.queryEmailAddress.withArgs('token').yields(null, 'asdf@example.com');
appConfigStoreStub.storeList.withArgs(['asdf@example.com'], 'emailaddress').yields(new Error());
auth.queryEmailAddress('token', function(err, emailAddress) {
expect(err).to.exist;
expect(emailAddress).to.exist;
done();
});
});
});
});
});

View File

@ -69,7 +69,7 @@ define(function(require) {
messages: []
};
folders = [inboxFolder, outboxFolder, trashFolder];
folders = [inboxFolder, outboxFolder, trashFolder, sentFolder];
account = {
emailAddress: emailAddress,
@ -1544,14 +1544,21 @@ define(function(require) {
publicKeysArmored: publicKeys
};
it('should send encrypted', function(done) {
it('should send encrypted and upload to sent', function(done) {
var msg = 'wow. such message. much rfc2822.';
imapClientStub.uploadMessage.withArgs({
path: sentFolder.path,
message: msg
}).yields();
pgpMailerStub.send.withArgs({
encrypt: true,
cleartextMessage: str.message,
mail: dummyMail,
smtpclient: undefined,
publicKeysArmored: publicKeys
}).yieldsAsync();
}).yieldsAsync(null, msg);
dao.sendEncrypted({
email: dummyMail
@ -1559,6 +1566,32 @@ define(function(require) {
expect(err).to.not.exist;
expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
done();
});
});
it('should send encrypted and not upload to sent', function(done) {
var msg = 'wow. such message. much rfc2822.';
dao.ignoreUploadOnSent = true;
pgpMailerStub.send.withArgs({
encrypt: true,
cleartextMessage: str.message,
mail: dummyMail,
smtpclient: undefined,
publicKeysArmored: publicKeys
}).yieldsAsync(null, msg);
dao.sendEncrypted({
email: dummyMail
}, function(err) {
expect(err).to.not.exist;
expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.called).to.be.false;
done();
});
@ -1573,6 +1606,7 @@ define(function(require) {
expect(err).to.exist;
expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.called).to.be.false;
done();
});
@ -1584,6 +1618,7 @@ define(function(require) {
dao.sendEncrypted({}, function(err) {
expect(err).to.exist;
expect(pgpMailerStub.send.called).to.be.false;
expect(imapClientStub.uploadMessage.called).to.be.false;
done();
});
});
@ -1593,17 +1628,45 @@ define(function(require) {
describe('#sendPlaintext', function() {
var dummyMail = {};
it('should send in the plain', function(done) {
it('should send in the plain and upload to sent', function(done) {
var msg = 'wow. such message. much rfc2822.';
pgpMailerStub.send.withArgs({
smtpclient: undefined,
mail: dummyMail
}).yieldsAsync();
}).yieldsAsync(null, msg);
imapClientStub.uploadMessage.withArgs({
path: sentFolder.path,
message: msg
}).yields();
dao.sendPlaintext({
email: dummyMail
}, function(err) {
expect(err).to.not.exist;
expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.calledOnce).to.be.true;
done();
});
});
it('should send in the plain and not upload to sent', function(done) {
var msg = 'wow. such message. much rfc2822.';
dao.ignoreUploadOnSent = true;
pgpMailerStub.send.withArgs({
smtpclient: undefined,
mail: dummyMail
}).yieldsAsync(null, msg);
dao.sendPlaintext({
email: dummyMail
}, function(err) {
expect(err).to.not.exist;
expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.called).to.be.false;
done();
});
});
@ -1616,6 +1679,7 @@ define(function(require) {
}, function(err) {
expect(err).to.exist;
expect(pgpMailerStub.send.calledOnce).to.be.true;
expect(imapClientStub.uploadMessage.called).to.be.false;
done();
});
});
@ -1626,6 +1690,7 @@ define(function(require) {
dao.sendPlaintext({}, function(err) {
expect(err).to.exist;
expect(pgpMailerStub.send.called).to.be.false;
expect(imapClientStub.uploadMessage.called).to.be.false;
done();
});
});

View File

@ -14,10 +14,10 @@ define(function(require) {
var scope, location, ctrl,
origEmailDao, emailDaoMock,
origKeychain, keychainMock,
origAuth, authStub,
emailAddress = 'fred@foo.com',
startAppStub,
checkForUpdateStub,
authStub,
initStub;
describe('initialization', function() {
@ -32,6 +32,7 @@ define(function(require) {
// remember original module to restore later, then replace it
origEmailDao = appController._emailDao;
origKeychain = appController._keychain;
origAuth = appController._auth;
appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
appController._auth = authStub = sinon.createStubInstance(Auth);
@ -54,6 +55,7 @@ define(function(require) {
// restore the app controller module
appController._emailDao = origEmailDao;
appController._keychain = origKeychain;
appController._auth = origAuth;
appController.start.restore && appController.start.restore();
appController.checkForUpdate.restore && appController.checkForUpdate.restore();
appController.init.restore && appController.init.restore();
@ -71,7 +73,10 @@ define(function(require) {
};
startAppStub.yields();
authStub.getEmailAddress.yields(null, emailAddress);
authStub.getEmailAddress.yields(null, {
emailAddress: emailAddress,
realname: 'asd'
});
initStub.yields(null, testKeys);
emailDaoMock.unlock.withArgs({
@ -106,7 +111,10 @@ define(function(require) {
};
startAppStub.yields();
authStub.getEmailAddress.yields(null, emailAddress);
authStub.getEmailAddress.yields(null, {
emailAddress: emailAddress,
realname: 'asd'
});
initStub.yields(null, testKeys);
emailDaoMock.unlock.withArgs({
@ -136,7 +144,10 @@ define(function(require) {
it('should forward to privatekey download login', function(done) {
startAppStub.yields();
authStub.getEmailAddress.yields(null, emailAddress);
authStub.getEmailAddress.yields(null, {
emailAddress: emailAddress,
realname: 'asd'
});
initStub.yields(null, {
publicKey: 'b'
});
@ -165,7 +176,10 @@ define(function(require) {
it('should forward to new device login', function(done) {
startAppStub.yields();
authStub.getEmailAddress.yields(null, emailAddress);
authStub.getEmailAddress.yields(null, {
emailAddress: emailAddress,
realname: 'asd'
});
initStub.yields(null, {
publicKey: 'b'
});
@ -194,7 +208,10 @@ define(function(require) {
it('should forward to initial login', function(done) {
startAppStub.yields();
authStub.getEmailAddress.yields(null, emailAddress);
authStub.getEmailAddress.yields(null, {
emailAddress: emailAddress,
realname: 'asd'
});
initStub.yields();
angular.module('logintest', []);

View File

@ -3,6 +3,7 @@ define(function(require) {
var expect = chai.expect,
angular = require('angular'),
Auth = require('js/bo/auth'),
mocks = require('angularMocks'),
LoginExistingCtrl = require('js/controller/login-existing'),
EmailDAO = require('js/dao/email-dao'),
@ -11,6 +12,7 @@ define(function(require) {
describe('Login (existing user) Controller unit test', function() {
var scope, location, ctrl, origEmailDao, emailDaoMock,
origAuth, authMock,
emailAddress = 'fred@foo.com',
passphrase = 'asd',
keychainMock;
@ -18,9 +20,10 @@ define(function(require) {
beforeEach(function() {
// remember original module to restore later
origEmailDao = appController._emailDao;
origAuth = appController._auth;
emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._emailDao = emailDaoMock;
appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._auth = authMock = sinon.createStubInstance(Auth);
keychainMock = sinon.createStubInstance(KeychainDAO);
emailDaoMock._keychain = keychainMock;
@ -44,6 +47,7 @@ define(function(require) {
afterEach(function() {
// restore the module
appController._emailDao = origEmailDao;
appController._auth = origAuth;
});
describe('initial state', function() {
@ -75,6 +79,7 @@ define(function(require) {
keypair: keypair,
passphrase: passphrase
}).yields();
authMock.storeCredentials.yields();
scope.confirmPassphrase();

View File

@ -3,6 +3,7 @@ define(function(require) {
var expect = chai.expect,
angular = require('angular'),
Auth = require('js/bo/auth'),
mocks = require('angularMocks'),
LoginInitialCtrl = require('js/controller/login-initial'),
PGP = require('js/crypto/pgp'),
@ -11,6 +12,7 @@ define(function(require) {
describe('Login (initial user) Controller unit test', function() {
var scope, ctrl, location, origEmailDao, emailDaoMock,
origAuth, authMock,
emailAddress = 'fred@foo.com',
passphrase = 'asd',
keyId, expectedKeyId,
@ -19,9 +21,10 @@ define(function(require) {
beforeEach(function() {
// remember original module to restore later
origEmailDao = appController._emailDao;
origAuth = appController._auth;
emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._emailDao = emailDaoMock;
appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._auth = authMock = sinon.createStubInstance(Auth);
keyId = '9FEB47936E712926';
expectedKeyId = '6E712926';
@ -49,6 +52,7 @@ define(function(require) {
afterEach(function() {
// restore the module
appController._emailDao = origEmailDao;
appController._auth = origAuth;
});
describe('initial state', function() {
@ -143,6 +147,7 @@ define(function(require) {
emailDaoMock.unlock.withArgs({
passphrase: passphrase
}).yields();
authMock.storeCredentials.yields();
scope.$apply = function() {
expect(location.$$path).to.equal('/desktop');

View File

@ -4,6 +4,7 @@ define(function(require) {
var expect = chai.expect,
angular = require('angular'),
mocks = require('angularMocks'),
Auth = require('js/bo/auth'),
LoginPrivateKeyDownloadCtrl = require('js/controller/login-privatekey-download'),
EmailDAO = require('js/dao/email-dao'),
appController = require('js/app-controller'),
@ -12,6 +13,7 @@ define(function(require) {
describe('Login Private Key Download Controller unit test', function() {
var scope, location, ctrl,
origEmailDao, emailDaoMock,
origAuth, authMock,
origKeychain, keychainMock,
emailAddress = 'fred@foo.com';
@ -19,8 +21,11 @@ define(function(require) {
// remember original module to restore later, then replace it
origEmailDao = appController._emailDao;
origKeychain = appController._keychain;
origAuth = appController._auth;
appController._emailDao = emailDaoMock = sinon.createStubInstance(EmailDAO);
appController._keychain = keychainMock = sinon.createStubInstance(KeychainDAO);
appController._auth = authMock = sinon.createStubInstance(Auth);
emailDaoMock._account = {
emailAddress: emailAddress
@ -43,6 +48,7 @@ define(function(require) {
// restore the app controller module
appController._emailDao = origEmailDao;
appController._keychain = origKeychain;
appController._auth = origAuth;
});
describe('initialization', function() {
@ -176,6 +182,7 @@ define(function(require) {
encryptedKey: 'keyArmored'
});
emailDaoMock.unlock.yields();
authMock.storeCredentials.yields();
scope.goTo = function(location) {
expect(location).to.equal('/desktop');

View File

@ -0,0 +1,100 @@
define(function(require) {
'use strict';
var expect = chai.expect,
angular = require('angular'),
mocks = require('angularMocks'),
ImapClient = require('imap-client'),
SmtpClient = require('smtpclient'),
SetCredentialsCtrl = require('js/controller/login-set-credentials'),
appController = require('js/app-controller');
describe('Login (Set Credentials) Controller unit test', function() {
var scope, location, setCredentialsCtrl;
var imap, smtp;
var origAuth;
var provider = 'providerproviderprovider';
beforeEach(function() {
origAuth = appController._auth;
appController._auth = {};
imap = sinon.createStubInstance(ImapClient);
smtp = sinon.createStubInstance(SmtpClient);
angular.module('setcredentialstest', []);
mocks.module('setcredentialstest');
mocks.inject(function($rootScope, $controller, $location) {
scope = $rootScope.$new();
location = $location;
location.search({
provider: provider
});
scope.state = {};
setCredentialsCtrl = $controller(SetCredentialsCtrl, {
$scope: scope
});
});
});
afterEach(function() {
appController._auth = origAuth;
});
describe('set credentials', function() {
it('should work', function(done) {
var imapCert = 'imapcertimapcertimapcertimapcertimapcertimapcert',
smtpCert = 'smtpcertsmtpcertsmtpcertsmtpcertsmtpcertsmtpcert';
var emailAddress, password, smtpHost, smtpPort, smtpSecure, imapHost, imapPort, imapSecure, realname;
scope.emailAddress = emailAddress = 'emailemailemailemail';
scope.password = password = 'passwdpasswdpasswdpasswd';
scope.smtpHost = smtpHost = 'hosthosthost';
scope.smtpPort = smtpPort = 1337;
scope.smtpSecure = smtpSecure = true;
scope.imapHost = imapHost = 'hosthosthost';
scope.imapPort = imapPort = 1337;
scope.imapSecure = imapSecure = true;
scope.realname = realname = 'peter pan';
imap.login.yields();
appController._auth.setCredentials = function(args) {
expect(smtp.connect.calledOnce).to.be.true;
expect(imap.login.calledOnce).to.be.true;
expect(args).to.deep.equal({
provider: provider,
emailAddress: scope.emailAddress,
username: scope.username || scope.emailAddress,
realname: scope.realname,
password: scope.password,
imap: {
host: scope.imapHost.toLowerCase(),
port: scope.imapPort,
secure: scope.imapSecure,
ca: scope.imapCert,
pinned: false
},
smtp: {
host: scope.smtpHost.toLowerCase(),
port: scope.smtpPort,
secure: scope.smtpSecure,
ca: scope.smtpCert,
pinned: false
}
});
done();
};
scope.test(imap, smtp);
imap.onCert(imapCert);
smtp.oncert(smtpCert);
smtp.onidle();
});
});
});
});

View File

@ -74,6 +74,7 @@ function startTests() {
'test/unit/login-initial-ctrl-test',
'test/unit/login-new-device-ctrl-test',
'test/unit/login-privatekey-download-ctrl-test',
'test/unit/login-set-credentials-ctrl-test',
'test/unit/privatekey-upload-ctrl-test',
'test/unit/login-ctrl-test',
'test/unit/read-ctrl-test',

View File

@ -2,12 +2,14 @@ define(function(require) {
'use strict';
var DeviceStorageDAO = require('js/dao/devicestorage-dao'),
Auth = require('js/bo/auth'),
cfg = require('js/app-config').config,
UpdateHandler = require('js/util/update/update-handler'),
config = require('js/app-config').config,
expect = chai.expect;
describe('UpdateHandler', function() {
var updateHandler, appConfigStorageStub, userStorageStub, origDbVersion;
var updateHandler, appConfigStorageStub, authStub, userStorageStub, origDbVersion;
chai.Assertion.includeStack = true;
@ -15,7 +17,8 @@ define(function(require) {
origDbVersion = cfg.dbVersion;
appConfigStorageStub = sinon.createStubInstance(DeviceStorageDAO);
userStorageStub = sinon.createStubInstance(DeviceStorageDAO);
updateHandler = new UpdateHandler(appConfigStorageStub, userStorageStub);
authStub = sinon.createStubInstance(Auth);
updateHandler = new UpdateHandler(appConfigStorageStub, userStorageStub, authStub);
});
afterEach(function() {
@ -137,7 +140,7 @@ define(function(require) {
it('should fail when persisting database version fails', function(done) {
userStorageStub.removeList.yieldsAsync();
appConfigStorageStub.storeList.yieldsAsync({});
appConfigStorageStub.storeList.yieldsAsync(new Error());
updateHandler.update(function(error) {
expect(error).to.exist;
@ -149,7 +152,7 @@ define(function(require) {
});
it('should fail when wiping emails from database fails', function(done) {
userStorageStub.removeList.yieldsAsync({});
userStorageStub.removeList.yieldsAsync(new Error());
updateHandler.update(function(error) {
expect(error).to.exist;
@ -190,7 +193,7 @@ define(function(require) {
it('should fail when persisting database version fails', function(done) {
userStorageStub.removeList.yieldsAsync();
appConfigStorageStub.storeList.yieldsAsync({});
appConfigStorageStub.storeList.yieldsAsync(new Error());
updateHandler.update(function(error) {
expect(error).to.exist;
@ -202,7 +205,7 @@ define(function(require) {
});
it('should fail when wiping emails from database fails', function(done) {
userStorageStub.removeList.yieldsAsync({});
userStorageStub.removeList.yieldsAsync(new Error());
updateHandler.update(function(error) {
expect(error).to.exist;
@ -243,7 +246,7 @@ define(function(require) {
it('should fail when persisting database version fails', function(done) {
userStorageStub.removeList.yieldsAsync();
appConfigStorageStub.storeList.yieldsAsync({});
appConfigStorageStub.storeList.yieldsAsync(new Error());
updateHandler.update(function(error) {
expect(error).to.exist;
@ -255,13 +258,91 @@ define(function(require) {
});
it('should fail when wiping emails from database fails', function(done) {
userStorageStub.removeList.yieldsAsync({});
userStorageStub.removeList.yieldsAsync(new Error());
updateHandler.update(function(error) {
expect(error).to.exist;
expect(userStorageStub.removeList.calledOnce).to.be.true;
expect(appConfigStorageStub.storeList.called).to.be.false;
done();
});
});
});
describe('v3 -> v4', function() {
var EMAIL_ADDR_DB_KEY = 'emailaddress';
var USERNAME_DB_KEY = 'username';
var PROVIDER_DB_KEY = 'provider';
var IMAP_DB_KEY = 'imap';
var SMTP_DB_KEY = 'smtp';
var REALNAME_DB_KEY = 'realname';
var emailaddress = 'bla@blubb.io';
var imap = config.gmail.imap,
smtp = config.gmail.smtp;
beforeEach(function() {
cfg.dbVersion = 4; // app requires database version 4
appConfigStorageStub.listItems.withArgs(versionDbType).yieldsAsync(null, [3]); // database version is 3
});
it('should add gmail as mail service provider with email address and no provider present in db', function(done) {
appConfigStorageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).yieldsAsync(null, [emailaddress]);
appConfigStorageStub.listItems.withArgs(PROVIDER_DB_KEY).yieldsAsync(null, []);
appConfigStorageStub.storeList.withArgs([4], versionDbType).yieldsAsync();
appConfigStorageStub.storeList.withArgs(['gmail'], PROVIDER_DB_KEY).yieldsAsync();
appConfigStorageStub.storeList.withArgs([emailaddress], USERNAME_DB_KEY).yieldsAsync();
appConfigStorageStub.storeList.withArgs([imap], IMAP_DB_KEY).yieldsAsync();
appConfigStorageStub.storeList.withArgs([smtp], SMTP_DB_KEY).yieldsAsync();
appConfigStorageStub.storeList.withArgs([''], REALNAME_DB_KEY).yieldsAsync();
authStub._loadCredentials.yieldsAsync();
updateHandler.update(function(error) {
expect(error).to.not.exist;
expect(appConfigStorageStub.storeList.callCount).to.equal(6);
expect(appConfigStorageStub.listItems.calledThrice).to.be.true;
expect(authStub._loadCredentials.calledOnce).to.be.true;
done();
});
});
it('should not add a provider when no email adress is in db', function(done) {
appConfigStorageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).yieldsAsync(null, []);
appConfigStorageStub.listItems.withArgs(PROVIDER_DB_KEY).yieldsAsync(null, []);
appConfigStorageStub.storeList.withArgs([4], versionDbType).yieldsAsync();
updateHandler.update(function(error) {
expect(error).to.not.exist;
expect(appConfigStorageStub.storeList.calledOnce).to.be.true;
expect(appConfigStorageStub.listItems.calledThrice).to.be.true;
done();
});
});
it('should fail when appConfigStore write fails', function(done) {
appConfigStorageStub.listItems.yieldsAsync(null, []);
appConfigStorageStub.storeList.yieldsAsync(new Error());
updateHandler.update(function(error) {
expect(error).to.exist;
expect(appConfigStorageStub.listItems.calledThrice).to.be.true;
expect(appConfigStorageStub.storeList.calledOnce).to.be.true;
done();
});
});
it('should fail when appConfigStore read fails', function(done) {
appConfigStorageStub.listItems.withArgs(EMAIL_ADDR_DB_KEY).yieldsAsync(new Error());
appConfigStorageStub.storeList.yieldsAsync(new Error());
updateHandler.update(function(error) {
expect(error).to.exist;
expect(appConfigStorageStub.listItems.calledTwice).to.be.true;
expect(appConfigStorageStub.storeList.called).to.be.false;
done();
});
});

View File

@ -13,7 +13,7 @@ define(function(require) {
describe('Write controller unit test', function() {
var ctrl, scope,
origEmailDao, origOutbox, origKeychain,
emailDaoMock, keychainMock, outboxMock, emailAddress;
emailDaoMock, keychainMock, outboxMock, emailAddress, realname;
beforeEach(function() {
// the app controller is a singleton, we need to remember the
@ -29,8 +29,10 @@ define(function(require) {
appController._emailDao = emailDaoMock;
emailAddress = 'fred@foo.com';
realname = 'Fred Foo';
emailDaoMock._account = {
emailAddress: emailAddress,
realname: realname
};
keychainMock = sinon.createStubInstance(KeychainDAO);
@ -357,7 +359,8 @@ define(function(require) {
outboxMock.put.withArgs(sinon.match(function(mail) {
expect(mail.from).to.deep.equal([{
address: emailAddress
address: emailAddress,
name: realname
}]);
expect(mail.to).to.deep.equal(scope.to);
expect(mail.cc).to.deep.equal(scope.cc);