mirror of
https://github.com/moparisthebest/mail
synced 2024-11-22 08:52:15 -05:00
[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:
parent
26f62668c6
commit
3e80138a10
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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-----
|
@ -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',
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
});
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
157
src/js/controller/login-set-credentials.js
Normal file
157
src/js/controller/login-set-credentials.js
Normal 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;
|
||||
});
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
});
|
105
src/js/util/update/update-v4.js
Normal file
105
src/js/util/update/update-v4.js
Normal 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;
|
||||
});
|
2
src/lib/forge/forge.min.js
vendored
2
src/lib/forge/forge.min.js
vendored
File diff suppressed because one or more lines are too long
@ -17,8 +17,7 @@
|
||||
"https://www.googleapis.com/",
|
||||
"identity", {
|
||||
"socket": [
|
||||
"tcp-connect:imap.gmail.com:993",
|
||||
"tcp-connect:smtp.gmail.com:465"
|
||||
"tcp-connect:*"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
@ -12,6 +12,8 @@
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
:invalid,
|
||||
.ng-invalid,
|
||||
.input-text-error {
|
||||
border-color: red;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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-->
|
49
src/tpl/login-set-credentials.html
Normal file
49
src/tpl/login-set-credentials.html
Normal 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>
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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', []);
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
100
test/unit/login-set-credentials-ctrl-test.js
Normal file
100
test/unit/login-set-credentials-ctrl-test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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',
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user