├── .gitignore ├── .gitmodules ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.md ├── chrome ├── _locales │ └── en │ │ └── messages.json ├── app.css ├── app.js ├── assets │ ├── css │ │ └── bootstrap.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── roboto │ │ │ ├── Roboto-Black.ttf │ │ │ ├── Roboto-BlackItalic.ttf │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-BoldItalic.ttf │ │ │ ├── Roboto-Italic.ttf │ │ │ ├── Roboto-Light.ttf │ │ │ ├── Roboto-LightItalic.ttf │ │ │ ├── Roboto-Medium.ttf │ │ │ ├── Roboto-MediumItalic.ttf │ │ │ ├── Roboto-Regular.ttf │ │ │ ├── Roboto-Thin.ttf │ │ │ ├── Roboto-ThinItalic.ttf │ │ │ ├── RobotoCondensed-Bold.ttf │ │ │ ├── RobotoCondensed-BoldItalic.ttf │ │ │ ├── RobotoCondensed-Italic.ttf │ │ │ ├── RobotoCondensed-Light.ttf │ │ │ ├── RobotoCondensed-LightItalic.ttf │ │ │ └── RobotoCondensed-Regular.ttf │ ├── img │ │ ├── chrome.png │ │ ├── encryption.png │ │ ├── gmail.png │ │ ├── icon_128.png │ │ ├── idcheck.svg │ │ ├── logo_password_catcher_color_2x_web_96dp.png │ │ └── safemail.svg │ └── js │ │ ├── angular-animate.js │ │ ├── angular-aria.js │ │ ├── angular-route.js │ │ └── angular.js ├── background.js ├── compiler.flags ├── components │ ├── appinfo │ │ ├── appinfo-service.js │ │ └── appinfo-service_test.js │ ├── auth │ │ ├── auth-service.js │ │ └── auth-service_test.js │ ├── autocomplete │ │ ├── autocomplete-directive.js │ │ ├── autocomplete-directive_test.js │ │ ├── autocomplete-service.js │ │ ├── autocomplete-service_test.js │ │ ├── autocomplete.css │ │ └── autocomplete.html │ ├── blobhref │ │ └── blobhref-directive.js │ ├── contacts │ │ ├── contacts-service.js │ │ └── contacts-service_test.js │ ├── fileupload │ │ └── fileupload-directive.js │ ├── gmail │ │ ├── gmail-service.js │ │ └── gmail-service_test.js │ ├── openpgp │ │ ├── openpgp-service.js │ │ └── openpgp-service_test.js │ ├── outerclick │ │ └── outerclick-directive.js │ ├── relativedate │ │ ├── relativedate-filter.js │ │ └── relativedate-filter_test.js │ ├── storage │ │ ├── storage-service.js │ │ └── storage-service_test.js │ ├── translate │ │ ├── translate-filter.js │ │ ├── translate-filter_test.js │ │ ├── translate-service.js │ │ └── translate-service_test.js │ ├── userinfo │ │ ├── userinfo-directive.js │ │ ├── userinfo-directive_test.js │ │ ├── userinfo.css │ │ └── userinfo.html │ └── userlist │ │ ├── userlist-directive.js │ │ ├── userlist-directive_test.js │ │ └── userlist.html ├── constants.js ├── index.html ├── karma │ ├── angular-mocks.js │ └── karma.conf.js ├── manifest.json ├── models │ ├── mail │ │ └── mail-model.js │ └── user │ │ └── user-model.js ├── pages │ ├── authorization │ │ ├── authorization-controller.js │ │ ├── authorization.css │ │ └── authorization.html │ ├── getstarted │ │ ├── getstarted-controller.js │ │ ├── getstarted.css │ │ └── getstarted.html │ ├── introduction │ │ ├── introduction-controller.js │ │ ├── introduction.css │ │ └── introduction.html │ ├── messages │ │ ├── messages-controller.js │ │ ├── messages-controller_test.js │ │ ├── messages.css │ │ └── messages.html │ ├── recover │ │ ├── recover-controller.js │ │ ├── recover-controller_test.js │ │ ├── recover.css │ │ └── recover.html │ ├── reset │ │ ├── reset-controller.js │ │ ├── reset-controller_test.js │ │ ├── reset.css │ │ └── reset.html │ ├── settings │ │ ├── settings-controller.js │ │ ├── settings-controller_test.js │ │ ├── settings.css │ │ └── settings.html │ ├── setup │ │ ├── setup-controller.js │ │ ├── setup-controller_test.js │ │ └── setup.html │ ├── showsecret │ │ ├── showsecret-controller.js │ │ ├── showsecret.css │ │ └── showsecret.html │ ├── threads │ │ ├── threads-controller.js │ │ ├── threads-controller_test.js │ │ ├── threads.css │ │ └── threads.html │ └── welcome │ │ ├── welcome-controller.js │ │ ├── welcome-controller_test.js │ │ ├── welcome.css │ │ └── welcome.html ├── util │ ├── email.js │ ├── email_test.js │ ├── http.js │ ├── http_test.js │ ├── recoverycode.js │ ├── recoverycode_test.js │ └── types.js └── worker │ ├── bootstrap.js │ └── indexeddbstorage.js ├── do.sh ├── package.json └── screenshots ├── basic.png └── icon.png /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | build/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "chrome-lib/end-to-end"] 2 | path = chrome-lib/end-to-end 3 | url = https://github.com/google/end-to-end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | dist: trusty 4 | 5 | addons: 6 | apt: 7 | sources: 8 | - google-chrome 9 | packages: 10 | - google-chrome-stable 11 | 12 | env: 13 | global: 14 | - NODEJS_VERSION=0.10 15 | - NODEJS_CMD=node 16 | 17 | install: 18 | - export PATH=$HOME/.local/bin:$PATH 19 | - pip install --user $USER six 20 | - pip install --user $USER git+https://github.com/google/closure-linter.git 21 | - nvm install ${NODEJS_VERSION} && nvm alias default ${NODEJS_VERSION} 22 | - ./do.sh setup 23 | 24 | before_script: 25 | - export DISPLAY=:99.0 26 | - export CHROME_BIN=/usr/bin/google-chrome-stable 27 | - sh -e /etc/init.d/xvfb start 28 | 29 | script: 30 | - ./do.sh lint 31 | - ./do.sh test_app 32 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of E2EMail authors for copyright purposes. 2 | # 3 | # Names should be added to this file as: 4 | # Name or Organization 5 | # The email address is not required for organizations. 6 | 7 | Eric Grosse 8 | Niels Provos 9 | Alex Stamos 10 | KB Sriram 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![E2EMail](screenshots/icon.png) E2EMail 2 | 3 | This is an experimental version of a simple Chrome application - a Gmail client that exchanges OpenPGP mail. At this stage, we recommend you use it only for testing and UI feedback. 4 | 5 | E2EMail is a simple way for non-technical users to exchange private text mail over Gmail, but is not a fully-featured email or OpenPGP client. 6 | 7 | It is a Chrome app that runs independent of the normal Gmail web interface. It behaves as a sandbox where you can only read or write encrypted email, but is otherwise similar to any other communication app. 8 | 9 | When launched, the app shows just the encrypted mail in the user's Gmail account. Any email sent from the app is also automatically signed and encrypted. 10 | 11 | ![Basic app view](screenshots/basic.png) 12 | 13 | # Why 14 | 15 | E2EMail is being developed to provide an easy and intuitive way for non-technical users to exchange confidential email. 16 | 17 | The goal is to improve data confidentiality for occasional small, sensitive messages. This way even the mail provider, Google in the case of Gmail, is unable to extract the message content. However, it does not protect against attacks on the local device, and, as usual with PGP, the identities of the correspondents and the subject line of the mail is not protected. 18 | 19 | The initial version only supports ordinary text email, and focuses on new users who all use E2EMail to read PGP/MIME mail. 20 | 21 | # How 22 | 23 | E2EMail is built on the Google end-to-end library, please refer to [its threat model](https://github.com/google/end-to-end/wiki/Threat-model) for a detailed analysis of the basic security properties. 24 | 25 | The target is a simple user experience - install app, approve permissions, start reading or send sending messages. As a result, the app automatically handles most of the key management. 26 | 27 | ## Key management 28 | 29 | In this initial version, E2EMail hosts its own keyserver. During app installation, it automatically generates an OpenPGP ECC key and uploads the public half to the keyserver. The keyserver accepts it if the user-id in the key matches an OAuth mediated check against the user's email address. 30 | 31 | The private half is stored locally, but can be regenerated via a secret "recovery code" provided to the user during installation. This code is a 128-bit value that can recreate the key material, and can also be used to regenerate the private key on a different device. 32 | 33 | When sending email, the app automatically looks up the recipient's public key in the keyserver. If it doesn't find it, the user cannot create the email, but may choose to send a pre-canned, unencrypted invitation for the recipient to install the app instead. 34 | 35 | If the recipient's key is found, it is saved locally and checked for subsequent changes (trust on first use/warn on change.) Sophisticated users may still compare key fingerprints in the traditional OpenPGP manner. 36 | 37 | To address the risk of the keyserver introducing spurious keys and to facilitate interoperability, we are [evaluating solutions](http://wiki.gnupg.org/OpenPGPEmailSummit201512?action=AttachFile&do=view&target=key_transparency.pdf) that add transparency and federation to the keyserver. We will adapt the client to use these services as the designs mature and have concrete implementations. 38 | 39 | For now, we use a simple app-engine implementation that runs the OAuth check (but does not offer transparency) so we can independently evolve the client, but is not meant for long-term use. 40 | 41 | ## Email 42 | 43 | The app uses the GMail API to read and send PGP/MIME messages. It also uses the GMail contacts API to enable auto-completing addresses. It reads and sends standard PGP/MIME messages, and emails are organized by threads into a simple list. A simple form is used to to start or reply to threads, and just text content is supported at the moment. 44 | 45 | As with standard PGP/MIME messages, we do not encrypt subject lines or other headers, but plan to track [ongoing efforts](http://wiki.gnupg.org/HeaderProtectionWithMemoryHole) to standardize encrypted headers. 46 | 47 | ## Building 48 | 49 | To build the app, you will need git, python, jdk1.7 or above, and ant. 50 | 51 | First clone this repository 52 | ``` 53 | $ git clone https://github.com/e2email-org/e2email 54 | ``` 55 | Go into the `e2email` directory and do a one-time setup to pull in its dependencies 56 | ``` 57 | $ ./do.sh setup 58 | ``` 59 | Finally, build the app 60 | ``` 61 | $ ./do.sh build 62 | ``` 63 | If everything goes well, an unbundled version of the app can be found under `build/e2email`. You can load this directory in developer mode within Chrome and start up the app. Visit `chrome://extensions` and enable developer mode, click "Load unpacked extension..." and open the `build/e2email` directory. 64 | 65 | To run tests under karma, you should further install nodejs, npm and nvm. 66 | ``` 67 | $ ./do.sh karma 68 | ``` 69 | This starts up a web server, providing a URL where you can point your Chrome browser and run the tests. 70 | 71 | # Attributions 72 | 73 | This repository uses code and assets from: 74 | 75 | * [Angular.js](https://github.com/angular/angular.js) Copyright (c) 2010-2016 Google, Inc and used under [the MIT licence](https://github.com/angular/angular.js/blob/master/LICENSE). 76 | * [Google End-To-End](https://github.com/google/end-to-end) Copyright 2014-2016 Google, Inc and used under the [Apache v2 License](https://github.com/google/end-to-end/blob/master/LICENSE). 77 | * [Google Roboto fonts](https://github.com/google/roboto) Copyright 2015 Google, Inc and used under the [Apache v2 License](https://github.com/google/roboto/blob/master/LICENSE) 78 | * [Twitter Bootstrap](https://github.com/twbs/bootstrap) Copyright 2011-2016 Twitter, Inc and used under [the MIT license](https://github.com/twbs/bootstrap/blob/master/LICENSE). 79 | -------------------------------------------------------------------------------- /chrome/app.css: -------------------------------------------------------------------------------- 1 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], 2 | .ng-cloak, .x-ng-cloak, 3 | .ng-hide:not(.ng-hide-animate) { 4 | display: none !important; 5 | } 6 | 7 | ng\:form { 8 | display: block; 9 | } 10 | 11 | @font-face { 12 | font-family: 'Roboto'; 13 | font-style: normal; 14 | font-weight: 300; 15 | src: local('Roboto Light'), local('Roboto-Light'), url('../fonts/roboto/Roboto-Light.ttf') format('truetype'); 16 | } 17 | 18 | @font-face { 19 | font-family: 'Roboto'; 20 | font-style: normal; 21 | font-weight: 400; 22 | src: local('Roboto'), local('Roboto-Regular'), url('../fonts/roboto/Roboto-Regular.ttf') format('truetype'); 23 | } 24 | 25 | @font-face { 26 | font-family: 'Roboto'; 27 | font-style: normal; 28 | font-weight: bold; 29 | src: local('Roboto Medium'), local('Roboto-Medium'), url('../fonts/roboto/Roboto-Medium.ttf') format('truetype'); 30 | } 31 | 32 | .maincontent { 33 | position: relative; 34 | } 35 | 36 | .viewcontent.ng-enter, .viewcontent.ng-leave { 37 | left: 0; 38 | position: absolute; 39 | right: 0; 40 | top: 0; 41 | transition: all 0.15s ease-in-out; 42 | } 43 | 44 | .viewcontent.ng-enter { 45 | opacity: 0; 46 | } 47 | 48 | .viewcontent.ng-enter-active { 49 | opacity: 1; 50 | } 51 | 52 | .viewcontent.ng-leave { 53 | opacity: 1; 54 | } 55 | 56 | .viewcontent.ng-leave-active { 57 | opacity: 0; 58 | } 59 | 60 | html, body { 61 | height: 100%; 62 | } 63 | 64 | body { 65 | font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; 66 | overflow-y: auto; 67 | } 68 | 69 | .oneline { 70 | overflow: hidden; 71 | text-overflow: ellipsis; 72 | white-space: nowrap; 73 | width: 100%; 74 | } 75 | 76 | .btn-focus { 77 | border-color: transparent; 78 | border-radius: 3px; 79 | border-style: solid; 80 | border-width: 1px; 81 | } 82 | 83 | .btn-focus:focus { 84 | border-color: #66afe9; 85 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px rgba(102, 175, 233, 0.6); 86 | outline: none; 87 | } 88 | 89 | .fade-show { 90 | opacity: 1; 91 | } 92 | 93 | .fade-show.ng-hide-add.ng-hide-add-active, 94 | .fade-show.ng-hide-remove.ng-hide-remove-active { 95 | transition: opacity linear 0.2s; 96 | } 97 | 98 | .fade-show.ng-hide { 99 | opacity: 0; 100 | } 101 | 102 | h1.wizard-title { 103 | font-family: inherit; 104 | font-weight: 300; 105 | text-color: #777; 106 | } 107 | 108 | .selectable { 109 | -webkit-user-select: initial; 110 | cursor: text; 111 | user-select: initial; 112 | } 113 | 114 | .clickable { 115 | cursor: pointer; 116 | } 117 | 118 | .just-hide { 119 | visibility: hidden; 120 | } 121 | 122 | .e2email-header { 123 | background-color: #eee; 124 | padding-bottom: 15px; 125 | padding-top: 15px; 126 | } 127 | 128 | .e2email-content-start { 129 | margin-top: 80px; 130 | } 131 | 132 | .top-row-buffer { 133 | margin-top: 1.4em; 134 | } 135 | 136 | .btn-blank { 137 | background-color: transparent; 138 | color: #808080; 139 | } 140 | 141 | .btn-blank:hover { 142 | color: #428bca; 143 | } 144 | 145 | div.big-icon { 146 | margin: auto; 147 | margin-top: 25px; 148 | margin-bottom: 20px; 149 | } 150 | 151 | div.big-icon > img { 152 | display: block; 153 | margin: auto; 154 | } 155 | 156 | .attach-img { 157 | height: 23px; 158 | margin-top: 10px; 159 | cursor: pointer; 160 | } 161 | 162 | .attach-icon { 163 | margin-top: 13px; 164 | } 165 | 166 | .attach-icon:hover { 167 | fill: #66afe9; 168 | } 169 | 170 | .sidebtn { 171 | width: 70px; 172 | height: 30px; 173 | font-size: 12px; 174 | } 175 | 176 | .single-attachment { 177 | background-color: #f5f5f5; 178 | border: 1px solid #dcdcdc; 179 | font-weight: bold; 180 | font-size: 13px; 181 | margin: 0 7px 6px; 182 | color: #1155CC !important; 183 | text-align: left; 184 | padding: 2px 6px; 185 | max-width: 448px; 186 | width: 80%; 187 | height: 23px; 188 | display: flex; 189 | } 190 | 191 | #textareaSpace { 192 | min-height: 250px; 193 | max-height: 500px; 194 | overflow: auto; 195 | background-color: white; 196 | } 197 | 198 | #textareaElement { 199 | flex;flex-direction: column; 200 | min-height: 250px; 201 | text-align: left; 202 | border: none; 203 | background-color: white; 204 | outline: none; 205 | resize: none; 206 | margin-bottom: 0px; 207 | } 208 | 209 | #attachmentRepeat { 210 | background-color: white; 211 | min-height: 20px; 212 | padding-top: 10px; 213 | display: flex; 214 | flex-direction: column; 215 | } 216 | 217 | .removeAttachment { 218 | border: none; 219 | font-size: 13px; 220 | color: gray; 221 | background-color: inherit; 222 | margin: 0 !important; 223 | padding: 0; 224 | float: right; 225 | } 226 | -------------------------------------------------------------------------------- /chrome/assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /chrome/assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/RobotoCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/RobotoCondensed-Bold.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/RobotoCondensed-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/RobotoCondensed-BoldItalic.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/RobotoCondensed-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/RobotoCondensed-Italic.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/RobotoCondensed-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/RobotoCondensed-Light.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/RobotoCondensed-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/RobotoCondensed-LightItalic.ttf -------------------------------------------------------------------------------- /chrome/assets/fonts/roboto/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/fonts/roboto/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /chrome/assets/img/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/img/chrome.png -------------------------------------------------------------------------------- /chrome/assets/img/encryption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/img/encryption.png -------------------------------------------------------------------------------- /chrome/assets/img/gmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/img/gmail.png -------------------------------------------------------------------------------- /chrome/assets/img/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/img/icon_128.png -------------------------------------------------------------------------------- /chrome/assets/img/logo_password_catcher_color_2x_web_96dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/chrome/assets/img/logo_password_catcher_color_2x_web_96dp.png -------------------------------------------------------------------------------- /chrome/assets/img/safemail.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 14 | 21 | 27 | 33 | 39 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /chrome/background.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | (function() { 18 | chrome.app.runtime.onLaunched.addListener(function(data) { 19 | chrome.app.window.create('../index.html', { 20 | id: 'e2email', 21 | innerBounds: { 22 | width: 440, 23 | height: 680, 24 | minWidth: 440, 25 | minHeight: 680 26 | } 27 | }, function(win) { 28 | if (win) { 29 | win.show(); 30 | win.focus(); 31 | } 32 | }); 33 | }); 34 | 35 | chrome.runtime.onInstalled.addListener(function() { 36 | console.log('installed'); 37 | }); 38 | 39 | chrome.runtime.onSuspend.addListener(function() { 40 | console.log('suspended'); 41 | }); 42 | })(); 43 | -------------------------------------------------------------------------------- /chrome/compiler.flags: -------------------------------------------------------------------------------- 1 | --externs=chrome-lib/end-to-end/lib/closure-compiler/contrib/externs/angular-1.3-http-promise.js 2 | --externs=chrome-lib/end-to-end/lib/closure-compiler/contrib/externs/angular-1.3-q.js 3 | --externs=chrome-lib/end-to-end/lib/closure-compiler/contrib/externs/angular-1.3.js 4 | --externs=chrome-lib/end-to-end/lib/closure-compiler/contrib/externs/chrome_extensions.js 5 | --js chrome-lib/end-to-end/lib/closure-library/closure/goog/deps.js 6 | --jscomp_error=accessControls 7 | --jscomp_error=ambiguousFunctionDecl 8 | --jscomp_error=checkDebuggerStatement 9 | --jscomp_error=checkEventfulObjectDisposal 10 | --jscomp_error=checkRegExp 11 | --jscomp_error=checkTypes 12 | --jscomp_error=checkVars 13 | --jscomp_error=const 14 | --jscomp_error=constantProperty 15 | --jscomp_error=duplicate 16 | --jscomp_error=duplicateMessage 17 | --jscomp_error=es3 18 | --jscomp_error=es5Strict 19 | --jscomp_error=externsValidation 20 | --jscomp_error=fileoverviewTags 21 | --jscomp_error=globalThis 22 | --jscomp_error=internetExplorerChecks 23 | --jscomp_error=invalidCasts 24 | --jscomp_error=missingProperties 25 | --jscomp_error=nonStandardJsDocs 26 | --jscomp_error=strictModuleDepCheck 27 | --jscomp_error=undefinedNames 28 | --jscomp_error=undefinedVars 29 | --jscomp_error=uselessCode 30 | --jscomp_error=visibility 31 | --jscomp_warning=deprecated 32 | --jscomp_warning=unknownDefines 33 | --language_in=ECMASCRIPT5 34 | --manage_closure_dependencies 35 | --only_closure_dependencies 36 | --js='!**_test.js' 37 | --js='!**_perf.js' -------------------------------------------------------------------------------- /chrome/components/appinfo/appinfo-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Service that holds basic information about 19 | * the application (e.g. its version.) 20 | */ 21 | goog.provide('e2email.components.appinfo.AppinfoService'); 22 | goog.provide('e2email.components.appinfo.module'); 23 | 24 | 25 | goog.scope(function() { 26 | 27 | 28 | 29 | /** 30 | * Service that provides access to basic information about 31 | * the application. 32 | * @param {!angular.$window} $window The angular $window service. 33 | * @param {!angular.$q} $q The angular $q service. 34 | * @ngInject 35 | * @constructor 36 | */ 37 | e2email.components.appinfo.AppinfoService = function($window, $q) { 38 | /** @private */ 39 | this.chrome_ = $window.chrome; 40 | /** @private */ 41 | this.q_ = $q; 42 | /** @private */ 43 | this.appVersion_ = this.chrome_.runtime.getManifest().version; 44 | /** @private {string|undefined} */ 45 | this.appPlatform_ = undefined; 46 | }; 47 | 48 | var AppinfoService = e2email.components.appinfo.AppinfoService; 49 | 50 | 51 | /** 52 | * Returns the current version of the application. 53 | * @return {string} 54 | */ 55 | AppinfoService.prototype.getVersion = function() { 56 | return this.appVersion_; 57 | }; 58 | 59 | 60 | /** 61 | * Returns information about the current platform. 62 | * @return {!angular.$q.Promise} 63 | */ 64 | AppinfoService.prototype.getPlatform = function() { 65 | if (goog.isDefAndNotNull(this.appPlatform_)) { 66 | return this.q_.when(this.appPlatform_); 67 | } 68 | var deferred = this.q_.defer(); 69 | this.chrome_.runtime.getPlatformInfo(goog.bind(function(info) { 70 | var platform = ''; 71 | if (goog.isDefAndNotNull(info['os'])) { 72 | platform = info['os']; 73 | } 74 | if (goog.isDefAndNotNull(info['arch'])) { 75 | platform = platform + ' ' + info['arch']; 76 | } 77 | this.appPlatform_ = platform; 78 | return deferred.resolve(platform); 79 | }, this)); 80 | return deferred.promise; 81 | }; 82 | 83 | 84 | /** 85 | * Angular module. 86 | * @type {!angular.Module} 87 | */ 88 | e2email.components.appinfo.module = angular 89 | .module('e2email.components.appinfo.AppinfoService', []) 90 | .service('appinfoService', AppinfoService); 91 | 92 | }); // goog.scope 93 | -------------------------------------------------------------------------------- /chrome/components/appinfo/appinfo-service_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the appinfo service. 19 | */ 20 | 21 | goog.require('e2email.components.appinfo.AppinfoService'); 22 | 23 | describe('AppinfoService', function() { 24 | var window_; 25 | var q_; 26 | var rootScope; 27 | var service; 28 | var TEST_VERSION = 'test-version'; 29 | var TEST_OS = 'test-os'; 30 | var TEST_ARCH = 'test-arch'; 31 | 32 | beforeEach(function() { 33 | inject(function($injector) { 34 | q_ = $injector.get('$q'); 35 | rootScope = $injector.get('$rootScope'); 36 | window_ = $injector.get('$window'); 37 | window_.chrome = { 38 | runtime: { 39 | getManifest: function() { 40 | return { 'version': TEST_VERSION }; 41 | }, 42 | getPlatformInfo: function(cb) { 43 | cb({'os': TEST_OS, 'arch': TEST_ARCH}); 44 | } 45 | } 46 | }; 47 | }); 48 | service = new e2email.components.appinfo.AppinfoService(window_, q_); 49 | }); 50 | 51 | it('should get the right version', function() { 52 | expect(service.getVersion()).toBe(TEST_VERSION); 53 | }); 54 | 55 | it('should get the right platform info', function() { 56 | var info = null; 57 | service.getPlatform().then(function(v) { 58 | info = v; 59 | }); 60 | expect(info).toBeNull(); 61 | rootScope.$apply(); 62 | expect(info).toBe(TEST_OS + ' ' + TEST_ARCH); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /chrome/components/auth/auth-service_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the auth service. 19 | */ 20 | 21 | goog.require('e2email.components.auth.AuthService'); 22 | 23 | describe('AuthService', function() { 24 | var window, q, log, http, service, token, httpBackend, rootScope; 25 | 26 | beforeEach(function() { 27 | inject(function($injector) { 28 | window = $injector.get('$window'); 29 | log = $injector.get('$log'); 30 | q = $injector.get('$q'); 31 | http = $injector.get('$http'); 32 | httpBackend = $injector.get('$httpBackend'); 33 | rootScope = $injector.get('$rootScope'); 34 | window.chrome = { 35 | identity: { 36 | getAuthToken: function(options, cb) { 37 | cb(token); 38 | }, 39 | removeCachedAuthToken: function(options, cb) { 40 | token = '*removed*'; 41 | cb(token); 42 | } 43 | }, 44 | runtime: { 45 | } 46 | }; 47 | }); 48 | service = new e2email.components.auth.AuthService(window, log, q, http); 49 | }); 50 | 51 | it('should retry requests with stale tokens', function() { 52 | // Ask the backend to return an "unauthorized" response on a 53 | // fake request we'll make. We'll return either a 400 or a 54 | // 200 depending on the token we get (which is passed in the url.) 55 | var FAKE_REQUEST = 'https://example.com/request'; 56 | // This request we'll reject. 57 | httpBackend.whenGET(FAKE_REQUEST + '/abc').respond(400, ''); 58 | // This one we'll accept. 59 | httpBackend.whenGET(FAKE_REQUEST + '/*removed*').respond(200, ''); 60 | 61 | // Store the tokens received by the operation. 62 | var tokens_received = []; 63 | var ok = false; 64 | var witherror = null; 65 | 66 | // Start off with a supposedly valid token. 67 | token = 'abc'; 68 | service.withAuth(null, true, function(access_token) { 69 | // save tokens so we can check what we got. 70 | tokens_received.push(access_token); 71 | return http.get(FAKE_REQUEST + '/' + access_token); 72 | }).then(function() { 73 | ok = true; 74 | }).catch(function(err) { 75 | witherror = err; 76 | }); 77 | rootScope.$apply(); 78 | httpBackend.flush(); 79 | httpBackend.verifyNoOutstandingRequest(); 80 | expect(witherror).toBeNull(); 81 | expect(ok).toBe(true); 82 | // We want to see the saved tokens to reflect we've made two 83 | // requests, and the second after removing the prior token from 84 | // the cache. 85 | expect(tokens_received).toEqual(['abc', '*removed*']); 86 | }); 87 | 88 | it('should remove cached tokens and revoke them', function() { 89 | token = 'xyz'; 90 | httpBackend.expectGET( 91 | 'https://accounts.google.com/o/oauth2/revoke?token=xyz').respond( 92 | 201, ''); 93 | var done = false; 94 | service.signOut().then(function() { 95 | done = true; 96 | }); 97 | rootScope.$apply(); 98 | httpBackend.flush(); 99 | 100 | httpBackend.verifyNoOutstandingExpectation(); 101 | expect(token).toEqual('*removed*'); 102 | expect(done).toBe(true); 103 | }); 104 | 105 | it('should add proper authorization headers', function() { 106 | var config = {}; 107 | service.addAuthorization(config, 'token'); 108 | expect(config.headers).toEqual({ 'Authorization': 'Bearer token'}); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /chrome/components/autocomplete/autocomplete-directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Angular directive providing a simple autocomplete tag. 19 | */ 20 | 21 | goog.provide('e2email.components.autocompletedirective'); 22 | goog.provide('e2email.components.autocompletedirective.module'); 23 | 24 | goog.require('goog.array'); 25 | goog.require('goog.events.KeyCodes'); 26 | 27 | goog.scope(function() { 28 | 29 | 30 | /** 31 | * A directive that provides autocomplete services for email addresses. 32 | * @param {!angular.JQLite} $document The angular $document service. 33 | * @param {!angular.$timeout} $timeout The angular $timeout service. 34 | * @param {!e2email.components.autocomplete.AutocompleteService} 35 | * autocompleteService that hosts a prioritized list of completions. 36 | * @return {!angular.Directive} The directive. 37 | * @ngInject 38 | */ 39 | e2email.components.autocompletedirective.autocompleteDirective = function( 40 | $document, $timeout, autocompleteService) { 41 | 42 | var link = function(scope, element, attrs) { 43 | scope['suggestions'] = []; 44 | scope['primarySuggestion'] = ''; 45 | scope['selectSuggestion'] = 0; 46 | var done = scope['done']; 47 | var jqInput = element.find('input'); 48 | var userInput = jqInput[0]; 49 | var nextInput = goog.array.find( 50 | element.parent().find('input'), function(input) { 51 | return (angular.element(input).hasClass('ac-tabbable')); 52 | }); 53 | var key = goog.events.KeyCodes; 54 | 55 | // Select specific suggestion from the list based on 56 | // user's up/down keystrokes 57 | var browseSuggestions = function(event) { 58 | // prevent overflow 59 | scope['selectSuggestion'] = 60 | scope['selectSuggestion'] % (scope['suggestions'].length); 61 | scope.$apply(function() { 62 | scope['primarySuggestion'] = 63 | scope['suggestions'][scope['selectSuggestion']]; 64 | }); 65 | event.preventDefault(); // keep the cursor at the end of the line 66 | }; 67 | 68 | // If we encounter the tab/enter characters, assume this is 69 | // equivalent to completing any existing suggestion. 70 | jqInput.unbind('keydown'); 71 | jqInput.bind('keydown', function(event) { 72 | if (event.keyCode === key.TAB || event.keyCode === key.ENTER) { 73 | scope['selectSuggestion'] = 0; 74 | var completion = scope['primarySuggestion']; 75 | if (goog.isString(completion) && (completion.length > 0)) { 76 | scope.$apply(function() { 77 | scope['ngModel'] = completion; 78 | }); 79 | } 80 | }else if (event.keyCode === key.DOWN) { 81 | if (scope['suggestions'].length != 0) { 82 | scope['selectSuggestion'] += 1; 83 | browseSuggestions(event); 84 | } 85 | }else if (event.keyCode === key.UP) { 86 | if (scope['suggestions'].length != 0) { 87 | // decrement, without becoming negative 88 | //(scope['selectSuggestion'] will eventually be 'mod'ed 89 | // by scope['suggestions'.length]) 90 | scope['selectSuggestion'] += (scope['suggestions'].length - 1); 91 | browseSuggestions(event); 92 | } 93 | }else { 94 | scope['selectSuggestion'] = 0; 95 | } 96 | return false; 97 | }); 98 | 99 | scope['onBlur'] = function() { 100 | scope['selectSuggestion'] = 0; 101 | // If there are no active suggestions, then use the contents 102 | // as-is. 103 | if (scope['suggestions'].length === 0) { 104 | done(); 105 | } else { 106 | // Blur occurred while there was an active selection menu. 107 | // This may happen if the menu itself is being selected. So, 108 | // wait for a bit, then decide what to do. 109 | $timeout(function() { 110 | if ($document[0].activeElement !== userInput) { 111 | // User is not in our text entry, so take its contents 112 | // as the desired value. 113 | scope['suggestions'] = []; 114 | scope['primarySuggestion'] = ''; 115 | done(); 116 | } 117 | }, 1000); 118 | } 119 | }; 120 | 121 | scope['clickSelectSuggestion'] = function(value) { 122 | if (goog.isDefAndNotNull(value)) { 123 | scope['ngModel'] = value; 124 | } 125 | if (goog.isDefAndNotNull(nextInput)) { 126 | nextInput.focus(); 127 | } 128 | }; 129 | 130 | scope.$watch('ngModel', function(value) { 131 | var primary = ''; 132 | var suggestions = []; 133 | if (goog.isString(value) && value.length > 0) { 134 | suggestions = autocompleteService.getCandidates(value); 135 | if (suggestions.length > 0) { 136 | primary = suggestions[0]; 137 | } 138 | } 139 | scope['suggestions'] = suggestions; 140 | scope['primarySuggestion'] = primary; 141 | }); 142 | }; 143 | 144 | return { 145 | restrict: 'E', 146 | scope: { 147 | acHint: '@', 148 | ngModel: '=', 149 | done: '&acDone' 150 | }, 151 | replace: true, 152 | templateUrl: 'components/autocomplete/autocomplete.html', 153 | link: link 154 | }; 155 | }; 156 | 157 | 158 | /** 159 | * Angular module. 160 | * @type {!angular.Module} 161 | */ 162 | e2email.components.autocompletedirective.module = angular 163 | .module( 164 | 'e2email.components.autocompletedirective.AutocompleteDirective', []) 165 | .directive( 166 | 'autocomplete', 167 | e2email.components.autocompletedirective.autocompleteDirective); 168 | }); // goog.scope 169 | -------------------------------------------------------------------------------- /chrome/components/autocomplete/autocomplete-directive_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the autocomplete directive. 19 | */ 20 | 21 | goog.require('e2email.components.autocomplete.module'); 22 | goog.require('e2email.components.autocompletedirective'); 23 | 24 | describe('AutocompleteDirective', function() { 25 | 26 | var $compile, $rootScope; 27 | var autocompleteService; 28 | 29 | beforeEach(module(e2email.components.autocomplete.module.name)); 30 | beforeEach(module(e2email.components.autocompletedirective.module.name)); 31 | beforeEach(module('components/autocomplete/autocomplete.html')); 32 | 33 | beforeEach(inject(function(_$compile_, _$rootScope_, _autocompleteService_) { 34 | $compile = _$compile_; 35 | $rootScope = _$rootScope_; 36 | autocompleteService = _autocompleteService_; 37 | })); 38 | 39 | it('Incorporates the correct suggestions', function() { 40 | var expectedText = ''; 41 | var i; 42 | for (i = 4; i >= 0; i--) { 43 | var candidate = 'candidate' + i; 44 | expectedText += candidate; 45 | autocompleteService.addCandidate(candidate, i); 46 | } 47 | var scope = $rootScope.$new(); 48 | scope['thing'] = 'cand'; 49 | var element = $compile( 50 | '' + 51 | '')(scope); 52 | $rootScope.$digest(); 53 | expect(element.text()).toContain(expectedText); 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /chrome/components/autocomplete/autocomplete-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Service that provides a prioritized list of 19 | * completions given a partially completed string. 20 | */ 21 | goog.provide('e2email.components.autocomplete.AutocompleteService'); 22 | goog.provide('e2email.components.autocomplete.module'); 23 | 24 | goog.require('goog.array'); 25 | goog.require('goog.asserts'); 26 | goog.require('goog.structs.Trie'); 27 | 28 | goog.scope(function() { 29 | 30 | 31 | 32 | /** 33 | * Service that manages a prioritized list of completion strings. 34 | * @ngInject 35 | * @constructor 36 | */ 37 | e2email.components.autocomplete.AutocompleteService = function() { 38 | /** 39 | * Trie that maintains all the candidates, and associates it 40 | * with its priority. (This is used primarily to obtain the list 41 | * of candidates, given a prefix.) 42 | * @private {!goog.structs.Trie} 43 | */ 44 | this.candidates_ = new goog.structs.Trie(); 45 | /** 46 | * Map that associates a candidate with its priority. (This is 47 | * maintained along with the trie to enable quicker priority lookups 48 | * for a given candidate.) 49 | * @private {!Object} 50 | */ 51 | this.candidatePriorities_ = {}; 52 | }; 53 | 54 | 55 | var AutocompleteService = e2email.components.autocomplete.AutocompleteService; 56 | 57 | 58 | /** 59 | * Adds a new candidate as a completion, and increments its priority 60 | * by the provided amount if it already exists. 61 | * @param {string} candidate A candidate completion. 62 | * @param {number} priority The priority for this candidate. 63 | */ 64 | AutocompleteService.prototype.addCandidate = function(candidate, priority) { 65 | var curPriority = priority; 66 | // Increment with any existing priority for this candidate. 67 | var existingPriority = this.candidatePriorities_[candidate]; 68 | if (goog.isNumber(existingPriority)) { 69 | curPriority += existingPriority; 70 | } 71 | // Add or update the entry in the trie and the lookup map. 72 | this.candidates_.set(candidate, curPriority); 73 | this.candidatePriorities_[candidate] = curPriority; 74 | }; 75 | 76 | 77 | /** 78 | * Returns a prioritized list of completions for a partially completed 79 | * string. 80 | * @param {string} partial The partially completed string. 81 | * @return {!Array} An array of candidate completions, with the 82 | * highest priority candidates at the beginning of the list. 83 | */ 84 | AutocompleteService.prototype.getCandidates = function(partial) { 85 | var candidateKeys = this.candidates_.getKeys(partial); 86 | // Remove candidates that are the same as the prefix. 87 | candidateKeys = candidateKeys.filter(function(candidate) { 88 | return (candidate !== partial); 89 | }); 90 | // Sort this array by decreasing order of priority. 91 | goog.array.sort(candidateKeys, goog.bind(function(candidate1, candidate2) { 92 | return goog.asserts.assertNumber(this.candidatePriorities_[candidate2]) - 93 | goog.asserts.assertNumber(this.candidatePriorities_[candidate1]); 94 | }, this)); 95 | return candidateKeys; 96 | }; 97 | 98 | 99 | /** 100 | * Angular module. 101 | * @type {!angular.Module} 102 | */ 103 | e2email.components.autocomplete.module = angular 104 | .module('e2email.components.autocomplete.AutocompleteService', []) 105 | .service('autocompleteService', AutocompleteService); 106 | 107 | }); // goog.scope 108 | -------------------------------------------------------------------------------- /chrome/components/autocomplete/autocomplete-service_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the autocomplete service. 19 | */ 20 | 21 | goog.require('e2email.components.autocomplete.AutocompleteService'); 22 | 23 | describe('AutocompleteService', function() { 24 | var service; 25 | 26 | beforeEach(function() { 27 | service = new e2email.components.autocomplete.AutocompleteService(); 28 | }); 29 | 30 | it('should prioritize candidates properly', function() { 31 | var i; 32 | for (i = 0; i < 5; i++) { 33 | service.addCandidate('candidate' + i, i); 34 | } 35 | var result = service.getCandidates('can'); 36 | expect(result.length).toBe(5); 37 | // Candidates should now be reverse sorted. 38 | for (i = 0; i < 5; i++) { 39 | expect(result[i]).toBe('candidate' + (4 - i)); 40 | } 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /chrome/components/autocomplete/autocomplete.css: -------------------------------------------------------------------------------- 1 | .email-autocomplete-area { 2 | position: relative; 3 | border: none; 4 | } 5 | 6 | .email-autocomplete-typeahead,.email-autocomplete-main { 7 | background-color: transparent; 8 | top: 0px; 9 | left: 0px; 10 | right: 0px; 11 | bottom: 0px; 12 | position: absolute; 13 | width: 100%; 14 | } 15 | 16 | .email-autocomplete-typeahead { 17 | color: #ccc; 18 | z-index: 0; 19 | padding: inherit; 20 | height: inherit; 21 | border: 1px solid transparent; 22 | } 23 | 24 | .email-autocomplete-main { 25 | z-index: 10; 26 | } 27 | 28 | .selected{ 29 | background-color: #e0e0e0 !important; 30 | } 31 | -------------------------------------------------------------------------------- /chrome/components/autocomplete/autocomplete.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /chrome/components/blobhref/blobhref-directive.js: -------------------------------------------------------------------------------- 1 | goog.provide('e2email.components.blobhrefdirective'); 2 | goog.provide('e2email.components.blobhrefdirective.module'); 3 | 4 | goog.require('goog.string'); 5 | 6 | 7 | goog.scope(function() { 8 | 9 | 10 | /** 11 | * Directive that puts a blob: URL into an href attribute. 12 | * Directive has to be used as blob: URLs are untrusted by Angular. 13 | * See https://docs.angularjs.org/api/ng/provider/$compileProvider 14 | * @return {!angular.Directive} 15 | */ 16 | e2email.components.blobhrefdirective.Directive = 17 | function() { 18 | return { 19 | link: function(scope, element, attrs) { 20 | if (goog.string.startsWith(attrs['blobHref'], 'blob:')) { 21 | angular.element(element[0]).attr('href', attrs['blobHref']); 22 | } 23 | }, 24 | restrict: 'A', 25 | }; 26 | }; 27 | 28 | 29 | /** 30 | * Angular module. 31 | * @type {!angular.Module} 32 | */ 33 | e2email.components.blobhrefdirective.module = angular 34 | .module( 35 | 'e2email.components.blobhrefdirective.FixDownloadLinkDirective', 36 | []) 37 | .directive( 38 | 'blobHref', 39 | e2email.components.blobhrefdirective.Directive); 40 | }); // goog.scope 41 | -------------------------------------------------------------------------------- /chrome/components/fileupload/fileupload-directive.js: -------------------------------------------------------------------------------- 1 | goog.provide('e2email.components.fileuploaddirective'); 2 | goog.provide('e2email.components.fileuploaddirective.module'); 3 | 4 | 5 | goog.scope(function() { 6 | 7 | 8 | /** 9 | * Directive to upload files. An input element with file type is created, which 10 | * binds clicks to invoke the upload function which requires a file as it's 11 | * element. 12 | * @return {!angular.Directive} 13 | */ 14 | e2email.components.fileuploaddirective.fileuploadDirective = function() { 15 | return { 16 | link: function(scope, element, attrs, ctrl) { 17 | var rootElement = element[0]; 18 | var inputAttachment = rootElement.lastChild; 19 | 20 | // Click the hidden input tag when the directive root element is clicked. 21 | rootElement.addEventListener('click', function(event) { 22 | inputAttachment.click(); 23 | }); 24 | 25 | // Clicking on the directive root element will trigger hidden input tag 26 | // to be activated, which isn't needed a second time. 27 | inputAttachment.addEventListener('click', function(event) { 28 | event.stopPropagation(); 29 | }); 30 | 31 | // Upload each file that the user has selected from the modal dialog. 32 | inputAttachment.addEventListener('change', function(event) { 33 | var files = event.target.files; 34 | 35 | angular.forEach(files, function(file) { 36 | var reader = new FileReader(); 37 | reader.onload = function(fileEvent) { 38 | scope.$apply(function() { 39 | scope.upload({'name': file.name, 'type': file.type, 40 | 'contents': new Uint8Array(fileEvent.target.result), 41 | 'size': file.size}); 42 | }); 43 | }; 44 | reader.readAsArrayBuffer(file); 45 | }); 46 | }); 47 | }, 48 | restrict: 'A', 49 | scope: { 50 | /** @export */ 51 | accept: '@asAccept', 52 | /** @export */ 53 | upload: '&asUpload' 54 | }, 55 | template: '' + 56 | '', 57 | transclude: true 58 | }; 59 | }; 60 | 61 | 62 | /** 63 | * Angular module. 64 | * @type {!angular.Module} 65 | */ 66 | e2email.components.fileuploaddirective.module = angular 67 | .module( 68 | 'e2email.components.fileuploaddirective.FileuploadDirective', []) 69 | .directive( 70 | 'asUpload', 71 | e2email.components.fileuploaddirective.fileuploadDirective); 72 | }); // goog.scope 73 | -------------------------------------------------------------------------------- /chrome/components/outerclick/outerclick-directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Angular directive providing an expression that 19 | * can be called when a click occurs outside a given element block. 20 | */ 21 | 22 | goog.provide('e2email.components.outerclick'); 23 | goog.provide('e2email.components.outerclick.module'); 24 | 25 | goog.scope(function() { 26 | 27 | 28 | /** 29 | * A directive that provides an attribute which evaluates an expression 30 | * if a click occurs outside its element block. 31 | * @param {!angular.JQLite} $document The angular $document service. 32 | * @return {!angular.Directive} The directive. 33 | * @ngInject 34 | */ 35 | e2email.components.outerclick.outerclickDirective = function($document) { 36 | 37 | var link = function(scope, element, attrs) { 38 | var handler = function(event) { 39 | if ((element.length > 0) && 40 | (event.target !== element[0]) && 41 | (goog.isFunction(element[0].contains)) && 42 | !element[0].contains(event.target)) { 43 | scope.$apply(attrs.outerclick); 44 | } 45 | }; 46 | $document.on('click', handler); 47 | scope.$on('$destroy', function() { 48 | $document.off('click', handler); 49 | }); 50 | }; 51 | 52 | return { 53 | restrict: 'A', 54 | link: link 55 | }; 56 | }; 57 | 58 | 59 | /** 60 | * Angular module. 61 | * @type {!angular.Module} 62 | */ 63 | e2email.components.outerclick.module = angular 64 | .module( 65 | 'e2email.components.outerclick.outerclickDirective', []) 66 | .directive( 67 | 'outerclick', 68 | e2email.components.outerclick.outerclickDirective); 69 | 70 | }); // goog.scope 71 | -------------------------------------------------------------------------------- /chrome/components/relativedate/relativedate-filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Angular filter that formats a date, also taking into 19 | * account how recent the date is. 20 | */ 21 | 22 | goog.provide('e2email.components.relativedate'); 23 | goog.provide('e2email.components.relativedate.module'); 24 | 25 | 26 | goog.scope(function() { 27 | 28 | 29 | /** 30 | * A filter that converts a Date into a suitable locale-specific 31 | * string, but also taking into account how recent the date is. Dates 32 | * that occur "today" show just the hour and minute. Dates that fall 33 | * in the same year are formatted with the day and month. All 34 | * other dates are formatted with their numerical day month and year. 35 | * 36 | * @param {!angular.$filter} $filter The Angular filter service. 37 | * @return {function (!Date): string} The filter. 38 | * @ngInject 39 | */ 40 | e2email.components.relativedate.dateFormatFilter = function($filter) { 41 | /** @type {function(!Date, string): string} */ 42 | var ngDateFilter = $filter('date'); 43 | 44 | return function(input) { 45 | // Verify the types as this comes from angularjs. 46 | if (!goog.isObject(input) || 47 | !(typeof input.getFullYear == 'function') || 48 | !(typeof input.getMonth == 'function') || 49 | !(typeof input.getDay == 'function')) { 50 | // Return an error message so we can track the issue. 51 | return 'relativedate expects a Date object.'; 52 | } 53 | 54 | var now = new Date(); 55 | 56 | // Return the 'shortDate' format if we're in different years. 57 | if (now.getFullYear() !== input.getFullYear()) { 58 | return ngDateFilter(input, 'shortDate'); 59 | } 60 | // Return the 'month day' format if this Date does not occur today. 61 | if ((now.getMonth() !== input.getMonth()) || 62 | (now.getDate() !== input.getDate())) { 63 | return ngDateFilter(input, 'MMM d'); 64 | } 65 | // Return the 'shortTime' format for Dates that occur today. 66 | return ngDateFilter(input, 'shortTime'); 67 | }; 68 | }; 69 | 70 | 71 | /** 72 | * Angular module. 73 | * @type {!angular.Module} 74 | */ 75 | e2email.components.relativedate.module = angular 76 | .module('e2email.components.relativedate.DateFormatFilter', []) 77 | .filter('relativedate', 78 | e2email.components.relativedate.dateFormatFilter); 79 | 80 | }); // goog.scope 81 | -------------------------------------------------------------------------------- /chrome/components/relativedate/relativedate-filter_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the relativedate filter. 19 | */ 20 | 21 | goog.require('e2email.components.relativedate'); 22 | 23 | describe('RelativeDate', function() { 24 | 25 | var mockFilter = function(name) { 26 | if (name === 'date') { 27 | return function(date, format) { 28 | // For testing purposes, just return the parameters 29 | // so we can check it was called correctly. 30 | return { 31 | 'date': date, 32 | 'format': format 33 | }; 34 | }; 35 | } else { 36 | return null; 37 | } 38 | }; 39 | 40 | var filter = e2email.components.relativedate.dateFormatFilter( 41 | mockFilter); 42 | 43 | it('should return short dates in different years', function() { 44 | var date = new Date(2001, 1, 1); 45 | expect(filter(date)).toEqual({ 46 | 'date': date, 47 | 'format': 'shortDate' 48 | }); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /chrome/components/storage/storage-service_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the storage service. 19 | */ 20 | 21 | goog.require('e2email.components.storage.module'); 22 | goog.require('goog.db'); 23 | 24 | describe('StorageService', function() { 25 | 26 | var storageService; 27 | var dbName = 'testDb'; 28 | var objectStoreCompose = [{name: 'contacts', indices: ['email', 29 | 'priority', 'name']}]; 30 | var storeName = objectStoreCompose[0].name; 31 | var keyField = objectStoreCompose[0].indices[0]; 32 | var additionalField = objectStoreCompose[0].indices[1]; 33 | var obj1 = {keyField: 'alice@google.com', additionalField: 'high'}; 34 | var obj2 = {keyField: 'bob@google.com', additionalField: 'low'}; 35 | var q, rootScope; 36 | var fakeDb = {}; 37 | var ans = ''; 38 | var dbHandle; 39 | 40 | 41 | beforeEach(module(e2email.components.storage.module.name)); 42 | 43 | beforeEach(function() { 44 | ans = ''; 45 | inject(function($injector) { 46 | q = $injector.get('$q'); 47 | rootScope = $injector.get('$rootScope'); 48 | storageService = $injector.get('storageService'); 49 | goog.db = { 50 | openDatabase: jasmine.createSpy().and.callFake(function(databaseName, 51 | version, upgradeCallback) { 52 | return goog.db; 53 | }), 54 | addCallbacks: jasmine.createSpy().and.callFake(function(resolve, 55 | reject, referenceToThis) { 56 | resolve(goog.db); 57 | }), 58 | addCallback: jasmine.createSpy().and.callFake(function(resolve) { 59 | resolve(goog.db); 60 | }), 61 | createTransaction: jasmine.createSpy().and.callFake( 62 | function(transactionName, mode) { 63 | return goog.db; 64 | }), 65 | wait: jasmine.createSpy().and.callFake( 66 | function() { 67 | return goog.db; 68 | }), 69 | Transaction: goog.db, 70 | TransactionMode: goog.db, 71 | READ_WRITE: goog.db, 72 | objectStore: jasmine.createSpy().and.callFake( 73 | function(objectStoreName) { 74 | return goog.db; 75 | }), 76 | put: jasmine.createSpy().and.callFake(function(obj, key) { 77 | fakeDb[key] = obj; 78 | return goog.db; 79 | }), 80 | get: jasmine.createSpy().and.callFake(function(obj) { 81 | ans = fakeDb[obj]; 82 | return goog.db; 83 | }), 84 | getAll: jasmine.createSpy().and.callFake(function() { 85 | ans = []; 86 | for (var i in fakeDb) { 87 | ans.push(fakeDb[i]); 88 | } 89 | return goog.db; 90 | }) 91 | }; 92 | }); 93 | }); 94 | 95 | 96 | // tests initializeDBAndObjectStores 97 | it('creates a new database and object store', function() { 98 | var res = ''; 99 | dbHandle = storageService.initializeDBAndObjectStores( 100 | dbName, objectStoreCompose); 101 | dbHandle.then(function() { 102 | res = 'created successfully'; 103 | }).catch(function() { 104 | res = 'failed'; 105 | }); 106 | rootScope.$apply(); 107 | expect(res).toBe('created successfully'); 108 | expect(goog.db.openDatabase).toHaveBeenCalled(); 109 | expect(goog.db.addCallbacks).toHaveBeenCalled(); 110 | }); 111 | 112 | 113 | // tests insertIntoObjectStore 114 | it('gets a database handle and inserts two objects', function() { 115 | var resInsert = ''; 116 | dbHandle = storageService.initializeDBAndObjectStores( 117 | dbName, objectStoreCompose); 118 | dbHandle.then(function(database) { 119 | storageService.insertIntoObjectStore(database, storeName, 120 | [{val: obj1, key: obj1.keyField}, {val: obj2, key: obj2.keyField}]); 121 | }).then(function() { 122 | resInsert = 'insertion successful'; 123 | }); 124 | rootScope.$apply(); 125 | expect(resInsert).toBe('insertion successful'); 126 | expect(goog.db.createTransaction).toHaveBeenCalled(); 127 | expect(goog.db.addCallbacks).toHaveBeenCalled(); 128 | }); 129 | 130 | 131 | // tests getKeyFromObjectStore 132 | it('gets a single key', function() { 133 | var resSingleGet = ''; 134 | dbHandle = storageService.initializeDBAndObjectStores( 135 | dbName, objectStoreCompose); 136 | dbHandle.then(function(database) { 137 | storageService.getKeyFromObjectStore(database, storeName, obj1.keyField); 138 | }).then(function() { 139 | resSingleGet = ans; 140 | }); 141 | rootScope.$apply(); 142 | expect(resSingleGet).toEqual(obj1); 143 | expect(goog.db.createTransaction).toHaveBeenCalled(); 144 | expect(goog.db.addCallbacks).toHaveBeenCalled(); 145 | }); 146 | 147 | 148 | // tests getAllKeysFromObjectStored 149 | it('gets all keys', function() { 150 | var resGetAll = ''; 151 | dbHandle = storageService.initializeDBAndObjectStores( 152 | dbName, objectStoreCompose); 153 | dbHandle.then(function(database) { 154 | storageService.getAllKeysFromObjectStore(database, storeName); 155 | }).then(function() { 156 | if ((ans[0] == obj1 || ans[0] == obj2) && 157 | (ans[1] == obj1 || ans[1] == obj2)) { 158 | resGetAll = 'Gets successful'; 159 | } 160 | }); 161 | rootScope.$apply(); 162 | expect(resGetAll).toBe('Gets successful'); 163 | expect(goog.db.createTransaction).toHaveBeenCalled(); 164 | expect(goog.db.addCallbacks).toHaveBeenCalled(); 165 | }); 166 | }); 167 | 168 | -------------------------------------------------------------------------------- /chrome/components/translate/translate-filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Angular filter providing access to Chrome's i18n API. 19 | */ 20 | 21 | goog.provide('e2email.components.translatefilter'); 22 | goog.provide('e2email.components.translatefilter.module'); 23 | 24 | 25 | goog.scope(function() { 26 | 27 | 28 | /** 29 | * A filter that converts a message key (or an array of strings 30 | * that starts with the key and is followed by substitution parameters) 31 | * into a translated string. 32 | * @param {!e2email.components.translate.TranslateService} translateService 33 | * The TranslateService instance that uses Chrome's i18n API. 34 | * @return {function ((string|!Array.)): string} The filter. 35 | * @ngInject 36 | */ 37 | e2email.components.translatefilter.translateFilter = function( 38 | translateService) { 39 | return function(input) { 40 | if (!goog.isDefAndNotNull(input)) { 41 | return ''; 42 | } 43 | if (goog.isString(input)) { 44 | return translateService.getMessage(input); 45 | } else if (goog.isArray(input) && input.length >= 1) { 46 | return translateService.getMessage(input[0], input.slice(1)); 47 | } else { 48 | return 'Invalid i18n key for translate filter.'; 49 | } 50 | }; 51 | }; 52 | 53 | 54 | /** 55 | * Angular module. 56 | * @type {!angular.Module} 57 | */ 58 | e2email.components.translatefilter.module = angular 59 | .module('e2email.components.translatefilter.TranslateFilter', []) 60 | .filter('translate', e2email.components.translatefilter.translateFilter); 61 | 62 | }); // goog.scope 63 | -------------------------------------------------------------------------------- /chrome/components/translate/translate-filter_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the translate filter. 19 | */ 20 | 21 | goog.require('e2email.components.translatefilter'); 22 | 23 | describe('TranslateFilter', function() { 24 | 25 | var TEST_KEY = 'test-key'; 26 | var TEST_RESULT = 'test-result'; 27 | var TEST_MULTI_RESULT = 'test-multi-result'; 28 | var TEST_ARG = 'test-arg'; 29 | 30 | var mockService = { 31 | getMessage: function(data, opt_arg) { 32 | if (data === TEST_KEY) { 33 | if (!goog.isDefAndNotNull(opt_arg)) { 34 | return TEST_RESULT; 35 | } else if (goog.isArray(opt_arg) && (opt_arg.length == 1) && 36 | (opt_arg[0] === TEST_ARG)) { 37 | return TEST_MULTI_RESULT; 38 | } else { 39 | return null; 40 | } 41 | } else { 42 | return null; 43 | } 44 | } 45 | }; 46 | 47 | var filter = e2email.components.translatefilter.translateFilter( 48 | mockService); 49 | 50 | it('should translate simple keys', function() { 51 | expect(filter(TEST_KEY)).toBe(TEST_RESULT); 52 | }); 53 | 54 | it('should translate keys with arguments', function() { 55 | expect(filter([TEST_KEY, TEST_ARG])).toBe(TEST_MULTI_RESULT); 56 | }); 57 | 58 | it('should return an empty string for undefined input', function() { 59 | expect(filter(undefined)).toBe(''); 60 | }); 61 | 62 | it('should catch unexpected input', function() { 63 | expect(filter(42)).toBe('Invalid i18n key for translate filter.'); 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /chrome/components/translate/translate-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Service providing access to Chrome's i18n API. 19 | */ 20 | goog.provide('e2email.components.translate.TranslateService'); 21 | goog.provide('e2email.components.translate.module'); 22 | 23 | 24 | goog.scope(function() { 25 | 26 | 27 | 28 | /** 29 | * Service that provides access to Chrome's i18n API. 30 | * @param {!angular.$window} $window The angular $window service. 31 | * @ngInject 32 | * @constructor 33 | */ 34 | e2email.components.translate.TranslateService = function($window) { 35 | /** @private */ 36 | this.chrome_ = $window.chrome; 37 | }; 38 | 39 | var TranslateService = e2email.components.translate.TranslateService; 40 | 41 | 42 | /** 43 | * Takes a translation key and optional substitution parameters, 44 | * and returns a translated string. If the key doesn't exist, it 45 | * will still return a string but with some error text to identify 46 | * the missing key. 47 | * @param {string} messageName A key for the translation message. 48 | * @param {(string|Array.)=} opt_args A string, or array 49 | * of strings to use for substitution parameters. 50 | * @return {string} 51 | */ 52 | TranslateService.prototype.getMessage = function(messageName, opt_args) { 53 | if (!goog.isDefAndNotNull(messageName)) { 54 | return 'No message key specified'; 55 | } 56 | var result = this.chrome_.i18n.getMessage(messageName, opt_args); 57 | if (goog.isDefAndNotNull(result)) { 58 | return result; 59 | } else { 60 | return 'Missing i18n key "' + messageName + '"'; 61 | } 62 | }; 63 | 64 | 65 | /** 66 | * Angular module. 67 | * @type {!angular.Module} 68 | */ 69 | e2email.components.translate.module = angular 70 | .module('e2email.components.translate.TranslateService', []) 71 | .service('translateService', TranslateService); 72 | 73 | }); // goog.scope 74 | -------------------------------------------------------------------------------- /chrome/components/translate/translate-service_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the translate service. 19 | */ 20 | 21 | goog.require('e2email.components.translate.TranslateService'); 22 | 23 | describe('TranslateService', function() { 24 | var window_; 25 | var service; 26 | var TEST_KEY = 'test-key'; 27 | var TEST_RESULT = 'test-result'; 28 | 29 | beforeEach(function() { 30 | inject(function($injector) { 31 | window_ = $injector.get('$window'); 32 | window_.chrome = { 33 | i18n: { 34 | getMessage: function(name) { 35 | if (name === TEST_KEY) { 36 | return TEST_RESULT; 37 | } else { 38 | return undefined; 39 | } 40 | } 41 | } 42 | }; 43 | }); 44 | service = new e2email.components.translate.TranslateService(window_); 45 | }); 46 | 47 | it('should translate valid keys', function() { 48 | expect(service.getMessage(TEST_KEY)).toBe(TEST_RESULT); 49 | }); 50 | 51 | it('should give a useful error for missing keys', function() { 52 | expect(service.getMessage('missing-key')).toBe( 53 | 'Missing i18n key "missing-key"'); 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /chrome/components/userinfo/userinfo-directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Angular directive that provides more detailed 19 | * information about a user as a popover. 20 | */ 21 | 22 | goog.provide('e2email.components.userinfo'); 23 | goog.provide('e2email.components.userinfo.module'); 24 | 25 | goog.scope(function() { 26 | 27 | 28 | /** 29 | * A directive that takes an email address and adds a popover to 30 | * reveal more detailed information about the user. 31 | * @param {!angular.$timeout} $timeout The angular $timeout service. 32 | * @param {!e2email.components.contacts.ContactsService} contactsService The 33 | * Contacts service. 34 | * @param {!e2email.components.translate.TranslateService} translateService 35 | * The translation service. 36 | * @return {!angular.Directive} The directive. 37 | * @ngInject 38 | */ 39 | e2email.components.userinfo.userinfoDirective = function( 40 | $timeout, contactsService, translateService) { 41 | 42 | var link = function(scope, element, attrs) { 43 | // Locate the popover div. 44 | var popover = angular.element(element.find('div')[1]); 45 | 46 | // And the main label section. 47 | var label = angular.element(element.find('b')[0]); 48 | 49 | var showing = false; 50 | 51 | scope['help'] = false; 52 | 53 | scope['maybeClose'] = function() { 54 | if (showing) { 55 | scope['toggle'](null); 56 | } 57 | }; 58 | 59 | scope['toggle'] = function(event) { 60 | scope['help'] = false; 61 | if (showing) { 62 | popover.removeClass('in'); 63 | // Remove the display after a timeout 64 | $timeout(function() { 65 | popover.removeClass('show'); 66 | showing = false; 67 | }, 200); 68 | } else { 69 | popover.addClass('show'); 70 | // Add the fade-in class in the next cycle. 71 | $timeout(function() { 72 | popover.addClass('in'); 73 | showing = true; 74 | }, 0); 75 | } 76 | if (goog.isDefAndNotNull(event)) { 77 | event.stopPropagation(); 78 | } 79 | }; 80 | 81 | scope['toggleHelp'] = function(event) { 82 | if (scope['help']) { 83 | scope['help'] = false; 84 | scope['toggle'](event); 85 | } else { 86 | scope['help'] = true; 87 | event.stopPropagation(); 88 | } 89 | }; 90 | 91 | scope.$watch('uiUser', function(newUser, oldUser) { 92 | scope['info'] = contactsService.users[newUser]; 93 | label.bind('click', scope['toggle']); 94 | }, false); 95 | }; 96 | 97 | 98 | return { 99 | restrict: 'E', 100 | scope: { 101 | uiUser: '=' 102 | }, 103 | replace: true, 104 | templateUrl: 'components/userinfo/userinfo.html', 105 | link: link 106 | }; 107 | }; 108 | 109 | 110 | /** 111 | * Angular module. 112 | * @type {!angular.Module} 113 | */ 114 | e2email.components.userinfo.module = angular 115 | .module( 116 | 'e2email.components.userinfo.UserinfoDirective', []) 117 | .directive( 118 | 'userinfo', 119 | e2email.components.userinfo.userinfoDirective); 120 | 121 | }); // goog.scope 122 | -------------------------------------------------------------------------------- /chrome/components/userinfo/userinfo-directive_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the userinfo filter. 19 | */ 20 | 21 | goog.require('e2email.components.userinfo.module'); 22 | 23 | describe('UserinfoDirective', function() { 24 | 25 | var $timeout, $compile, $rootScope; 26 | var mockContactsService, mockTranslateService, mockTimeout; 27 | var mockTranslateFilter; 28 | 29 | beforeEach(module(e2email.components.userinfo.module.name)); 30 | beforeEach(module('components/userinfo/userinfo.html')); 31 | 32 | beforeEach(function() { 33 | mockContactsService = { 34 | 'users': { 35 | 'test@example.com': { 36 | 'fingerprintHex': 'xyzabc' 37 | } 38 | } 39 | }; 40 | mockTranslateFilter = function(data) { 41 | return data.toString(); 42 | }; 43 | mockTranslateService = { 44 | 'getMessage': function(label) { 45 | return label; 46 | } 47 | }; 48 | mockTimeout = function(f, delay) { 49 | // Call it right away for testing purposes. 50 | f(); 51 | }; 52 | 53 | module(function($provide) { 54 | $provide.value('contactsService', mockContactsService); 55 | $provide.value('translateService', mockTranslateService); 56 | $provide.value('translateFilter', mockTranslateFilter); 57 | $provide.value('$timeout', mockTimeout); 58 | }); 59 | 60 | inject(function(_$compile_, _$rootScope_) { 61 | $compile = _$compile_; 62 | $rootScope = _$rootScope_; 63 | }); 64 | }); 65 | 66 | it('should load the fingerprint correctly.', function() { 67 | var scope = $rootScope.$new(); 68 | scope['theUser'] = 'test@example.com'; 69 | var element = angular.element(''); 70 | $compile(element)(scope); 71 | scope.$apply(); 72 | expect(angular.element(element.find('p')[0]).text()).toBe('xyzabc'); 73 | expect(angular.element(element.find('p')[1]).text()) 74 | .toContain('test@example.com'); 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /chrome/components/userinfo/userinfo.css: -------------------------------------------------------------------------------- 1 | .user-fingerprint { 2 | font-family: monospace; 3 | font-size: 0.8em; 4 | } 5 | -------------------------------------------------------------------------------- /chrome/components/userinfo/userinfo.html: -------------------------------------------------------------------------------- 1 |
2 | {{uiUser}} 3 |
4 |
5 |
6 |
7 |

ID Check

8 |

{{info.fingerprintHex}}

9 |

{{['fingerprintChangedStatus', uiUser] | translate}}

10 |

11 |

{{['fingerprintHelp1', uiUser] | translate}}

12 |

{{['fingerprintChangedHelp1', uiUser] | translate}}

13 |

{{'fingerprintHelp2' | translate}}

14 |

17 |

18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /chrome/components/userlist/userlist-directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Angular directive that converts an array of 19 | * email addresses into a suitable format that reveals 20 | * additional details about the address. 21 | */ 22 | 23 | goog.provide('e2email.components.userlist'); 24 | goog.provide('e2email.components.userlist.module'); 25 | 26 | goog.require('goog.array'); 27 | goog.require('goog.format.EmailAddress'); 28 | 29 | goog.scope(function() { 30 | 31 | 32 | /** 33 | * A directive that wraps an array of email addresses in a suitable 34 | * template to make them nicer to read, and show additional 35 | * information about the user. 36 | * @param {!e2email.components.gmail.GmailService} gmailService The 37 | * Gmail service. 38 | * @param {!e2email.components.translate.TranslateService} translateService 39 | * The translation service. 40 | * @return {!angular.Directive} The directive. 41 | * @ngInject 42 | */ 43 | e2email.components.userlist.userlistDirective = function( 44 | gmailService, translateService) { 45 | 46 | var link = function(scope, element, attrs) { 47 | 48 | scope['members'] = []; 49 | 50 | // Watch for any changes in the model, and update the scope as 51 | // needed. 52 | scope.$watch('ulMembers', function(newMembers, oldMembers) { 53 | var members = []; 54 | if (goog.isArray(newMembers)) { 55 | goog.array.forEach(newMembers, function(item) { 56 | var name = null; 57 | if (item !== gmailService.mailbox.email) { 58 | // Use the first part of the address, not including the 59 | // domain. 60 | var parsed = goog.format.EmailAddress.parse(item); 61 | if (parsed.isValid()) { 62 | var addr = parsed.getAddress(); 63 | if (addr.indexOf('@') > 0) { 64 | name = addr.substring(0, addr.indexOf('@')); 65 | } 66 | } 67 | } else { 68 | name = translateService.getMessage('me'); 69 | } 70 | if (goog.isDefAndNotNull(name)) { 71 | members.push({'displayName': name}); 72 | } 73 | }); 74 | } 75 | if (members.length === 0) { 76 | // Fallback to always adding me as a name. 77 | members.push({'displayName': translateService.getMessage('me')}); 78 | } 79 | scope['members'] = members; 80 | }, true); 81 | }; 82 | 83 | return { 84 | restrict: 'E', 85 | scope: { 86 | ulMembers: '=' 87 | }, 88 | replace: true, 89 | templateUrl: 'components/userlist/userlist.html', 90 | link: link 91 | }; 92 | }; 93 | 94 | 95 | /** 96 | * Angular module. 97 | * @type {!angular.Module} 98 | */ 99 | e2email.components.userlist.module = angular 100 | .module( 101 | 'e2email.components.userlist.UserlistDirective', []) 102 | .directive( 103 | 'userlist', 104 | e2email.components.userlist.userlistDirective); 105 | 106 | }); // goog.scope 107 | -------------------------------------------------------------------------------- /chrome/components/userlist/userlist-directive_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the userlist filter. 19 | */ 20 | 21 | goog.require('e2email.components.userlist.module'); 22 | 23 | describe('UserlistDirective', function() { 24 | 25 | var $compile, $rootScope; 26 | var mockGmailService, mockTranslateService; 27 | 28 | beforeEach(function() { 29 | mockGmailService = { 30 | 'mailbox': { 31 | 'email': 'myself@example.com' 32 | } 33 | }; 34 | mockTranslateService = { 35 | 'getMessage': function(label) { 36 | return label; 37 | } 38 | }; 39 | 40 | module(function($provide) { 41 | $provide.value('gmailService', mockGmailService); 42 | $provide.value('translateService', mockTranslateService); 43 | }); 44 | 45 | module(e2email.components.userlist.module.name); 46 | module('components/userlist/userlist.html'); 47 | 48 | inject(function(_$compile_, _$rootScope_) { 49 | $compile = _$compile_; 50 | $rootScope = _$rootScope_; 51 | }); 52 | }); 53 | 54 | it('should convert user email to the me label', function() { 55 | var scope = $rootScope.$new(); 56 | scope['emails'] = ['myself@example.com']; 57 | var element = angular.element(''); 58 | $compile(element)(scope); 59 | scope.$apply(); 60 | var innerScope = scope.$$childHead; 61 | expect(innerScope.members.length).toBe(1); 62 | expect(innerScope.members[0].displayName).toBe('me'); 63 | }); 64 | 65 | it('should convert multiple emails appropriately.', function() { 66 | var scope = $rootScope.$new(); 67 | scope['emails'] = ['someone@foo.com', 'myself@example.com']; 68 | var element = angular.element(''); 69 | $compile(element)(scope); 70 | scope.$apply(); 71 | var innerScope = scope.$$childHead; 72 | expect(innerScope.members.length).toBe(2); 73 | expect(innerScope.members[0].displayName).toBe('someone'); 74 | expect(innerScope.members[1].displayName).toBe('me'); 75 | }); 76 | 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /chrome/components/userlist/userlist.html: -------------------------------------------------------------------------------- 1 | 2 | {{member.displayName}}, 3 | 4 | -------------------------------------------------------------------------------- /chrome/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Provides constants used in the Safe Mail application. 19 | */ 20 | 21 | goog.provide('e2email.constants.Location'); 22 | 23 | 24 | /** 25 | * The locations of various views within the application. 26 | * @enum {string} 27 | */ 28 | e2email.constants.Location = { 29 | MESSAGES: '/messages', 30 | RECOVER: '/recover', 31 | RESET: '/reset', 32 | SETTINGS: '/settings', 33 | SETUP: '/setup', 34 | SHOWSECRET: '/showsecret', 35 | THREADS: '/threads', 36 | WELCOME: '/welcome', 37 | AUTHORIZATION: '/authorization', 38 | INTRODUCTION: '/introduction', 39 | GETSTARTED: '/getstarted' 40 | }; 41 | -------------------------------------------------------------------------------- /chrome/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | E2EMail 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /chrome/karma/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | 'basePath': '../', 4 | 'frameworks': [ 5 | 'jasmine' 6 | ], 7 | 'plugins': [ 8 | 'karma-jasmine', 9 | 'karma-ng-html2js-preprocessor', 10 | 'karma-html2js-preprocessor', 11 | 'karma-chrome-launcher' 12 | ], 13 | 'preprocessors': { 14 | '**/*.html': [ 15 | 'ng-html2js' 16 | ] 17 | }, 18 | 'usePolling': true, 19 | 'files': [ 20 | '../build/e2email/assets/js/angular.js', 21 | '../build/e2email/assets/js/angular-aria.js', 22 | '../build/e2email/assets/js/angular-animate.js', 23 | '../build/e2email/assets/js/angular-route.js', 24 | '../build/e2email/e2email_binary.js', 25 | 'karma/angular-mocks.js', 26 | '**/*_test.js', 27 | '**/*.html' 28 | ] 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "description": "__MSG_appDescription__", 4 | "version": "0.1.0", 5 | "manifest_version": 2, 6 | "minimum_chrome_version": "37", 7 | "oauth2": { 8 | "client_id": "442418751519-e81shoqhvd0oraabfkq141mbura3ceo3.apps.googleusercontent.com", 9 | "scopes": [ 10 | "email", 11 | "https://www.googleapis.com/auth/gmail.modify", 12 | "https://www.googleapis.com/auth/contacts.readonly" 13 | ] 14 | }, 15 | "default_locale": "en", 16 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy56O73gcL0+tu3FyR/SWAGFcRElilDlEhwT0hxhWrKG+z55JHNfDbj74pFu1xxLvM+YJOZv0+2NEPSQhMbDdjyvVom61UcCryG1vd6jP9PaXNwWHh41lbGeezfKeRQbHy22rFPyUSoQIIKMA/VNolZikoPKHr8HtK6V9f5eMIfgMdxFa9n+QT0eUj93nqpdQOOktGI5Rcp0OaA+i9YlrRkVrF7oSvxAT1TM+W3H5Nf79ieO1qjKRZMhhC0/GF0st5OpCZGYii/zYalMGlBIXJ/vCTgN7R3XC5FfNX+FA+aH7KOTYorlkWnTkKHaiPKAhWHt2rliCPuUwGbNWujVqwQIDAQAB", 17 | "icons": { 18 | "128": "assets/img/icon_128.png" 19 | }, 20 | "app": { 21 | "background": { 22 | "scripts": ["background.js"] 23 | } 24 | }, 25 | "permissions": [ 26 | "identity", 27 | "identity.email", 28 | "storage", 29 | "unlimitedStorage", 30 | "https://www.googleapis.com/", 31 | "https://www.google.com/", 32 | "https://accounts.google.com/", 33 | "https://hkpserverext.appspot.com/", 34 | "https://www.google-analytics.com/" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /chrome/models/mail/mail-model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Typedefs for a user's mailbox. 19 | */ 20 | 21 | goog.provide('e2email.models.mail.Attachment'); 22 | goog.provide('e2email.models.mail.Mail'); 23 | goog.provide('e2email.models.mail.Mailbox'); 24 | goog.provide('e2email.models.mail.Thread'); 25 | 26 | 27 | /** 28 | * This is the model for the attachments support. 29 | * The filename is the attachment's name, size is its size in bytes, 30 | * encoding is the encoding of the attachment, content is the string 31 | * with its content, type is the atttachment's type. 32 | * @typedef {{ 33 | * filename: string, 34 | * type: string, 35 | * encoding: string, 36 | * content: string, 37 | * size: number 38 | * }} 39 | */ 40 | e2email.models.mail.Attachment; 41 | 42 | 43 | /** 44 | * @typedef {{ 45 | * id: string, 46 | * subject: string, 47 | * from: string, 48 | * messageId: string, 49 | * to: !Array., 50 | * created: !Date, 51 | * warning: ?string, 52 | * unread: boolean, 53 | * status: ?string, 54 | * hasErrors: ?string, 55 | * mimeContent: ?Array.<{content: string, type: string, url: string, filename: string, filesize: number}> 56 | * }} 57 | */ 58 | e2email.models.mail.Mail; 59 | 60 | 61 | /** 62 | * This is the model for a Safe Mail user's mailbox. The email is the 63 | * user's email address, the threads represents all the mail threads 64 | * in the user's mailbox, and the status describes any (transient) 65 | * operations in the background. 66 | * @typedef {{ 67 | * email: string, 68 | * threads: !Array., 69 | * status: ?string 70 | * }} 71 | */ 72 | e2email.models.mail.Mailbox; 73 | 74 | 75 | /** 76 | * @typedef {{ 77 | * id: string, 78 | * subject: string, 79 | * updated: !Date, 80 | * from: string, 81 | * messageId: string, 82 | * participants: !Array., 83 | * to: !Array., 84 | * snippet: string, 85 | * mails: !Array., 86 | * unread: boolean, 87 | * isMarked: boolean 88 | * }} 89 | */ 90 | e2email.models.mail.Thread; 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /chrome/models/user/user-model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Typedefs for user information. 19 | */ 20 | 21 | goog.provide('e2email.models.user.User'); 22 | 23 | 24 | /** 25 | * @typedef {{ 26 | * email: string, 27 | * fingerprintChanged: boolean, 28 | * name: ?string, 29 | * fingerprintHex: ?string 30 | * }} 31 | */ 32 | e2email.models.user.User; 33 | -------------------------------------------------------------------------------- /chrome/pages/authorization/authorization-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview controller for the authorization page. 19 | */ 20 | goog.provide('e2email.pages.authorization.AuthorizationCtrl'); 21 | 22 | goog.scope(function() { 23 | 24 | 25 | 26 | /** 27 | * Authorization page controller. The view is launched if the application 28 | * discovers the user is not authorized to connect to Gmail but the user 29 | * is logged in to chrome, and 30 | * contains a method to launch the sign-in prcoess. If the sign-in flow 31 | * succeeds, it launches the setup view, otherwise it remains in this 32 | * view. 33 | * 34 | * @param {!angular.$location} $location the angular $location service. 35 | * @param {!e2email.components.translate.TranslateService} translateService 36 | * @param {!e2email.components.gmail.GmailService} gmailService 37 | * the gmail service. 38 | * @constructor 39 | * @ngInject 40 | * @export 41 | */ 42 | e2email.pages.authorization.AuthorizationCtrl = function( 43 | $location, translateService, gmailService) { 44 | /** @type {boolean} */ 45 | this.inProgress = false; 46 | /** 47 | * @private {!angular.$location} 48 | */ 49 | this.location_ = $location; 50 | /** 51 | * @private {!e2email.components.translate.TranslateService} 52 | */ 53 | this.translateService_ = translateService; 54 | /** 55 | * @private {!e2email.components.gmail.GmailService} 56 | */ 57 | this.gmailService_ = gmailService; 58 | /** 59 | * @type {?string} 60 | */ 61 | this.status = null; 62 | }; 63 | 64 | var AuthorizationCtrl = e2email.pages.authorization.AuthorizationCtrl; 65 | 66 | 67 | /** 68 | * Start the chrome OAuth signin-process. 69 | * @export 70 | */ 71 | AuthorizationCtrl.prototype.signIn = function() { 72 | this.inProgress = true; 73 | this.status = this.translateService_.getMessage('requestApprovalStatus'); 74 | 75 | this.gmailService_.signIn().then(goog.bind(function(approved) { 76 | if (approved) { 77 | this.status = null; 78 | this.location_.path('/setup'); 79 | } else { 80 | // stay where we are. 81 | this.status = this.translateService_.getMessage( 82 | 'approvalUnavailableStatus'); 83 | } 84 | }, this)).catch(goog.bind(function(err) { 85 | this.status = err.toString(); 86 | }, this)).finally(goog.bind(function() { 87 | this.inProgress = false; 88 | }, this)); 89 | }; 90 | 91 | 92 | /** 93 | * Returns the current status. 94 | * @return {?string} The status message. 95 | * @export 96 | */ 97 | AuthorizationCtrl.prototype.getStatus = function() { 98 | return this.status; 99 | }; 100 | 101 | 102 | /** 103 | * Returns whether there's an in-progress operation. 104 | * @return {boolean} returns true if there's an in-progress operation. 105 | * @export 106 | */ 107 | AuthorizationCtrl.prototype.isInProgress = function() { 108 | return this.inProgress; 109 | }; 110 | 111 | 112 | }); // goog.scope 113 | -------------------------------------------------------------------------------- /chrome/pages/authorization/authorization.css: -------------------------------------------------------------------------------- 1 | div.gmail-icon { 2 | padding-top: 60px; 3 | padding-bottom: 50px; 4 | height: 50px; 5 | } 6 | 7 | h1.authorization-title { 8 | padding-top: 22% !important; 9 | font-weight: bold; 10 | } 11 | -------------------------------------------------------------------------------- /chrome/pages/authorization/authorization.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |

{{'permissions' | translate}}

6 |
7 |
{{'authorizationHelp' | translate}}
8 |
9 |
10 |
11 |

12 | {{'accessEmail' | translate}} 13 |

14 |

{{'emailAccessWhy' | translate}}

15 |

16 | {{'deleteEmail' | translate}} 17 |

18 |

{{'deleteAccessWhy' | translate}}

19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |
{{authorizationCtrl.status}}
29 |
30 | -------------------------------------------------------------------------------- /chrome/pages/getstarted/getstarted-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview controller for the getstarted page. 19 | */ 20 | goog.provide('e2email.pages.getstarted.GetStartedCtrl'); 21 | 22 | goog.scope(function() { 23 | 24 | /** 25 | * Getstarted page controller 26 | * Redirects the user to the Sign in page after describing the app. 27 | * @param {!angular.$location} $location the angular $location service. 28 | * @param {!e2email.components.translate.TranslateService} translateService 29 | * @param {!e2email.components.gmail.GmailService} gmailService 30 | * the gmail service. 31 | * @constructor 32 | * @ngInject 33 | * @export 34 | */ 35 | 36 | e2email.pages.getstarted.GetStartedCtrl = function( 37 | $location, translateService, gmailService) { 38 | /** 39 | * @private {!angular.$location} 40 | */ 41 | this.location_ = $location; 42 | /** 43 | * @private {!e2email.components.gmail.GmailService} 44 | */ 45 | this.gmailService_ = gmailService; 46 | /** 47 | * @private {!e2email.components.translate.TranslateService} 48 | */ 49 | this.translateService_ = translateService; 50 | }; 51 | 52 | var GetStartedCtrl = e2email.pages.getstarted.GetStartedCtrl; 53 | 54 | 55 | /** 56 | * Redirects to the welcome view. 57 | * @export 58 | */ 59 | GetStartedCtrl.prototype.getEmailAddress = function() { 60 | this.gmailService_.getEmailAddress().then(goog.bind(function(email) { 61 | if (email == null) { 62 | this.location_.path(e2email.constants.Location.WELCOME); 63 | } else { 64 | this.location_.path(e2email.constants.Location.AUTHORIZATION); 65 | } 66 | }, this)); 67 | }; 68 | 69 | }); // goog.scope 70 | -------------------------------------------------------------------------------- /chrome/pages/getstarted/getstarted.css: -------------------------------------------------------------------------------- 1 | div.main-icon { 2 | padding-top: 30px; 3 | margin-bottom: 10px; 4 | padding-bottom: 10px; 5 | } 6 | 7 | div.intro-title { 8 | font-family: inherit; 9 | padding-top: 15%; 10 | } 11 | 12 | div.getstarted-format { 13 | font-size: 18px; 14 | padding-left: 35px; 15 | padding-right: 35px; 16 | } 17 | -------------------------------------------------------------------------------- /chrome/pages/getstarted/getstarted.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |

{{'endtoendProtection' | translate}}

6 |
7 |
{{'recipientInfo' | translate}}
8 |
9 |
10 | 11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /chrome/pages/introduction/introduction-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview controller for the introduction page. 19 | */ 20 | goog.provide('e2email.pages.introduction.IntroductionCtrl'); 21 | 22 | goog.scope(function() { 23 | 24 | 25 | 26 | /** 27 | *Introduction Controller 28 | * Is shown when the user does not have permissions and/or is not logged in. 29 | * The "proceed" button leads the user to the next introductiory page "Get 30 | * Started" 31 | * @param {!angular.$location} $location the angular $location service. 32 | * @param {!e2email.components.translate.TranslateService} translateService 33 | * @constructor 34 | * @ngInject 35 | * @export 36 | */ 37 | e2email.pages.introduction.IntroductionCtrl = function( 38 | $location, translateService) { 39 | /** 40 | * @private {!angular.$location} 41 | */ 42 | this.location_ = $location; 43 | /** 44 | * @private {!e2email.components.translate.TranslateService} 45 | */ 46 | this.translateService_ = translateService; 47 | }; 48 | 49 | var IntroductionCtrl = e2email.pages.introduction.IntroductionCtrl; 50 | 51 | 52 | /** 53 | * Redirects to the getstarted view. 54 | * @export 55 | */ 56 | IntroductionCtrl.prototype.proceed = function() { 57 | this.location_.path(e2email.constants.Location.GETSTARTED); 58 | }; 59 | 60 | }); // goog.scope 61 | -------------------------------------------------------------------------------- /chrome/pages/introduction/introduction.css: -------------------------------------------------------------------------------- 1 | div.main-icon { 2 | padding-top: 100px; 3 | padding-bottom: 70px; 4 | min-height: 50px; 5 | } 6 | 7 | div.context { 8 | min-height: 550px; 9 | } 10 | 11 | h1.maintext { 12 | color: white; 13 | font-weight: bold; 14 | padding-top: 55% !important; 15 | } 16 | 17 | div.intro-text { 18 | color: #F79D81; 19 | font-size: 18px; 20 | } 21 | -------------------------------------------------------------------------------- /chrome/pages/introduction/introduction.html: -------------------------------------------------------------------------------- 1 |
3 |
4 |
5 |
6 | 7 |
8 |

{{'appName' | translate}}

9 | 10 |
{{'usageDescription' | translate}}
11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /chrome/pages/messages/messages-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Controller for the messages view for a single thread. 19 | */ 20 | goog.provide('e2email.pages.messages.MessagesCtrl'); 21 | 22 | goog.require('e2email.constants.Location'); 23 | 24 | 25 | goog.scope(function() { 26 | 27 | 28 | /** 29 | * Milliseconds to wait for the compose window to fade into view, 30 | * before scrolling into view the entire window and selecting the 31 | * textarea. 32 | * @const 33 | * @private 34 | */ 35 | var SCROLL_INTO_VIEW_INTERVAL_ = 400; 36 | 37 | 38 | 39 | /** 40 | * Messages page controller. 41 | * 42 | * @param {!angular.Scope} $scope 43 | * @param {!angular.$location} $location The Angular location service. 44 | * @param {!angular.$routeParams} $routeParams The Angular route 45 | * parameter service. 46 | * @param {!e2email.components.gmail.GmailService} gmailService 47 | * the gmail service. 48 | * @constructor 49 | * @ngInject 50 | * @export 51 | */ 52 | e2email.pages.messages.MessagesCtrl = function( 53 | $scope, $location, $routeParams, gmailService) { 54 | 55 | /** @private */ 56 | this.location_ = $location; 57 | /** @private {string} */ 58 | this.threadId_ = $routeParams.threadId; 59 | /** @private */ 60 | this.gmailService_ = gmailService; 61 | 62 | this.status = null; 63 | 64 | this.thread = gmailService.getThread(this.threadId_); 65 | 66 | /** 67 | * Contains the state related to any replies by the user 68 | * for this thread. 69 | * @type {!{baseTitle: string, showText: boolean, content: ?string, attachments: Array}} 70 | */ 71 | this.reply = { 72 | 'baseTitle': 'reply', 73 | 'content': null, 74 | 'showText': false, 75 | 'attachments': [] 76 | }; 77 | 78 | // Run an async task to fetch/decrypt messages in this thread 79 | // once the view has loaded. 80 | $scope.$on('$viewContentLoaded', goog.bind(this.refresh_, this)); 81 | }; 82 | 83 | 84 | var MessagesCtrl = e2email.pages.messages.MessagesCtrl; 85 | 86 | 87 | /** 88 | * Requests the reply compose model to be reset. 89 | * @param {angular.Scope.Event=} opt_event The event that triggered this call 90 | * @export 91 | */ 92 | MessagesCtrl.prototype.cancelReply = function(opt_event) { 93 | if (goog.isDefAndNotNull(opt_event)) { 94 | opt_event.preventDefault(); 95 | } 96 | this.reply['baseTitle'] = 'reply'; 97 | this.reply['content'] = null; 98 | this.reply['showText'] = false; 99 | this.reply['attachments'] = []; 100 | }; 101 | 102 | 103 | /** 104 | * @param {string} name The name of the file. 105 | * @param {string} type The type of the file. 106 | * @param {string} contents The contents of the file. 107 | * @param {number} size The size of the file. 108 | * @export 109 | */ 110 | MessagesCtrl.prototype.onFileUpload = function(name, type, contents, size) { 111 | var obj = { 112 | 'filename': name, 113 | 'type': type, 114 | 'encoding': 'base64', 115 | 'content': contents, 116 | 'size': size 117 | }; 118 | this.reply.attachments.push(obj); 119 | }; 120 | 121 | 122 | /** 123 | * Removes the attachment object from the list of attachments. 124 | * @param {number} index Index of the file. 125 | * @export 126 | */ 127 | MessagesCtrl.prototype.removeObj = function(index) { 128 | this.reply.attachments.splice(index, 1); 129 | }; 130 | 131 | 132 | /** 133 | * Requests the reply compose model to be initialized, or 134 | * sent if it was already initialized. 135 | * @export 136 | */ 137 | MessagesCtrl.prototype.onReply = function() { 138 | if (!this.reply['showText']) { 139 | // Initialize the model. 140 | this.reply['showText'] = true; 141 | this.reply['baseTitle'] = 'send'; 142 | this.reply['content'] = null; 143 | this.reply['attachments'] = []; 144 | setTimeout(function() { 145 | document.querySelector('textarea').focus(); 146 | document.querySelector('#replyButton').scrollIntoView(); 147 | }, SCROLL_INTO_VIEW_INTERVAL_); 148 | } else if (goog.isDefAndNotNull(this.reply['content'])) { 149 | var messageLength = this.reply['content'].length; 150 | this.gmailService_.encryptAndSendMail( 151 | this.thread.to, this.threadId_, this.thread.messageId, 152 | this.thread.subject, this.reply['content'], this.reply['attachments']). 153 | then(goog.bind(function() { 154 | return this.gmailService_.refreshThread(this.threadId_); 155 | }, this)).then(goog.bind(this.cancelReply, this)); 156 | } 157 | }; 158 | 159 | 160 | /** 161 | * Redirects the application to the thread list view. 162 | * @export 163 | */ 164 | MessagesCtrl.prototype.showThreads = function() { 165 | this.location_.path(e2email.constants.Location.THREADS); 166 | }; 167 | 168 | 169 | /** 170 | * Returns the message thread object within the controller. 171 | * @return {?e2email.models.mail.Thread} 172 | * @export 173 | */ 174 | MessagesCtrl.prototype.getMessages = function() { 175 | return this.thread; 176 | }; 177 | 178 | 179 | /** 180 | * Returns the thread reply object within the controller. 181 | * @return {!{baseTitle: string, showText: boolean, content: ?string}} 182 | * @export 183 | */ 184 | MessagesCtrl.prototype.getReply = function() { 185 | return this.reply; 186 | }; 187 | 188 | 189 | /** 190 | * Runs an async task to fetch or decrypt messages in this thread. 191 | * @private 192 | */ 193 | MessagesCtrl.prototype.refresh_ = function() { 194 | this.gmailService_.refreshThread(this.threadId_).catch(function(issue) { 195 | if (goog.isDefAndNotNull(issue.message)) { 196 | this.errors = issue.message; 197 | } else { 198 | this.errors = issue.toString(); 199 | } 200 | }); 201 | }; 202 | 203 | 204 | }); // goog.scope 205 | -------------------------------------------------------------------------------- /chrome/pages/messages/messages-controller_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the messages page controller. 19 | */ 20 | 21 | goog.require('e2email.pages.messages.MessagesCtrl'); 22 | 23 | describe('MessagesCtrl', function() { 24 | var q, controller, messagesController, scope, location; 25 | var mockGmailService, mockWindowService, mockTranslateService; 26 | var TEST_EMAIL = 'mail@example.com'; 27 | var TEST_SUBJECT = 'test subject'; 28 | var TEST_THREAD_ID = 'test-thread-id'; 29 | var TEST_MESSAGE_ID = 'test-message-id'; 30 | var TEST_CONTENT = 'test content'; 31 | var TEST_FILENAME = 'dull'; 32 | var TEST_TYPE = 'dull'; 33 | var TEST_ENCODING = 'base64'; 34 | var TEST_CONTENT_ATTACHMENT = 'dull'; 35 | var TEST_SIZE = 12; 36 | var TEST_ATTACHMENTS = [{'filename': TEST_FILENAME, 'type': TEST_TYPE, 37 | 'encoding': TEST_ENCODING, 'content': TEST_CONTENT_ATTACHMENT, 38 | 'size': TEST_SIZE}]; 39 | 40 | var TEST_THREAD = { 41 | 'to': [TEST_EMAIL], 42 | 'messageId': TEST_MESSAGE_ID, 43 | 'subject': TEST_SUBJECT 44 | }; 45 | 46 | beforeEach(module(function($controllerProvider) { 47 | $controllerProvider.register( 48 | 'MessagesCtrl', e2email.pages.messages.MessagesCtrl); 49 | })); 50 | 51 | beforeEach(inject(function($q, $rootScope, $controller, $location) { 52 | q = $q; 53 | scope = $rootScope.$new(); 54 | controller = $controller; 55 | location = $location; 56 | 57 | mockWindowService = { 58 | open: function(url, name, options) { 59 | }, 60 | document: { 61 | title: 'not set' 62 | } 63 | }; 64 | 65 | mockGmailService = { 66 | refresh: function(force, progress) { 67 | return q.when(undefined); 68 | }, 69 | refreshThread: function(threadId) { 70 | if (threadId === TEST_THREAD_ID) { 71 | return q.when(undefined); 72 | } else { 73 | return q.reject('bad parameters'); 74 | } 75 | }, 76 | encryptAndSendMail: function( 77 | recipients, threadId, messageId, subject, content, attachments) { 78 | if ((recipients.length === 1) && (recipients[0] === TEST_EMAIL) && 79 | (threadId == TEST_THREAD_ID) && (messageId === TEST_MESSAGE_ID) && 80 | (subject === TEST_SUBJECT) && (content === TEST_CONTENT) && 81 | (attachments === TEST_ATTACHMENTS)) { 82 | return q.when(undefined); 83 | } else { 84 | return q.reject('bad parameters'); 85 | } 86 | }, 87 | getThread: function(threadId) { 88 | if (threadId === TEST_THREAD_ID) { 89 | return TEST_THREAD; 90 | } else { 91 | return null; 92 | } 93 | }, 94 | mailbox: { 95 | email: TEST_EMAIL 96 | } 97 | }; 98 | })); 99 | 100 | it('should refresh thread correctly when requested', function() { 101 | spyOn(mockGmailService, 'refreshThread').and.callThrough(); 102 | messagesController = controller( 103 | 'MessagesCtrl as messagesCtrl', { 104 | $scope: scope, 105 | $location: location, 106 | $routeParams: { 'threadId': TEST_THREAD_ID }, 107 | gmailService: mockGmailService 108 | }); 109 | 110 | messagesController.refresh_(); 111 | // Resolve promises. 112 | scope.$digest(); 113 | // Check expected calls were made 114 | expect(mockGmailService.refreshThread).toHaveBeenCalled(); 115 | expect(messagesController.thread).toEqual(TEST_THREAD); 116 | }); 117 | 118 | it('should encrypt and send an initialized reply model', function() { 119 | spyOn(mockGmailService, 'encryptAndSendMail').and.callThrough(); 120 | spyOn(mockGmailService, 'refreshThread').and.callThrough(); 121 | messagesController = controller( 122 | 'MessagesCtrl as messagesCtrl', { 123 | $scope: scope, 124 | $location: location, 125 | $routeParams: { 'threadId': TEST_THREAD_ID }, 126 | gmailService: mockGmailService 127 | }); 128 | 129 | messagesController.refresh_(); 130 | scope.$digest(); 131 | 132 | messagesController.reply['showText'] = true; 133 | messagesController.reply['content'] = TEST_CONTENT; 134 | messagesController.reply['attachments'] = TEST_ATTACHMENTS; 135 | messagesController.onReply(); 136 | // Resolve promises. 137 | scope.$digest(); 138 | // Check expected calls were made 139 | expect(mockGmailService.encryptAndSendMail).toHaveBeenCalled(); 140 | expect(mockGmailService.refreshThread).toHaveBeenCalled(); 141 | // check reply model is reset. 142 | expect(messagesController.reply['content']).toBeNull(); 143 | expect(messagesController.reply['showText']).toBe(false); 144 | }); 145 | 146 | it('should create an object with the properties and add it to attachments', 147 | function() { 148 | messagesController = controller( 149 | 'MessagesCtrl as messagesCtrl', { 150 | $scope: scope, 151 | $location: location, 152 | $window: mockWindowService, 153 | gmailService: mockGmailService, 154 | $routeParams: { 'threadId': TEST_THREAD_ID } 155 | }); 156 | messagesController.onFileUpload(TEST_FILENAME, TEST_TYPE, 157 | TEST_CONTENT_ATTACHMENT, TEST_SIZE); 158 | 159 | // Verify attachments to have been updated. 160 | expect(messagesController.reply['attachments']). 161 | toEqual(TEST_ATTACHMENTS); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /chrome/pages/messages/messages.css: -------------------------------------------------------------------------------- 1 | .email-detail-entry { 2 | border-bottom: 1px solid rgba(0,0,0,0.04); 3 | margin-bottom: 1em; 4 | } 5 | 6 | .email-detail-from .glyphicon { 7 | font-size: 10px; 8 | padding-left: 0.25em; 9 | } 10 | 11 | .email-detail-reply-form { 12 | margin-top: 1em; 13 | margin-bottom: 2em; 14 | } 15 | 16 | .email-detail-reply-form button { 17 | margin-top: 1em; 18 | margin-left: 1em; 19 | } 20 | 21 | .email-subject { 22 | font-family: Roboto; 23 | color: #505050; 24 | } 25 | 26 | .email-mime-entry { 27 | margin-top: 1em; 28 | margin-bottom: 1em; 29 | } 30 | 31 | .email-text-content { 32 | white-space: pre-line; 33 | } 34 | 35 | .email-image-content { 36 | width: 100%; 37 | } 38 | 39 | .email-unsupported-content { 40 | border: 1px solid rgba(0,0,0,0.25); 41 | white-space: pre-line; 42 | } 43 | 44 | .email-error-content { 45 | border: 1px solid rgba(0,0,0,0.25); 46 | white-space: pre-line; 47 | } 48 | 49 | .single-attachment-received { 50 | background-color: #f5f5f5; 51 | border: 1px solid #dcdcdc; 52 | font-weight: bold; 53 | font-size: 13px; 54 | margin: 0 7px 6px; 55 | color: #428bca !important; 56 | overflow-y: hidden; 57 | text-align: left; 58 | padding: 2px 6px; 59 | max-width: 448px; 60 | width: 80%; 61 | height: 23px; 62 | margin-bottom: -5px; 63 | } 64 | 65 | .file-description { 66 | color: #1155CC !important; 67 | margin: 0px; 68 | padding:0px; 69 | overflow-y: hidden; 70 | width:60%; 71 | } 72 | 73 | .file-size { 74 | width:30%; 75 | overflow-y:hidden; 76 | } 77 | -------------------------------------------------------------------------------- /chrome/pages/messages/messages.html: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 | 17 |
18 |
    19 | 46 |
47 | 48 | 69 |
70 | -------------------------------------------------------------------------------- /chrome/pages/recover/recover-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Controller for the key recovery page. 19 | */ 20 | goog.provide('e2email.pages.recover.RecoverCtrl'); 21 | 22 | goog.require('e2email.constants.Location'); 23 | 24 | goog.scope(function() { 25 | 26 | 27 | 28 | /** 29 | * Recover page controller. 30 | * 31 | * @param {!angular.$log} $log The Angular $log service. 32 | * @param {!angular.$location} $location The Angular $location service. 33 | * @param {!e2email.components.translate.TranslateService} translateService 34 | * The Translate service. 35 | * @param {!e2email.components.openpgp.OpenPgpService} openpgpService 36 | * The OpenPGP service. 37 | * @param {!e2email.components.gmail.GmailService} gmailService 38 | * The Gmail service. 39 | * @param {!e2email.components.auth.AuthService} authService 40 | * The Authentication service. 41 | * @constructor 42 | * @ngInject 43 | * @export 44 | */ 45 | e2email.pages.recover.RecoverCtrl = function( 46 | $log, $location, translateService, openpgpService, gmailService, 47 | authService) { 48 | /** @private */ 49 | this.log_ = $log; 50 | /** @private */ 51 | this.location_ = $location; 52 | /** @private */ 53 | this.translateService_ = translateService; 54 | /** @private */ 55 | this.openpgpService_ = openpgpService; 56 | /** @private */ 57 | this.gmailService_ = gmailService; 58 | /** @private */ 59 | this.authService_ = authService; 60 | /** @type {?string} */ 61 | this.recoveryCode1 = null; 62 | /** @type {?string} */ 63 | this.recoveryCode2 = null; 64 | /** @type {?string} */ 65 | this.recoveryCode3 = null; 66 | /** @type {?string} */ 67 | this.recoveryCode4 = null; 68 | /** @type {?string} */ 69 | this.recoveryCode5 = null; 70 | /** @type {?string} */ 71 | this.errors = null; 72 | /** @type {?string} */ 73 | this.status = null; 74 | }; 75 | 76 | var RecoverCtrl = e2email.pages.recover.RecoverCtrl; 77 | 78 | 79 | /** 80 | * Regular expression that selects for valid base58 encoding 81 | * characters, but includes spaces. 82 | * @const 83 | * @export 84 | */ 85 | RecoverCtrl.prototype.REGEX_BASE58 = /^[1-9A-HJ-NP-Za-km-z ]+$/; 86 | 87 | 88 | /** 89 | * Goes into the reset account flow. 90 | * @export 91 | */ 92 | RecoverCtrl.prototype.reset = function() { 93 | this.location_.path(e2email.constants.Location.RESET); 94 | }; 95 | 96 | 97 | /** 98 | * Returns any current error message. 99 | * @return {?string} 100 | * @export 101 | */ 102 | RecoverCtrl.prototype.getErrors = function() { 103 | return this.errors; 104 | }; 105 | 106 | 107 | /** 108 | * Uses the provided code to recover the keyring. 109 | * @export 110 | */ 111 | RecoverCtrl.prototype.unlock = function() { 112 | if (!goog.isDefAndNotNull(this.recoveryCode1)) { 113 | this.errors = this.translateService_.getMessage('missingCodeStatus'); 114 | return; 115 | } else if (!goog.isDefAndNotNull(this.recoveryCode2)) { 116 | this.errors = this.translateService_.getMessage('missingCodeStatus'); 117 | return; 118 | } else if (!goog.isDefAndNotNull(this.recoveryCode3)) { 119 | this.errors = this.translateService_.getMessage('missingCodeStatus'); 120 | return; 121 | } else if (!goog.isDefAndNotNull(this.recoveryCode4)) { 122 | this.errors = this.translateService_.getMessage('missingCodeStatus'); 123 | return; 124 | } else if (!goog.isDefAndNotNull(this.recoveryCode5)) { 125 | this.errors = this.translateService_.getMessage('missingCodeStatus'); 126 | return; 127 | } 128 | 129 | this.status = this.translateService_.getMessage('checkingCodeStatus'); 130 | this.errors = null; 131 | // Removes all spaces 132 | var code = this.recoveryCode1.replace(/\s+/g, '') + 133 | this.recoveryCode2.replace(/\s+/g, '') + 134 | this.recoveryCode3.replace(/\s+/g, '') + 135 | this.recoveryCode4.replace(/\s+/g, '') + 136 | this.recoveryCode5.replace(/\s+/g, ''); 137 | this.authService_.getIdentityToken().then(goog.bind(function(idtoken) { 138 | return this.openpgpService_.restoreFromSecretBackupCode( 139 | code, this.gmailService_.mailbox.email, idtoken, this); 140 | }, this)).then(goog.bind(function() { 141 | // Sends a notification on a successful recovery. 142 | this.status = this.translateService_.getMessage('sendNotificationStatus'); 143 | return this.gmailService_.sendNewDeviceMail(); 144 | }, this)).then(goog.bind(function() { 145 | // And continues to the main application view. 146 | this.location_.path(e2email.constants.Location.THREADS); 147 | }, this)).catch(goog.bind(function(err) { 148 | this.log_.info(err); 149 | if (goog.isDefAndNotNull(err.message)) { 150 | this.errors = err.message; 151 | } else { 152 | this.errors = err.toString(); 153 | } 154 | }, this)).finally(goog.bind(function() { 155 | this.status = null; 156 | }, this)); 157 | }; 158 | 159 | }); // goog.scope 160 | -------------------------------------------------------------------------------- /chrome/pages/recover/recover-controller_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the recovery page controller. 19 | */ 20 | 21 | goog.require('e2email.pages.recover.RecoverCtrl'); 22 | 23 | describe('RecoverCtrl', function() { 24 | var q, controller, recoverController, scope, location; 25 | var mockTranslateService, mockOpenpgpService, mockGmailService, 26 | mockAuthService, mockAuthService; 27 | var TEST_TOKEN = 'idtoken'; 28 | var TEST_CODE = 'test-code'; 29 | var TEST_CODE_COMPLETE = TEST_CODE + TEST_CODE + 30 | TEST_CODE + TEST_CODE + TEST_CODE; 31 | var TEST_EMAIL = 'mail@example.com'; 32 | var TEST_KEY = 'test-key'; 33 | 34 | beforeEach(module(function($controllerProvider) { 35 | $controllerProvider.register( 36 | 'RecoverCtrl', e2email.pages.recover.RecoverCtrl); 37 | })); 38 | 39 | beforeEach(inject(function($rootScope, $controller, $location, $q) { 40 | scope = $rootScope.$new(); 41 | controller = $controller; 42 | location = $location; 43 | q = $q; 44 | 45 | mockTranslateService = { 46 | getMessage: function(m) { return 'dummy'; } 47 | }; 48 | mockOpenpgpService = { 49 | restoreFromSecretBackupCode: function(code, email, idtoken) { 50 | if ((code === TEST_CODE_COMPLETE) && (email === TEST_EMAIL) && 51 | (idtoken === TEST_TOKEN)) { 52 | return q.when(TEST_KEY); 53 | } else { 54 | return q.reject('bad code'); 55 | } 56 | } 57 | }; 58 | 59 | mockGmailService = { 60 | mailbox: { 61 | email: TEST_EMAIL 62 | }, 63 | sendNewDeviceMail: function() { 64 | return q.when(undefined); 65 | } 66 | }; 67 | mockAuthService = { 68 | getIdentityToken: function() { 69 | return q.when(TEST_TOKEN); 70 | } 71 | }; 72 | })); 73 | 74 | it('should perform all unlock steps', function() { 75 | spyOn(mockGmailService, 'sendNewDeviceMail').and.callThrough(); 76 | spyOn(mockOpenpgpService, 'restoreFromSecretBackupCode').and.callThrough(); 77 | recoverController = controller( 78 | 'RecoverCtrl as recoverCtrl', { 79 | $scope: scope, 80 | $location: location, 81 | gmailService: mockGmailService, 82 | translateService: mockTranslateService, 83 | openpgpService: mockOpenpgpService, 84 | authService: mockAuthService 85 | }); 86 | recoverController.recoveryCode1 = TEST_CODE; 87 | recoverController.recoveryCode2 = TEST_CODE; 88 | recoverController.recoveryCode3 = TEST_CODE; 89 | recoverController.recoveryCode4 = TEST_CODE; 90 | recoverController.recoveryCode5 = TEST_CODE; 91 | // Unlock using code. 92 | recoverController.unlock(); 93 | scope.$digest(); 94 | // Just check all interesting service methods were called. 95 | expect(mockOpenpgpService.restoreFromSecretBackupCode).toHaveBeenCalled(); 96 | expect(mockGmailService.sendNewDeviceMail).toHaveBeenCalled(); 97 | // And that we landed at the right end-point. 98 | expect(location.path()).toBe('/threads'); 99 | }); 100 | 101 | 102 | }); 103 | -------------------------------------------------------------------------------- /chrome/pages/recover/recover.css: -------------------------------------------------------------------------------- 1 | div.resetbtn { 2 | margin-bottom: 28px; 3 | } 4 | 5 | div.recover-box { 6 | min-height: 120px; 7 | padding: 10px; 8 | border: 2px solid #ECC67B; 9 | background-color: #F9EDBE; 10 | } 11 | 12 | div.recover-help { 13 | margin-bottom: 39px; 14 | padding-left: 45px; 15 | padding-right: 45px; 16 | } 17 | 18 | input.input-box { 19 | font-family: monospace; 20 | border-radius: 0px; 21 | border: none; 22 | height: 50px; 23 | width: 5em; 24 | margin-left: 2px; 25 | margin-left: 2px; 26 | display: inline-block; 27 | } 28 | 29 | div.info-format { 30 | margin-bottom: 12px; 31 | color: #313339; 32 | font-weight: bold; 33 | } 34 | -------------------------------------------------------------------------------- /chrome/pages/recover/recover.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

{{'recoverScreenTitle' | translate}}

5 | 6 |
{{'recoverHelp' | translate}}
7 | 8 |
9 |
10 |
{{'recoveryCodeInput' | translate}}
11 |
12 | 15 | 16 | 19 | 20 | 23 | 24 | 27 | 28 | 31 |
32 | {{'incorrectCodeStatus' | translate}} 34 | 35 |
36 |
37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
47 | {{recoverCtrl.status}} 49 |
50 |
51 | 52 |
53 |
54 | {{recoverCtrl.errors}} 56 |
57 |
58 | -------------------------------------------------------------------------------- /chrome/pages/reset/reset-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Controller for the reset account view. 19 | */ 20 | goog.provide('e2email.pages.reset.ResetCtrl'); 21 | 22 | goog.require('e2email.constants.Location'); 23 | 24 | goog.scope(function() { 25 | 26 | 27 | 28 | /** 29 | * Reset page controller. 30 | * 31 | * @param {!angular.$location} $location angular location service. 32 | * @param {!e2email.components.translate.TranslateService} translateService 33 | * @param {!e2email.components.gmail.GmailService} gmailService 34 | * @param {!e2email.components.openpgp.OpenPgpService} openpgpService 35 | * @param {!e2email.components.auth.AuthService} authService 36 | * The Authentication service. 37 | * @constructor 38 | * @ngInject 39 | * @export 40 | */ 41 | e2email.pages.reset.ResetCtrl = function( 42 | $location, translateService, gmailService, openpgpService, authService) { 43 | /** @private */ 44 | this.location_ = $location; 45 | /** @private */ 46 | this.translateService_ = translateService; 47 | /** @private */ 48 | this.gmailService_ = gmailService; 49 | /** @private */ 50 | this.openpgpService_ = openpgpService; 51 | /** @private */ 52 | this.authService_ = authService; 53 | /** @type {?string} */ 54 | this.status = null; 55 | /** 56 | * Controller sets this to true when there are any in-progress 57 | * background operations. 58 | * @type {boolean} 59 | */ 60 | this.inProgress = false; 61 | }; 62 | 63 | var ResetCtrl = e2email.pages.reset.ResetCtrl; 64 | 65 | 66 | /** 67 | * Goes back to the recover account flow. 68 | * @export 69 | */ 70 | ResetCtrl.prototype.cancel = function() { 71 | this.location_.path(e2email.constants.Location.RECOVER); 72 | }; 73 | 74 | 75 | /** 76 | * Generates a new key, publishes it and finally redirects the 77 | * user to the "write down my secret code" page. 78 | * @export 79 | */ 80 | ResetCtrl.prototype.goAhead = function() { 81 | this.status = this.translateService_.getMessage('resetKeyStatus'); 82 | this.inProgress = true; 83 | 84 | // Run a sequence of async tasks that generates a new keypair, 85 | // and sends an email that this account was reset. 86 | this.authService_.getIdentityToken().then( 87 | goog.bind(function(idtoken) { 88 | // Idtoken is guaranteed to be available here. 89 | return this.openpgpService_.generateKey( 90 | this.gmailService_.mailbox.email, idtoken, this); 91 | }, this)).then(goog.bind(function(keys) { 92 | // Chains a task that posts an email that this account was 93 | // reset. 94 | this.status = this.translateService_.getMessage('sendResetMailStatus'); 95 | return this.gmailService_.sendResetMail(); 96 | }, this)).then(goog.bind(function() { 97 | // Redirects the user to the showsecret page. 98 | this.status = null; 99 | this.location_.path(e2email.constants.Location.SHOWSECRET); 100 | }, this)).catch(goog.bind(function(err) { 101 | this.status = err.toString(); 102 | }, this)).finally(goog.bind(function() { 103 | this.inProgress = false; 104 | }, this)); 105 | }; 106 | 107 | 108 | }); // goog.scope 109 | -------------------------------------------------------------------------------- /chrome/pages/reset/reset-controller_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the reset page controller. 19 | */ 20 | 21 | goog.require('e2email.pages.reset.ResetCtrl'); 22 | 23 | describe('ResetCtrl', function() { 24 | var q, controller, resetController, scope, location; 25 | var mockTranslateService, mockOpenpgpService, mockGmailService, 26 | mockAuthService; 27 | var TEST_TOKEN = 'idtoken'; 28 | var TEST_CODE = 'test-code'; 29 | var TEST_EMAIL = 'mail@example.com'; 30 | var TEST_KEY = 'test-key'; 31 | 32 | beforeEach(module(function($controllerProvider) { 33 | $controllerProvider.register( 34 | 'ResetCtrl', e2email.pages.reset.ResetCtrl); 35 | })); 36 | 37 | beforeEach(inject(function($rootScope, $controller, $location, $q) { 38 | scope = $rootScope.$new(); 39 | controller = $controller; 40 | location = $location; 41 | q = $q; 42 | 43 | mockTranslateService = { 44 | getMessage: function(m) { return 'dummy'; } 45 | }; 46 | mockOpenpgpService = { 47 | generateKey: function(email, idtoken) { 48 | if ((email === TEST_EMAIL) && (idtoken === TEST_TOKEN)) { 49 | return q.when(TEST_KEY); 50 | } else { 51 | return q.reject('bad code'); 52 | } 53 | } 54 | }; 55 | mockGmailService = { 56 | mailbox: { 57 | email: TEST_EMAIL 58 | }, 59 | sendResetMail: function() { 60 | return q.when(undefined); 61 | } 62 | }; 63 | mockAuthService = { 64 | getIdentityToken: function() { 65 | return q.when(TEST_TOKEN); 66 | } 67 | }; 68 | })); 69 | 70 | it('should perform all reset steps', function() { 71 | spyOn(mockGmailService, 'sendResetMail').and.callThrough(); 72 | spyOn(mockOpenpgpService, 'generateKey').and.callThrough(); 73 | resetController = controller( 74 | 'ResetCtrl as resetCtrl', { 75 | $scope: scope, 76 | $location: location, 77 | gmailService: mockGmailService, 78 | translateService: mockTranslateService, 79 | openpgpService: mockOpenpgpService, 80 | authService: mockAuthService 81 | }); 82 | // Ask controller to reset. 83 | scope.resetCtrl.goAhead(); 84 | scope.$digest(); 85 | // Just check all interesting service methods were called. 86 | expect(mockOpenpgpService.generateKey).toHaveBeenCalled(); 87 | expect(mockGmailService.sendResetMail).toHaveBeenCalled(); 88 | // And that we landed at the right end-point. 89 | expect(location.path()).toBe('/showsecret'); 90 | }); 91 | 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /chrome/pages/reset/reset.css: -------------------------------------------------------------------------------- 1 | .reset-row { 2 | margin-top: 163px; 3 | } 4 | -------------------------------------------------------------------------------- /chrome/pages/reset/reset.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

{{'resetTitle' | translate}}

5 | 6 |
7 |
{{'resetHelp' | translate}}
8 |
{{'resetWarning' | translate}}
9 |
10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /chrome/pages/settings/settings-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Controller for the settings view for the application. 19 | */ 20 | goog.provide('e2email.pages.settings.SettingsCtrl'); 21 | 22 | goog.require('e2email.constants.Location'); 23 | 24 | 25 | goog.scope(function() { 26 | 27 | 28 | 29 | /** 30 | * Settings page controller. 31 | * 32 | * @param {!angular.Scope} $scope 33 | * @param {!angular.$location} $location The Angular location service. 34 | * @param {!e2email.components.appinfo.AppinfoService} appinfoService 35 | * the appinfo service. 36 | * @param {!e2email.components.openpgp.OpenPgpService} openpgpService 37 | * The OpenPGP service. 38 | * @param {!e2email.components.gmail.GmailService} gmailService 39 | * the gmail service. 40 | * @constructor 41 | * @ngInject 42 | * @export 43 | */ 44 | e2email.pages.settings.SettingsCtrl = function( 45 | $scope, $location, appinfoService, openpgpService, gmailService) { 46 | 47 | /** @private */ 48 | this.location_ = $location; 49 | 50 | /** @private */ 51 | this.appinfoService_ = appinfoService; 52 | 53 | /** @private */ 54 | this.openpgpService_ = openpgpService; 55 | 56 | var email = goog.isDefAndNotNull(gmailService.mailbox.email) ? 57 | gmailService.mailbox.email : 'Unknown'; 58 | /** @type {!e2email.models.user.User} */ 59 | this.info = { 60 | 'email': email, 61 | 'fingerprintChanged': false, 62 | 'name': null, 63 | 'fingerprintHex': null 64 | }; 65 | /** @type {?string} */ 66 | this.recoveryCode = null; 67 | 68 | /** @type {boolean} */ 69 | this.showIDCheckHelp = false; 70 | 71 | /** @type {string} */ 72 | this.appVersion = appinfoService.getVersion(); 73 | 74 | /** @type {string|undefined} */ 75 | this.appPlatform = undefined; 76 | 77 | // Refresh additional information 78 | $scope.$on('$viewContentLoaded', goog.bind(this.refresh_, this)); 79 | }; 80 | 81 | 82 | var SettingsCtrl = e2email.pages.settings.SettingsCtrl; 83 | 84 | 85 | /** 86 | * Runs an async task to update user information. 87 | * @private 88 | */ 89 | SettingsCtrl.prototype.refresh_ = function() { 90 | this.openpgpService_.searchPublicKey(this.info.email, false) 91 | .then(goog.bind(function(key) { 92 | if (goog.isDefAndNotNull(key)) { 93 | this.info.fingerprintHex = key.key.fingerprintHex; 94 | } 95 | }, this)) 96 | .then(goog.bind(this.createBackupCode_, this)) 97 | .then(goog.bind(this.getApplicationInfo_, this)); 98 | }; 99 | 100 | 101 | /** 102 | * Creates a backup code if necessary. 103 | * @private 104 | * @return {!angular.$q.Promise} 105 | */ 106 | SettingsCtrl.prototype.createBackupCode_ = function() { 107 | return this.openpgpService_.getSecretBackupCode(this.info.email) 108 | .then(goog.bind(function(code) { 109 | this.recoveryCode = this.nicelyFormat_(code); 110 | }, this)); 111 | }; 112 | 113 | 114 | /** 115 | * Fetches application info if necessary. 116 | * @private 117 | * @return {!angular.$q.Promise} 118 | */ 119 | SettingsCtrl.prototype.getApplicationInfo_ = function() { 120 | return this.appinfoService_.getPlatform() 121 | .then(goog.bind(function(platform) { 122 | this.appPlatform = platform; 123 | }, this)); 124 | }; 125 | 126 | 127 | /** 128 | * Returns user information around primary user. 129 | * @return {?e2email.models.user.User} 130 | * @export 131 | */ 132 | SettingsCtrl.prototype.getInfo = function() { 133 | return this.info; 134 | }; 135 | 136 | 137 | /** 138 | * Returns the recovery code, if available. 139 | * @return {?string} 140 | * @export 141 | */ 142 | SettingsCtrl.prototype.getRecoveryCode = function() { 143 | return this.recoveryCode; 144 | }; 145 | 146 | 147 | /** 148 | * Toggles whether the help screen should be displayed or not. 149 | * @param {!angular.Scope.Event} event 150 | * @export 151 | */ 152 | SettingsCtrl.prototype.toggleIDCheckHelp = function(event) { 153 | this.showIDCheckHelp = !this.showIDCheckHelp; 154 | event.stopPropagation(); 155 | }; 156 | 157 | 158 | /** 159 | * Returns true if the help screen for the ID Check should be visible. 160 | * @return {boolean} 161 | * @export 162 | */ 163 | SettingsCtrl.prototype.getShowIDCheckHelp = function() { 164 | return this.showIDCheckHelp; 165 | }; 166 | 167 | 168 | /** 169 | * Returns the application version. 170 | * @return {string} 171 | * @export 172 | */ 173 | SettingsCtrl.prototype.getAppVersion = function() { 174 | return this.appVersion; 175 | }; 176 | 177 | 178 | /** 179 | * Returns the application platform. 180 | * @return {string|undefined} 181 | * @export 182 | */ 183 | SettingsCtrl.prototype.getAppPlatform = function() { 184 | return this.appPlatform; 185 | }; 186 | 187 | 188 | /** 189 | * Given a string, chunks it to make it somewhat easier to read. 190 | * @param {string} input 191 | * @return {string} formatted string. 192 | * @private 193 | */ 194 | SettingsCtrl.prototype.nicelyFormat_ = function(input) { 195 | return input.match(/.{1,5}/g).join(' '); 196 | }; 197 | 198 | 199 | /** 200 | * Redirects the application to the thread list view. 201 | * @export 202 | */ 203 | SettingsCtrl.prototype.showThreads = function() { 204 | this.location_.path(e2email.constants.Location.THREADS); 205 | }; 206 | 207 | 208 | }); // goog.scope 209 | -------------------------------------------------------------------------------- /chrome/pages/settings/settings-controller_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for the settings page controller. 3 | */ 4 | 5 | goog.require('e2email.pages.settings.SettingsCtrl'); 6 | 7 | describe('SettingsCtrl', function() { 8 | var q, controller, settingsController, rootScope, scope, location; 9 | var TEST_EMAIL = 'test@email.com'; 10 | var TEST_BACKUP_CODE = 'test-backup-code'; 11 | var TEST_VERSION = 'test-version'; 12 | var TEST_PLATFORM = 'test-platform'; 13 | var TEST_KEY = { 14 | key: { 15 | fingerprintHex: 'abc' 16 | } 17 | }; 18 | var mockAppinfoService = { 19 | getVersion: function() { 20 | return TEST_VERSION; 21 | }, 22 | getPlatform: function() { 23 | return q.when(TEST_PLATFORM); 24 | } 25 | }; 26 | var mockOpenpgpService = { 27 | searchPublicKey: function(email, remote) { 28 | if ((email === TEST_EMAIL) && !remote) { 29 | return q.when(TEST_KEY); 30 | } else { 31 | return q.reject('bad arguments'); 32 | } 33 | }, 34 | getSecretBackupCode: function(email) { 35 | if (email === TEST_EMAIL) { 36 | return q.when(TEST_BACKUP_CODE); 37 | } else { 38 | return q.reject('bad argument'); 39 | } 40 | } 41 | }; 42 | var mockGmailService = { 43 | mailbox: { 44 | email: TEST_EMAIL 45 | } 46 | }; 47 | 48 | beforeEach(module(function($controllerProvider) { 49 | $controllerProvider.register( 50 | 'SettingsCtrl', e2email.pages.settings.SettingsCtrl); 51 | })); 52 | 53 | beforeEach(inject(function($q, $rootScope, $controller, $location) { 54 | rootScope = $rootScope; 55 | scope = $rootScope.$new(); 56 | controller = $controller; 57 | location = $location; 58 | q = $q; 59 | })); 60 | 61 | it('should initialize info after refresh', function() { 62 | settingsController = controller( 63 | 'SettingsCtrl as settingsCtrl', { 64 | $scope: scope, 65 | $location: location, 66 | appinfoService: mockAppinfoService, 67 | openpgpService: mockOpenpgpService, 68 | gmailService: mockGmailService 69 | }); 70 | expect(settingsController.info.email).toBe(TEST_EMAIL); 71 | expect(settingsController.info.fingerprintHex).toBeNull(); 72 | settingsController.refresh_(); 73 | rootScope.$apply(); 74 | expect(settingsController.info.fingerprintHex).toBe( 75 | TEST_KEY.key.fingerprintHex); 76 | }); 77 | 78 | it('should send to mailbox on return', function() { 79 | settingsController = controller( 80 | 'SettingsCtrl as settingsCtrl', { 81 | $scope: scope, 82 | $location: location, 83 | appinfoService: mockAppinfoService, 84 | openpgpService: mockOpenpgpService, 85 | gmailService: mockGmailService 86 | }); 87 | location.path('/settings'); 88 | expect(location.path()).toBe('/settings'); 89 | // punch back button 90 | scope.settingsCtrl.showThreads(); 91 | rootScope.$apply(); 92 | expect(location.path()).toBe('/threads'); 93 | }); 94 | 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /chrome/pages/settings/settings.css: -------------------------------------------------------------------------------- 1 | .settings-fingerprint { 2 | font-family: monospace; 3 | } 4 | 5 | .settings-title { 6 | text-transform: uppercase; 7 | letter-spacing: 1px; 8 | font-size: 12px; 9 | margin-top: 1.4em; 10 | } 11 | -------------------------------------------------------------------------------- /chrome/pages/settings/settings.html: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |
16 |
{{'settings' | translate}}
17 |
18 |
19 |
{{'primaryEmailTitle' | translate}}
20 |
21 |
22 |
{{settingsCtrl.info.email}}
23 |
24 | 25 |
26 |
{{'recoveryCode' | translate}}
27 |
28 |
29 |
30 | 34 |
35 |
{{settingsCtrl.recoveryCode}}
36 |
37 |
38 |
{{'settingsSecretHelp' | translate}}
39 |
40 | 41 |
42 |
{{'idCheck' | translate}}
43 |
44 |
45 |
{{settingsCtrl.info.fingerprintHex}}
46 |
47 |
48 | 49 |
50 |
51 |
{{'fingerprintSettingsHelp' | translate}}
52 |
53 |
54 |
55 |
60 |
61 | 62 |
63 |
{{'appinfoTitle' | translate}}
64 |
65 |
66 |
Version
67 |
{{settingsCtrl.appVersion}}
68 |
69 |
70 |
Platform
71 |
{{settingsCtrl.appPlatform}}
72 |
73 | 74 | 75 |
76 | 77 |
78 | -------------------------------------------------------------------------------- /chrome/pages/setup/setup-controller_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the setup page controller. 19 | */ 20 | 21 | goog.require('e2email.pages.setup.SetupCtrl'); 22 | 23 | describe('SetupCtrl', function() { 24 | var q, controller, setupController, scope, location; 25 | var mockTranslateService, mockOpenpgpService, mockGmailService, 26 | mockAuthService; 27 | var TEST_TOKEN = 'idtoken'; 28 | var TEST_CODE = 'test-code'; 29 | var TEST_EMAIL = 'mail@example.com'; 30 | var TEST_KEY = { key: 'test-key'}; 31 | var gmailAuthorized = true; 32 | var privateKey, remoteKey; 33 | 34 | beforeEach(module(function($controllerProvider) { 35 | $controllerProvider.register( 36 | 'SetupCtrl', e2email.pages.setup.SetupCtrl); 37 | })); 38 | 39 | beforeEach(inject(function($rootScope, $controller, $location, $q) { 40 | scope = $rootScope.$new(); 41 | controller = $controller; 42 | location = $location; 43 | q = $q; 44 | 45 | mockTranslateService = { 46 | getMessage: function(m) { return 'dummy'; } 47 | }; 48 | mockOpenpgpService = { 49 | initialize: function(password) { 50 | return q.when(undefined); 51 | }, 52 | searchPrivateKey: function(email) { 53 | if (email === TEST_EMAIL) { 54 | return q.when(privateKey); 55 | } else { 56 | return q.reject('bad email'); 57 | } 58 | }, 59 | searchPublicKey: function(email, remote) { 60 | if ((email === TEST_EMAIL) && remote) { 61 | return q.when(remoteKey); 62 | } else { 63 | return q.reject('bad email'); 64 | } 65 | }, 66 | generateKey: function(email, idtoken) { 67 | if ((email === TEST_EMAIL) && (idtoken === TEST_TOKEN)) { 68 | return q.when(TEST_KEY); 69 | } else { 70 | return q.reject('bad code'); 71 | } 72 | } 73 | }; 74 | 75 | mockGmailService = { 76 | mailbox: { 77 | email: TEST_EMAIL 78 | }, 79 | sendSetupMail: function() { 80 | return q.when(undefined); 81 | }, 82 | sendWelcomeMail: function() { 83 | return q.when(undefined); 84 | }, 85 | isAuthorized: function() { 86 | return q.when(gmailAuthorized); 87 | } 88 | }; 89 | mockAuthService = { 90 | getIdentityToken: function() { 91 | return q.when(TEST_TOKEN); 92 | } 93 | }; 94 | })); 95 | 96 | it('should send to introduction when unauthorized', function() { 97 | gmailAuthorized = false; 98 | setupController = controller( 99 | 'SetupCtrl as setupCtrl', { 100 | $scope: scope, 101 | $location: location, 102 | gmailService: mockGmailService, 103 | translateService: mockTranslateService, 104 | openpgpService: mockOpenpgpService, 105 | authService: mockAuthService 106 | }); 107 | // Hand-crank the check code. 108 | scope.setupCtrl.check_(); 109 | // Resolve promises. 110 | scope.$digest(); 111 | // Verify we landed at the right end-point. 112 | expect(location.path()).toBe('/introduction'); 113 | }); 114 | 115 | it('should redirect to recover when user only has a public key', function() { 116 | gmailAuthorized = true; 117 | privateKey = null; 118 | remoteKey = TEST_KEY; 119 | setupController = controller( 120 | 'SetupCtrl as setupCtrl', { 121 | $scope: scope, 122 | $location: location, 123 | gmailService: mockGmailService, 124 | translateService: mockTranslateService, 125 | openpgpService: mockOpenpgpService, 126 | authService: mockAuthService 127 | }); 128 | // Hand-crank the check code. 129 | scope.setupCtrl.check_(); 130 | // Resolve promises. 131 | scope.$digest(); 132 | // Verify we landed at the right end-point. 133 | expect(location.path()).toBe('/recover'); 134 | }); 135 | 136 | it('should make a new key with no local or remote keys', function() { 137 | gmailAuthorized = true; 138 | privateKey = null; 139 | remoteKey = null; 140 | spyOn(mockOpenpgpService, 'generateKey').and.callThrough(); 141 | spyOn(mockGmailService, 'sendWelcomeMail').and.callThrough(); 142 | setupController = controller( 143 | 'SetupCtrl as setupCtrl', { 144 | $scope: scope, 145 | $location: location, 146 | gmailService: mockGmailService, 147 | translateService: mockTranslateService, 148 | openpgpService: mockOpenpgpService, 149 | authService: mockAuthService 150 | }); 151 | // Hand-crank the check code. 152 | scope.setupCtrl.check_(); 153 | // Resolve promises. 154 | scope.$digest(); 155 | // Check generate key was called. 156 | expect(mockOpenpgpService.generateKey).toHaveBeenCalled(); 157 | // Check new user email was sent. 158 | expect(mockGmailService.sendWelcomeMail).toHaveBeenCalled(); 159 | 160 | // Verify we landed at the right end-point. 161 | expect(location.path()).toBe('/showsecret'); 162 | }); 163 | 164 | 165 | }); 166 | -------------------------------------------------------------------------------- /chrome/pages/setup/setup.html: -------------------------------------------------------------------------------- 1 |

{{'appName' | translate}}

2 | 3 |
4 |
{{ ['setupInfo', setupCtrl.email] | translate }}
5 |
6 | 7 |
8 |
{{setupCtrl.status}}
9 |
10 | -------------------------------------------------------------------------------- /chrome/pages/showsecret/showsecret-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview controller for the showsecret key view. 19 | */ 20 | goog.provide('e2email.pages.showsecret.ShowSecretCtrl'); 21 | 22 | goog.require('e2email.constants.Location'); 23 | 24 | goog.scope(function() { 25 | 26 | 27 | 28 | /** 29 | * ShowSecret page controller. 30 | * 31 | * @param {!angular.Scope} $scope 32 | * @param {!angular.$location} $location angular location service. 33 | * @param {!e2email.components.translate.TranslateService} translateService 34 | * The translate service. 35 | * @param {!e2email.components.openpgp.OpenPgpService} openpgpService 36 | * The OpenPGP service. 37 | * @param {!e2email.components.gmail.GmailService} gmailService 38 | * The Gmail service. 39 | * @constructor 40 | * @ngInject 41 | * @export 42 | */ 43 | e2email.pages.showsecret.ShowSecretCtrl = function( 44 | $scope, $location, translateService, openpgpService, gmailService) { 45 | /** @private */ 46 | this.location_ = $location; 47 | /** @private */ 48 | this.openpgpService_ = openpgpService; 49 | /** @private */ 50 | this.gmailService_ = gmailService; 51 | /** 52 | * @type {?string} 53 | */ 54 | this.status = translateService.getMessage('generateCodeStatus'); 55 | /** 56 | * @type {?string} 57 | */ 58 | this.code = null; 59 | $scope.$on('$viewContentLoaded', goog.bind(this.generateCode_, this)); 60 | }; 61 | 62 | var ShowSecretCtrl = e2email.pages.showsecret.ShowSecretCtrl; 63 | 64 | 65 | /** 66 | * Redirects to the threads view. 67 | * @export 68 | */ 69 | ShowSecretCtrl.prototype.proceed = function() { 70 | this.location_.path(e2email.constants.Location.THREADS); 71 | }; 72 | 73 | 74 | /** 75 | * Requests the context for a backup code. 76 | * @private 77 | */ 78 | ShowSecretCtrl.prototype.generateCode_ = function() { 79 | this.openpgpService_.getSecretBackupCode(this.gmailService_.mailbox.email) 80 | .then(goog.bind(function(code) { 81 | this.status = null; 82 | this.code = this.nicelyFormat_(code); 83 | }, this)).catch(goog.bind(function(err) { 84 | this.status = err.toString(); 85 | }, this)); 86 | }; 87 | 88 | 89 | /** 90 | * Given a string, chunks it to make it somewhat easier to read. 91 | * @param {string} input 92 | * @return {string} formatted string. 93 | * @private 94 | */ 95 | ShowSecretCtrl.prototype.nicelyFormat_ = function(input) { 96 | return input.match(/.{1,5}/g).join(' '); 97 | }; 98 | 99 | }); // goog.scope 100 | -------------------------------------------------------------------------------- /chrome/pages/showsecret/showsecret.css: -------------------------------------------------------------------------------- 1 | div.showsecret-vcenter { 2 | height: 80px; 3 | line-height: 80px; 4 | margin-bottom: 22.4px; 5 | } 6 | 7 | span.showsecret-vcenter { 8 | display: inline-block; 9 | vertical-align: middle; 10 | line-height: 22.4px; 11 | } 12 | 13 | div.instruct-box { 14 | height: 200px; 15 | width: 400px; 16 | padding: 10px; 17 | border: 2px solid #ECC67B; 18 | background-color: #F9EDBE; 19 | margin: auto; 20 | margin-bottom: 30px; 21 | } 22 | 23 | div.secret-box { 24 | height: 60px; 25 | width: 370px; 26 | padding: 10px; 27 | background-color: white; 28 | margin: auto; 29 | } 30 | 31 | span.secret-instruction { 32 | font-weight: bold; 33 | color: #50504E; 34 | } 35 | -------------------------------------------------------------------------------- /chrome/pages/showsecret/showsecret.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

{{'showsecretTitle' | translate}}

5 | 6 |
7 |
{{showSecretCtrl.status}}
8 |
9 | 10 |
11 |
12 |
{{'showsecretHelpIntro' | translate}}
13 |
14 |
15 |
16 |
17 | {{'showsecretHelpSteps' | translate}} 18 |
19 |
20 | 21 |
22 |
23 |

{{showSecretCtrl.code}}

24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 | -------------------------------------------------------------------------------- /chrome/pages/threads/threads.css: -------------------------------------------------------------------------------- 1 | hr.email-hr { 2 | margin: 0 0 0 0; 3 | } 4 | 5 | .email-subject-entry { 6 | cursor: pointer; 7 | margin-left: -15px; 8 | margin-right: -15px; 9 | padding-bottom: 0; 10 | padding-left: 15px; 11 | padding-right: 15px; 12 | padding-top: 1em; 13 | } 14 | 15 | .email-subject-entry:hover { 16 | background-color: rgba(0,0,0,0.04); 17 | } 18 | 19 | div.email-subject-date { 20 | color: rgba(0,0,0,0.54); 21 | min-height: 0px !important; 22 | } 23 | 24 | div.email-subject-from { 25 | color: rgba(0,0,0,0.54); 26 | min-height: 0px !important; 27 | } 28 | 29 | .email-subject-title { 30 | font-size: 16px; 31 | line-height: 1.4; 32 | margin-bottom: 0.5em; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | white-space: nowrap; 36 | } 37 | 38 | .bold { 39 | font-weight: bold; 40 | } 41 | 42 | div.email-select { 43 | background-color: transparent; 44 | border: 1px solid #ccc; 45 | color: #428bca; 46 | height: 12px; 47 | margin-top: 4px; 48 | position: relative; 49 | speak: none; 50 | width: 12px; 51 | } 52 | 53 | div.email-select > span { 54 | font-size: 24px; 55 | left: -3px; 56 | opacity: 0; 57 | position: absolute; 58 | top: -12px; 59 | } 60 | 61 | div.email-select-active > span { 62 | opacity: 1; 63 | } 64 | 65 | div.header-icon-container { 66 | text-align: center; 67 | } 68 | 69 | svg.header-icon > path { 70 | fill: #808080; 71 | } 72 | 73 | svg.header-icon:hover > path { 74 | fill: #428bca; 75 | } 76 | 77 | .spinning { 78 | -webkit-animation: spin 3s infinite linear; 79 | animation: spin 3s infinite linear; 80 | fill: #999999; 81 | } 82 | 83 | @-webkit-keyframes spin { 84 | 0% { 85 | -webkit-transform: rotate(0deg); 86 | transform: rotate(0deg); 87 | } 88 | 100% { 89 | -webkit-transform: rotate(360deg); 90 | transform: rotate(360deg); 91 | } 92 | } 93 | @keyframes spin { 94 | 0% { 95 | -webkit-transform: rotate(0deg); 96 | transform: rotate(0deg); 97 | } 98 | 100% { 99 | -webkit-transform: rotate(360deg); 100 | transform: rotate(360deg); 101 | } 102 | } 103 | 104 | input.email-select[type="checkbox"]:checked + label.email-select > span { 105 | display: initial; 106 | } 107 | 108 | .email-subject-tease { 109 | padding-bottom: 1em; 110 | } 111 | 112 | .email-subject-tease .glyphicon { 113 | color: rgba(0,0,0,0.24); 114 | font-size: 10px; 115 | padding-right: 0.3em; 116 | } 117 | 118 | .thread-select-label { 119 | padding-bottom: 1.25em; 120 | padding-top: 1.25em; 121 | } 122 | 123 | .email-main-compose-form button { 124 | margin-left: 1em; 125 | margin-top: 1em; 126 | } 127 | 128 | .email-main-recipient-input { 129 | border-bottom-left-radius: 0; 130 | border-bottom-right-radius: 0; 131 | } 132 | 133 | .email-main-subject-input { 134 | border-radius: 0; 135 | border-top: none; 136 | } 137 | 138 | .email-main-compose-textarea { 139 | border-top-left-radius: 0; 140 | border-top-right-radius: 0; 141 | border-top-style: none; 142 | } 143 | 144 | .email-main-compose-textarea[disabled]::-webkit-input-placeholder { 145 | color: rgba(0, 0, 0, 0.3333); 146 | } 147 | 148 | .email-main-compose-textarea::-webkit-input-placeholder { 149 | color: rgba(0, 115, 0, 0.4); 150 | } 151 | 152 | .email-main-recipient-missing { 153 | background-color: #fcf8e3; 154 | border-bottom-left-radius: 4px; 155 | border-bottom-right-radius: 4px; 156 | border-color: #faebcc; 157 | border: 1px solid #faebcc; 158 | color: #c09853; 159 | margin-bottom: 20px; 160 | padding: 15px; 161 | } 162 | 163 | .email-main-recipient-invalid { 164 | background-color: #f2dede; 165 | border-bottom-left-radius: 4px; 166 | border-bottom-right-radius: 4px; 167 | border-color: #ebccd1; 168 | border: 1px solid #faebcc; 169 | color: #b94a48; 170 | margin-bottom: 20px; 171 | padding: 15px; 172 | } 173 | 174 | .email-no-threads { 175 | margin-top: 1em; 176 | } 177 | 178 | .composebtn { 179 | width: 100px; 180 | height: 35px; 181 | font-size: 15px; 182 | padding: 0px; 183 | } 184 | 185 | .compose-content { 186 | max-height: 700px; 187 | overflow: auto; 188 | } 189 | 190 | #attachName { 191 | margin: 0px; 192 | padding:0px; 193 | overflow-y: hidden; 194 | width: 60%; 195 | } 196 | 197 | #attachSize { 198 | width:30%; 199 | overflow-y: hidden; 200 | } 201 | -------------------------------------------------------------------------------- /chrome/pages/threads/threads.html: -------------------------------------------------------------------------------- 1 | 44 | 45 |
46 | 81 | 82 |
    83 | 104 |
105 | 106 |
107 |
108 | {{threadsCtrl.status}} 109 |
110 |
111 | 112 | 115 | 116 |
117 | -------------------------------------------------------------------------------- /chrome/pages/welcome/welcome-controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview controller for the welcome page. 19 | */ 20 | goog.provide('e2email.pages.welcome.WelcomeCtrl'); 21 | 22 | goog.scope(function() { 23 | 24 | 25 | 26 | /** 27 | * Welcome page controller. The view is launched if the application 28 | * discovers the user is not authorized to connect to Gmail, and 29 | * contains a method to launch the sign-in prcoess. If the sign-in flow 30 | * succeeds, it launches the setup view, otherwise it remains in this 31 | * view. 32 | * 33 | * @param {!angular.$location} $location the angular $location service. 34 | * @param {!e2email.components.translate.TranslateService} translateService 35 | * @param {!e2email.components.gmail.GmailService} gmailService 36 | * the gmail service. 37 | * @constructor 38 | * @ngInject 39 | * @export 40 | */ 41 | e2email.pages.welcome.WelcomeCtrl = function( 42 | $location, translateService, gmailService) { 43 | /** @type {boolean} */ 44 | this.inProgress = false; 45 | /** 46 | * @private {!angular.$location} 47 | */ 48 | this.location_ = $location; 49 | /** 50 | * @private {!e2email.components.translate.TranslateService} 51 | */ 52 | this.translateService_ = translateService; 53 | /** 54 | * @private {!e2email.components.gmail.GmailService} 55 | */ 56 | this.gmailService_ = gmailService; 57 | /** 58 | * @type {?string} 59 | */ 60 | this.status = null; 61 | }; 62 | 63 | var WelcomeCtrl = e2email.pages.welcome.WelcomeCtrl; 64 | 65 | 66 | /** 67 | * Start the chrome OAuth signin-process. 68 | * @export 69 | */ 70 | WelcomeCtrl.prototype.signIn = function() { 71 | this.inProgress = true; 72 | this.status = this.translateService_.getMessage('requestApprovalStatus'); 73 | 74 | this.gmailService_.signIn().then(goog.bind(function(approved) { 75 | if (approved) { 76 | this.status = null; 77 | this.location_.path('/setup'); 78 | } else { 79 | // stay where we are. 80 | this.status = this.translateService_.getMessage( 81 | 'approvalUnavailableStatus'); 82 | } 83 | }, this)).catch(goog.bind(function(err) { 84 | this.status = err.toString(); 85 | }, this)).finally(goog.bind(function() { 86 | this.inProgress = false; 87 | }, this)); 88 | }; 89 | 90 | 91 | /** 92 | * Returns the current status. 93 | * @return {?string} The status message. 94 | * @export 95 | */ 96 | WelcomeCtrl.prototype.getStatus = function() { 97 | return this.status; 98 | }; 99 | 100 | 101 | /** 102 | * Returns whether there's an in-progress operation. 103 | * @return {boolean} returns true if there's an in-progress operation. 104 | * @export 105 | */ 106 | WelcomeCtrl.prototype.isInProgress = function() { 107 | return this.inProgress; 108 | }; 109 | 110 | 111 | }); // goog.scope 112 | -------------------------------------------------------------------------------- /chrome/pages/welcome/welcome-controller_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for the welcome page controller. 3 | */ 4 | 5 | goog.require('e2email.pages.welcome.WelcomeCtrl'); 6 | 7 | describe('WelcomeCtrl', function() { 8 | var q, controller, welcomeController, rootScope, scope, location; 9 | var approved = true; 10 | var mockService; 11 | var mockTranslateService = { 12 | getMessage: function(label) { 13 | return label; 14 | } 15 | }; 16 | 17 | beforeEach(module(function($controllerProvider) { 18 | $controllerProvider.register( 19 | 'WelcomeCtrl', e2email.pages.welcome.WelcomeCtrl); 20 | })); 21 | 22 | beforeEach(inject(function($q, $rootScope, $controller, $location) { 23 | rootScope = $rootScope; 24 | scope = $rootScope.$new(); 25 | controller = $controller; 26 | location = $location; 27 | q = $q; 28 | mockService = { 29 | signIn: function() { 30 | return q.when(approved); 31 | } 32 | }; 33 | })); 34 | 35 | it('should go to /setup upon approval', function() { 36 | approved = true; 37 | welcomeController = controller( 38 | 'WelcomeCtrl as welcomeCtrl', { 39 | $scope: scope, 40 | $location: location, 41 | translateService: mockTranslateService, 42 | gmailService: mockService 43 | }); 44 | location.path('/welcome'); 45 | expect(location.path()).toBe('/welcome'); 46 | // punch the sign-in button 47 | scope.welcomeCtrl.signIn(); 48 | rootScope.$apply(); 49 | expect(location.path()).toBe('/setup'); 50 | }); 51 | 52 | it('location should remain unchanged without approval', function() { 53 | approved = false; 54 | welcomeController = controller( 55 | 'WelcomeCtrl as welcomeCtrl', { 56 | $scope: scope, 57 | $location: location, 58 | translateService: mockTranslateService, 59 | gmailService: mockService 60 | }); 61 | location.path('/welcome'); 62 | expect(location.path()).toBe('/welcome'); 63 | // punch the sign-in button 64 | scope.welcomeCtrl.signIn(); 65 | rootScope.$apply(); 66 | expect(location.path()).toBe('/welcome'); 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /chrome/pages/welcome/welcome.css: -------------------------------------------------------------------------------- 1 | div.welcomeIcon { 2 | margin: auto; 3 | padding-top: 60px; 4 | padding-bottom: 50px; 5 | height: 50px; 6 | } 7 | 8 | h1.welcome-title { 9 | padding-top: 22% !important; 10 | font-weight: bold; 11 | } 12 | 13 | div.welcome-format { 14 | text-align: left; 15 | line-height: 1.0 !important; 16 | padding-left: 40px; 17 | padding-right: 40px; 18 | color: #5D5E5E !important; 19 | font-size: 18px !important; 20 | } 21 | 22 | main-title { 23 | padding-top: 25% !important; 24 | color: black; 25 | } 26 | -------------------------------------------------------------------------------- /chrome/pages/welcome/welcome.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |

{{'signInAndPermissions' | translate}}

7 |
{{'welcomeHelp' | translate}}
8 |
9 |
{{'signInChrome' | translate}}
10 |
{{'logInEmailAccess' | translate}}
11 |
{{'emailAccessWhy' | translate}}
12 |
{{'logInDeleteAccess' | translate}}
13 |
{{'deleteAccessWhy' | translate}}
14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |
{{welcomeCtrl.status}}
23 |
24 | -------------------------------------------------------------------------------- /chrome/util/email.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview small utility for email related functions 19 | */ 20 | 21 | goog.provide('e2email.util.Email'); 22 | 23 | goog.require('goog.format.EmailAddress'); 24 | 25 | goog.scope(function() { 26 | 27 | 28 | var Email = e2email.util.Email; 29 | 30 | 31 | /** 32 | * Given a string, parses a bare email address from it, or returns 33 | * null if it was unable to find a valid email address. 34 | * @param {?string} content The string containing the email address. 35 | * @return {?string} A bare email address, or null if none was found. 36 | */ 37 | Email.parseEmail = function(content) { 38 | if (!goog.isDefAndNotNull(content)) { 39 | return null; 40 | } 41 | var parsed = goog.format.EmailAddress.parse(content); 42 | if (parsed.isValid()) { 43 | return parsed.getAddress(); 44 | } else { 45 | return null; 46 | } 47 | }; 48 | 49 | 50 | 51 | 52 | }); // goog.scope 53 | -------------------------------------------------------------------------------- /chrome/util/email_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the email implementation. 19 | */ 20 | 21 | goog.require('e2email.util.Email'); 22 | 23 | goog.scope(function() { 24 | 25 | 26 | describe('Email utility', function() { 27 | var email = e2email.util.Email; 28 | it('checks the parseEmail function', function() { 29 | var mockEmail = null; 30 | expect(email.parseEmail(mockEmail)).toBe(null); 31 | mockEmail = 'example@gmail'; 32 | expect(email.parseEmail(mockEmail)).toBe(null); 33 | mockEmail = 'example.com'; 34 | expect(email.parseEmail(mockEmail)).toBe(null); 35 | mockEmail = 'example@gmail.com'; 36 | expect(email.parseEmail(mockEmail)).toBe(mockEmail); 37 | }); 38 | }); 39 | 40 | }); // goog.scope 41 | -------------------------------------------------------------------------------- /chrome/util/http.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview small utility containing a function to validate http responses 19 | */ 20 | 21 | goog.provide('e2email.util.Http'); 22 | 23 | 24 | goog.scope(function() { 25 | 26 | 27 | var http = e2email.util.Http; 28 | 29 | 30 | /** 31 | * Given a http service response, determine if this was a successful 32 | * response from 33 | * @param {Object} response 34 | * @return {boolean} Returns true if this looks like a successful response. 35 | */ 36 | http.goodResponse = function(response) { 37 | return (goog.isDefAndNotNull(response.status) && 38 | goog.isNumber(response.status) && 39 | (response.status === 200) && 40 | goog.isDefAndNotNull(response.data)); 41 | }; 42 | 43 | 44 | 45 | 46 | }); // goog.scope 47 | -------------------------------------------------------------------------------- /chrome/util/http_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the http implementation. 19 | */ 20 | 21 | goog.require('e2email.util.Http'); 22 | 23 | goog.scope(function() { 24 | 25 | 26 | describe('http utility', function() { 27 | var http = e2email.util.Http; 28 | it('should check for a proper HTTP response', function() { 29 | var mockResponse = { }; 30 | mockResponse.data = {}; 31 | // Missing a status, should fail. 32 | expect(http.goodResponse(mockResponse)).toBe(false); 33 | // This should fail, as status is not a number. 34 | mockResponse.status = '200'; 35 | expect(http.goodResponse(mockResponse)).toBe(false); 36 | mockResponse.status = 400; 37 | // This should fail, as status is not OK. 38 | expect(http.goodResponse(mockResponse)).toBe(false); 39 | mockResponse.status = 200; 40 | mockResponse.data = undefined; 41 | // This should fail, as there is no data. 42 | expect(http.goodResponse(mockResponse)).toBe(false); 43 | mockResponse.data = {}; 44 | // This should work. 45 | expect(http.goodResponse(mockResponse)).toBe(true); 46 | }); 47 | 48 | }); 49 | 50 | }); // goog.scope 51 | -------------------------------------------------------------------------------- /chrome/util/recoverycode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview convert byte array to and from a readable code. 19 | * The code is essentially a base58 encoding of the array, but the 20 | * array is prefixed with a single version byte. 21 | */ 22 | goog.provide('e2email.util.RecoveryCode'); 23 | 24 | goog.require('e2e.BigNum'); 25 | goog.require('e2e.error.InvalidArgumentsError'); 26 | goog.require('goog.array'); 27 | goog.require('goog.crypt.Sha1'); 28 | 29 | goog.scope(function() { 30 | 31 | 32 | /** 33 | * Base constant used for base58 calculations. 34 | * @type {number} 35 | * @const 36 | * @private 37 | */ 38 | var NUM58_ = 58; 39 | 40 | 41 | /** 42 | * Bignum base constant, used for base58 calculations. 43 | * @type {!e2e.BigNum} 44 | * @const 45 | * @private 46 | */ 47 | var BIGNUM58_ = e2e.BigNum.fromInteger(NUM58_); 48 | 49 | 50 | /** 51 | * Code version. 52 | * @type {number} 53 | * @const 54 | * @private 55 | */ 56 | var CODE_VERSION_ = 1; 57 | 58 | 59 | /** 60 | * Base58 Alphabet array. 61 | * @type {string} 62 | * @const 63 | * @private 64 | */ 65 | var BASE58ALPHABET_ = 66 | '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; 67 | 68 | 69 | /** 70 | * Base58 map - from character to number. 71 | * @type {Object} 72 | * @private 73 | */ 74 | var BASE58MAP_ = {}; 75 | (function() { 76 | var len = BASE58ALPHABET_.length; 77 | for (var i = 0; i < len; i++) { 78 | BASE58MAP_[BASE58ALPHABET_[i]] = e2e.BigNum.fromInteger(i); 79 | } 80 | })(); 81 | 82 | 83 | /** 84 | * This is the current implementation of converting a series of 85 | * bytes into a code. 86 | * 1. Concatenate the version (1) with the payload. 87 | * 2. Take the first 2 bytes of the sha-1 of the above bytearray, and append 88 | * it. This is used only as a checksum, to avoid accidental typos. 89 | * 3. Convert the above array into a big-endian number, then do base58 90 | * encoding with the typical base58 alphabet. 91 | * @param {!e2e.ByteArray} bytes 92 | * @return {string} encoded value. 93 | */ 94 | e2email.util.RecoveryCode.encode = function(bytes) { 95 | 96 | // 1. Prepend the payload with the version. 97 | bytes = goog.array.concat(CODE_VERSION_, bytes); 98 | 99 | // 2. Calculate the sha1. 100 | var sha1 = new goog.crypt.Sha1(); 101 | sha1.update(bytes); 102 | 103 | // 3. Append the first 2 bytes of the sha1. 104 | bytes = goog.array.concat(bytes, sha1.digest().slice(0, 2)); 105 | 106 | // Note: CODE_VERSION_ > 0, so this array has no leading zeros. 107 | // Therefore, we don't perform any additional operations related to 108 | // handling leading zeros. 109 | 110 | // unfinished holds the portion of the number that has not yet been 111 | // converted. We div this number by the base until it is zero. 112 | var unfinished = new e2e.BigNum(bytes); 113 | var result = []; 114 | while (unfinished.compare(e2e.BigNum.ZERO) > 0) { 115 | var qr = unfinished.divmodInt(NUM58_); 116 | unfinished = qr.quotient; 117 | result.push(BASE58ALPHABET_[qr.remainder]); 118 | } 119 | return result.reverse().join(''); 120 | }; 121 | 122 | 123 | /** 124 | * This is the inverse of the encoding operation. Converts the string 125 | * into a big-endian number, with the standard base58 alphabet. 126 | * Convert it into a bytearray, check that the first byte is the 127 | * version byte, verify the checksum and return the payload. 128 | * @param {string} code is the encoded string. 129 | * @return {!e2e.ByteArray} 130 | */ 131 | e2email.util.RecoveryCode.decode = function(code) { 132 | // Holds the bignum represented by the code. 133 | var val = e2e.BigNum.ZERO.clone(); 134 | var len = code.length; 135 | for (var i = 0; i < len; i++) { 136 | val = val.multiply(BIGNUM58_).add(BASE58MAP_[code[i]]); 137 | } 138 | var result = val.toByteArray(); 139 | // length should at least be 1(version) + 2(checksum) bytes 140 | if (result.length < 3) { 141 | throw new e2e.error.InvalidArgumentsError( 142 | 'This is not a recovery code.'); 143 | } 144 | if (result[0] !== CODE_VERSION_) { 145 | throw new e2e.error.InvalidArgumentsError( 146 | 'This is not a recovery code.'); 147 | } 148 | // Verify the checksum. 149 | var expected = result.slice(-2); 150 | var sha1 = new goog.crypt.Sha1(); 151 | sha1.update(result.slice(0, -2)); 152 | var actual = sha1.digest().slice(0, 2); 153 | if (!goog.array.equals(expected, actual)) { 154 | throw new e2e.error.InvalidArgumentsError( 155 | 'Invalid recovery code, is there a typo?'); 156 | } 157 | return result.slice(1, -2); 158 | }; 159 | 160 | 161 | }); // goog.scope 162 | -------------------------------------------------------------------------------- /chrome/util/recoverycode_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Tests for the code implementation. 19 | */ 20 | 21 | goog.require('e2e.error.InvalidArgumentsError'); 22 | goog.require('e2email.util.RecoveryCode'); 23 | 24 | goog.scope(function() { 25 | 26 | 27 | describe('SecretCode', function() { 28 | 29 | // Check that encode and decode both behave as 30 | // expected. 31 | function verify(expected_code, expected_array) { 32 | expect(e2email.util.RecoveryCode.encode(expected_array)) 33 | .toBe(expected_code); 34 | expect(e2email.util.RecoveryCode.decode(expected_code)) 35 | .toEqual(expected_array); 36 | } 37 | 38 | it('should transform', function() { 39 | // zero-length byte arrays should work. 40 | verify('b4N', []); 41 | // Handle single-byte extremes. 42 | verify('2V1N4', [0]); 43 | verify('3xhB4', [255]); 44 | // A multi-byte sequence. 45 | verify('3CyypSi3r', [1, 2, 3, 4]); 46 | }); 47 | 48 | it('should catch bad encodings', function() { 49 | // byte array values should be between 0 and 255 50 | expect(function() { 51 | e2email.util.RecoveryCode.encode([256]); 52 | }).toThrow(new e2e.error.InvalidArgumentsError( 53 | 'Input should be a byte array.')); 54 | expect(function() { 55 | e2email.util.RecoveryCode.encode([-1]); 56 | }).toThrow(new e2e.error.InvalidArgumentsError( 57 | 'Input should be a byte array.')); 58 | // base58 should always have the first byte as 1 (the version_number.) 59 | expect(function() { 60 | e2email.util.RecoveryCode.decode('31'); 61 | }).toThrow(new e2e.error.InvalidArgumentsError( 62 | 'This is not a recovery code.')); 63 | // Checksums errors should be detected. 64 | expect(function() { 65 | e2email.util.RecoveryCode.decode('b4M'); 66 | }).toThrow(new e2e.error.InvalidArgumentsError( 67 | 'Invalid recovery code, is there a typo?')); 68 | }); 69 | 70 | 71 | }); 72 | 73 | }); // goog.scope 74 | -------------------------------------------------------------------------------- /chrome/util/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | /** 19 | * @fileoverview Types used across the app. 20 | */ 21 | goog.provide('e2email.util.Progress'); 22 | 23 | 24 | /** 25 | * Object that can be updated within asynchronous operations 26 | * to give some indication of its progress. 27 | * @typedef {{status: ?string}} 28 | */ 29 | e2email.util.Progress; 30 | -------------------------------------------------------------------------------- /chrome/worker/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview OpenPGP Context WebWorker bootstrap file. 19 | */ 20 | 21 | goog.provide('e2email.worker.bootstrap'); 22 | 23 | goog.require('e2e.async.Result'); 24 | goog.require('e2e.async.WorkerSelf'); 25 | goog.require('e2e.openpgp.ContextImpl'); 26 | goog.require('e2e.openpgp.ContextService'); 27 | goog.require('e2email.worker.IndexedDbStorage'); 28 | 29 | 30 | /** 31 | * Bootstrap function for the code in the Web Worker. 32 | */ 33 | e2email.worker.bootstrap = function() { 34 | var contextPromise = new e2e.async.Result(); 35 | new e2email.worker.IndexedDbStorage('keyring', function(storage) { 36 | contextPromise.callback(new e2e.openpgp.ContextImpl(storage)); 37 | }); 38 | var workerPeer = new e2e.async.WorkerSelf(); 39 | e2e.openpgp.ContextService.launch(workerPeer, contextPromise); 40 | }; 41 | 42 | e2email.worker.bootstrap(); 43 | -------------------------------------------------------------------------------- /chrome/worker/indexeddbstorage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2016 E2EMail authors. All rights reserved. 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview Implements a storage mechanism using IndexedDB. 19 | */ 20 | 21 | goog.provide('e2email.worker.IndexedDbStorage'); 22 | 23 | goog.require('goog.db'); 24 | goog.require('goog.db.Transaction'); 25 | goog.require('goog.storage.mechanism.IterableMechanism'); 26 | goog.require('goog.structs.Map'); 27 | 28 | 29 | 30 | /** 31 | * Creates an iterable mechanism that uses IndexedDB for persistence. 32 | * @param {string} key Key in the database to use for persisting data. 33 | * @param {function(!e2email.worker.IndexedDbStorage)=} opt_callback Callback 34 | * function to call once storage mechanism has been initialized. 35 | * @constructor 36 | * @extends {goog.storage.mechanism.IterableMechanism} 37 | * @final 38 | */ 39 | e2email.worker.IndexedDbStorage = function(key, opt_callback) { 40 | /** 41 | * Internal storage object. 42 | * @type {Object} 43 | * @private 44 | */ 45 | this.storage_ = {}; 46 | /** 47 | * @type {!goog.db.IndexedDb} 48 | * @private 49 | */ 50 | this.db_; 51 | /** 52 | * @type {string} 53 | * @private 54 | */ 55 | this.storageKey_ = key; 56 | 57 | goog.db.openDatabase( 58 | e2email.worker.IndexedDbStorage.DB_NAME_, 59 | e2email.worker.IndexedDbStorage.DB_VERSION_, 60 | function(ev, db, tx) { 61 | db.createObjectStore(e2email.worker.IndexedDbStorage.OBJECTSTORE_NAME_); 62 | }).addCallback(function(db) { 63 | this.db_ = db; 64 | this.load_(opt_callback); 65 | }, this); 66 | }; 67 | goog.inherits(e2email.worker.IndexedDbStorage, 68 | goog.storage.mechanism.IterableMechanism); 69 | 70 | 71 | /** 72 | * Name of the object store in the IndexedDB database. 73 | * @private 74 | * @const {string} 75 | */ 76 | e2email.worker.IndexedDbStorage.OBJECTSTORE_NAME_ = 'store'; 77 | 78 | 79 | /** 80 | * Name of the IndexedDB database. 81 | * @private 82 | * @const {string} 83 | */ 84 | e2email.worker.IndexedDbStorage.DB_NAME_ = 'e2email.worker.IndexedDbStorage'; 85 | 86 | 87 | /** 88 | * Version of the IndexedDB database. 89 | * @private 90 | * @const {number} 91 | */ 92 | e2email.worker.IndexedDbStorage.DB_VERSION_ = 1; 93 | 94 | 95 | /** 96 | * Initializes internal storage with the data in IndexedDB. 97 | * @param {function(!e2email.worker.IndexedDbStorage)=} opt_callback 98 | * @private 99 | */ 100 | e2email.worker.IndexedDbStorage.prototype.load_ = function( 101 | opt_callback) { 102 | var getTx = this.db_.createTransaction( 103 | [e2email.worker.IndexedDbStorage.OBJECTSTORE_NAME_]); 104 | var request = getTx. 105 | objectStore(e2email.worker.IndexedDbStorage.OBJECTSTORE_NAME_). 106 | get(this.storageKey_); 107 | request.addCallback(function(result) { 108 | this.storage_ = result || {}; 109 | opt_callback && opt_callback(this); 110 | }, this); 111 | }; 112 | 113 | 114 | /** 115 | * Persists the data in internal storage into IndexedDB. 116 | * @private 117 | */ 118 | e2email.worker.IndexedDbStorage.prototype.persist_ = function() { 119 | var putTx = this.db_.createTransaction( 120 | [e2email.worker.IndexedDbStorage.OBJECTSTORE_NAME_], 121 | goog.db.Transaction.TransactionMode.READ_WRITE); 122 | putTx. 123 | objectStore(e2email.worker.IndexedDbStorage.OBJECTSTORE_NAME_). 124 | put(this.storage_, this.storageKey_); 125 | }; 126 | 127 | 128 | /** @override */ 129 | e2email.worker.IndexedDbStorage.prototype.set = function(key, value) { 130 | this.storage_[key] = value; 131 | this.persist_(); 132 | }; 133 | 134 | 135 | /** @override */ 136 | e2email.worker.IndexedDbStorage.prototype.get = function(key) { 137 | return this.storage_[key]; 138 | }; 139 | 140 | 141 | /** @override */ 142 | e2email.worker.IndexedDbStorage.prototype.remove = function(key) { 143 | delete this.storage_[key]; 144 | this.persist_(); 145 | }; 146 | 147 | 148 | /** @override */ 149 | e2email.worker.IndexedDbStorage.prototype.__iterator__ = function(opt_keys) { 150 | return new goog.structs.Map(this.storage_).__iterator__(opt_keys); 151 | }; 152 | -------------------------------------------------------------------------------- /do.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | set -o nounset 15 | set -o errexit 16 | set -o pipefail 17 | 18 | cd ${0%/*} 19 | 20 | readonly PYTHON_CMD="${PYTHON_CMD:-python}" 21 | readonly JAVA_CMD="${JAVA_CMD:-java}" 22 | readonly NPM_CMD="${NPM_CMD:-npm}" 23 | readonly NODEJS_CMD="${NODEJS_CMD:-node}" 24 | readonly LINT_CMD="${LINT_CMD:-gjslint}" 25 | readonly JSCOMPILE="${JAVA_CMD} -jar chrome-lib/end-to-end/lib/closure-compiler/build/compiler.jar --flagfile=chrome/compiler.flags" 26 | readonly BUILD="build" 27 | readonly CHROME_BUILD="${BUILD}/e2email" 28 | readonly LIB="chrome-lib" 29 | readonly E2ELIB="${LIB}/end-to-end" 30 | 31 | e2email_setup() { 32 | git submodule init 33 | git submodule update --recursive 34 | chrome-lib/end-to-end/do.sh install_deps 35 | } 36 | 37 | e2email_assert_npm() { 38 | # Check if nodejs and npm are installed. 39 | type "$NODEJS_CMD" >/dev/null 2>&1 || { echo >&2 "Please install nodejs to run tests."; exit 1; } 40 | type "$NPM_CMD" >/dev/null 2>&1 || { echo >&2 "Please install npm to run tests."; exit 1; } 41 | } 42 | 43 | e2email_assert_node_modules() { 44 | # Check or download required modules for karma 45 | ${NPM_CMD} install 46 | } 47 | 48 | e2email_check() { 49 | chrome-lib/end-to-end/do.sh check_deps 50 | } 51 | 52 | e2email_generate_jsdeps() { 53 | local deps=${1:-""} 54 | local src=${2:-""} 55 | local roots=( \ 56 | ${E2ELIB}/lib/zlib.js \ 57 | ${E2ELIB}/src/javascript/crypto/e2e ) 58 | if [[ -n "$src" && -d "$src" ]]; then 59 | roots+=("$src") 60 | fi 61 | local depscmd="$PYTHON_CMD ${E2ELIB}/lib/closure-library/closure/bin/build/depswriter.py" 62 | for var in "${roots[@]}" 63 | do 64 | depscmd+=" --root=${var}" 65 | done 66 | $depscmd > "${deps}" 67 | } 68 | 69 | e2email_jscompile() { 70 | local out=$1 71 | local entry=$2 72 | local extra=$3 73 | local src_dirs=( \ 74 | ${E2ELIB}/src \ 75 | ${E2ELIB}/lib/closure-library/closure/goog \ 76 | ${E2ELIB}/lib/closure-library/third_party/closure/goog \ 77 | ${E2ELIB}/lib/zlib.js/src \ 78 | ${E2ELIB}/lib/typedarray ) 79 | 80 | local jscompile_e2email="${JSCOMPILE} ${extra}" 81 | for var in "${src_dirs[@]}" 82 | do 83 | jscompile_e2email+=" --js='$var/**.js'" 84 | done 85 | local exclude_dirs=( \ 86 | ${E2ELIB}/lib/closure-library/closure/goog/demos \ 87 | ${E2ELIB}/src/javascript/crypto/e2e/compatibility_tests \ 88 | ${E2ELIB}/src/javascript/crypto/e2e/extension ) 89 | for var in "${exclude_dirs[@]}" 90 | do 91 | jscompile_e2email+=" --js='!${var}/**.js'" 92 | done 93 | jscompile_e2email+=" --closure_entry_point ${entry} --js_output_file ${out}" 94 | # jscompile_e2email+=" --debug --formatting=PRETTY_PRINT -O WHITESPACE_ONLY" 95 | ${jscompile_e2email} 96 | } 97 | 98 | e2email_build_worker() { 99 | local debug=${1:-no} 100 | local extra=" --js='chrome/worker/**.js' --define=e2e.openpgp.ContextImpl.KEY_SERVER_URL='https://hkpserverext.appspot.com' --define=e2e.openpgp.ContextImpl.KEY_SERVER_URL='https://hkpserverext.appspot.com' --js ${BUILD}/e2e-deps.js" 101 | if [[ ${debug} == "debug" ]]; then 102 | extra+=" --debug --formatting=PRETTY_PRINT -O WHITESPACE_ONLY" 103 | fi 104 | e2email_jscompile "${CHROME_BUILD}/assets/js/worker_binary.js" "e2email.worker.bootstrap" "${extra}" 105 | } 106 | 107 | e2email_build_app() { 108 | local debug=${1:-no} 109 | local extra=" --js='chrome/**.js' --js='!chrome/worker/**.js' --js='!chrome/assets/**.js' --js='!chrome/background.js' --js='!chrome/karma/**.js' --define=e2email.components.openpgp.OpenPgpService.KEYSERVER_URL='https://hkpserverext.appspot.com' --define=e2email.components.openpgp.OpenPgpService.WORKER_BINARY_PATH='assets/js/worker_binary.js' --angular_pass=true --js ${BUILD}/e2email-deps.js" 110 | if [[ ${debug} == "debug" ]]; then 111 | extra+=" --debug --formatting=PRETTY_PRINT -O WHITESPACE_ONLY" 112 | fi 113 | e2email_jscompile "${CHROME_BUILD}/e2email_binary.js" "e2email.application.module" "${extra}" 114 | } 115 | 116 | e2email_build() { 117 | local debug=${1:-no} 118 | e2email_check 119 | echo "Building e2email app to ${CHROME_BUILD}" 120 | rm -rf "${BUILD}" 121 | mkdir -p "${CHROME_BUILD}/assets/js" 122 | e2email_generate_jsdeps "${BUILD}/e2e-deps.js" 123 | e2email_generate_jsdeps "${BUILD}/e2email-deps.js" "chrome" 124 | 125 | echo "Building worker..." 126 | e2email_build_worker ${debug} 127 | echo "Building app..." 128 | e2email_build_app ${debug} 129 | 130 | echo "Copying assets..." 131 | # Copy assets 132 | cp -pr chrome/assets \ 133 | chrome/manifest.json \ 134 | chrome/_locales \ 135 | chrome/background.js \ 136 | "${CHROME_BUILD}" 137 | # Copy html 138 | pushd chrome > /dev/null 139 | local html=$(find . -name \*html) 140 | popd > /dev/null 141 | tar -C chrome -cf - ${html} | tar -C "${CHROME_BUILD}" -xf - 142 | # Copy css 143 | find chrome -name assets -prune -o -type f -name \*css -exec cat '{}' \; > ${CHROME_BUILD}/assets/css/styles-bundle.css 144 | echo "Unpacked app available at ${CHROME_BUILD}" 145 | } 146 | 147 | e2email_clean() { 148 | if [[ -x ${E2ELIB}/do.sh ]]; then 149 | ${E2ELIB}/do.sh clean 150 | fi 151 | rm -rf ${BUILD} 152 | rm -rf node_modules 153 | } 154 | 155 | e2email_prepare_karma() { 156 | e2email_assert_npm 157 | e2email_check 158 | e2email_assert_node_modules 159 | echo Building debug version of app... 160 | e2email_build 161 | } 162 | 163 | e2email_karma() { 164 | e2email_prepare_karma 165 | ${NODEJS_CMD} ./node_modules/karma/bin/karma start chrome/karma/karma.conf.js 166 | } 167 | 168 | e2email_test_app() { 169 | e2email_prepare_karma 170 | ${NODEJS_CMD} ./node_modules/karma/bin/karma --single-run --browsers Chrome start chrome/karma/karma.conf.js 171 | } 172 | 173 | e2email_lint() { 174 | type "$LINT_CMD" >/dev/null 2>&1 || { echo >&2 "Please install Closure Linter (https://developers.google.com/closure/utilities/docs/linter_howto) first."; exit 1; } 175 | 176 | ${LINT_CMD} --strict --closurized_namespaces=goog,e2e --limited_doc_files=_test.js -r chrome -e chrome/assets,chrome/karma 177 | } 178 | 179 | readonly CMD=${1:-help} 180 | 181 | case "$CMD" in 182 | build) 183 | e2email_build; 184 | ;; 185 | clean) 186 | e2email_clean; 187 | ;; 188 | karma) 189 | e2email_karma; 190 | ;; 191 | lint) 192 | e2email_lint; 193 | ;; 194 | setup) 195 | e2email_setup; 196 | ;; 197 | test_app) 198 | e2email_test_app; 199 | ;; 200 | *) 201 | echo "Usage: $0 {build|clean|karma|lint|setup|test_app}" 202 | exit 1 203 | esac 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2email", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/e2email-org/e2email" 6 | }, 7 | "devDependencies": { 8 | "jasmine-core": "^2.4.1", 9 | "karma": "^0.13.19", 10 | "karma-chrome-launcher": "^0.2.2", 11 | "karma-html2js-preprocessor": "^0.1.0", 12 | "karma-jasmine": "^0.3.7", 13 | "karma-ng-html2js-preprocessor": "^0.2.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /screenshots/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/screenshots/basic.png -------------------------------------------------------------------------------- /screenshots/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2email-org/e2email/434f99c66efe491bee58e69aabf585d0598c9bf6/screenshots/icon.png --------------------------------------------------------------------------------