├── .babelrc
├── .dev.sample.json
├── .github
├── CODEOWNERS
├── FUNDING.yml
├── changelog_configuration.json
└── workflows
│ ├── lint.yaml
│ └── release.yaml
├── .gitignore
├── .jshintrc
├── .nvmrc
├── CHANGELOG
├── DEVELOPMENT.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── reviewers
├── firefox-beta.md
└── firefox.md
├── scripts
├── build-zip.js
└── generateBuildConfig.js
├── src
├── background
│ ├── context-menu.js
│ ├── create-alias.js
│ ├── index.js
│ └── onboarding.js
├── content_script
│ ├── input_tools.css
│ └── input_tools.js
├── icons
│ ├── icon_128.png
│ ├── icon_16.png
│ ├── icon_32.png
│ ├── icon_48.png
│ ├── icon_96.png
│ ├── icon_beta_128.png
│ └── icon_beta_48.png
├── images
│ ├── arrow-up.png
│ ├── back-button.svg
│ ├── chrome-permission-screenshot.png
│ ├── firefox-permission-screenshot.png
│ ├── horizontal-logo.svg
│ ├── icon-copy.svg
│ ├── icon-dropdown.svg
│ ├── icon-more.svg
│ ├── icon-puzzle.png
│ ├── icon-settings.svg
│ ├── icon-simplelogin.png
│ ├── icon-trash.svg
│ ├── loading-three-dots.svg
│ ├── loading.svg
│ ├── proton.svg
│ └── sl-button-demo.png
├── manifest.json
└── popup
│ ├── APIService.js
│ ├── App-color.scss
│ ├── App-scrollbar.scss
│ ├── App.scss
│ ├── App.vue
│ ├── EventManager.js
│ ├── Navigation.js
│ ├── SLStorage.js
│ ├── Theme.js
│ ├── Theme.scss
│ ├── Utils.js
│ ├── buildConfig.json
│ ├── components
│ ├── AliasMoreOptions.vue
│ ├── ApiKeySetting.vue
│ ├── AppSettings.vue
│ ├── ExpandTransition.vue
│ ├── Header.vue
│ ├── Login.vue
│ ├── Main.vue
│ ├── NewAliasResult.vue
│ ├── ReverseAlias.vue
│ ├── SelfHostSetting.vue
│ ├── SplashScreen.vue
│ └── TextareaAutosize.vue
│ ├── popup.html
│ └── popup.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "@babel/plugin-proposal-optional-chaining"
4 | ],
5 | "presets": [
6 | ["@babel/preset-env", {
7 | "useBuiltIns": "usage",
8 | "corejs": 3,
9 | "targets": {
10 | // https://jamie.build/last-2-versions
11 | "browsers": ["> 0.25%", "not ie 11", "not op_mini all"]
12 | }
13 | }]
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.dev.sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "DEFAULT_API_URL": "https://app.simplelogin.io",
3 | "EXTRA_ALLOWED_DOMAINS": [],
4 | "permissions": []
5 | }
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | ## code changes will send PR to following users
2 | * @acasajus @cquintana92 @nguyenkims
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: simplelogin
2 | open_collective: simplelogin
3 | custom: ["https://www.paypal.me/RealSimpleLogin"]
4 |
--------------------------------------------------------------------------------
/.github/changelog_configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "template": "${{CHANGELOG}}",
3 | "pr_template": "- ${{TITLE}} #${{NUMBER}}",
4 | "empty_template": "- no changes",
5 | "categories": [
6 | {
7 | "title": "## 🚀 Features",
8 | "labels": ["feature"]
9 | },
10 | {
11 | "title": "## 🐛 Fixes",
12 | "labels": ["fix", "bug"]
13 | },
14 | {
15 | "title": "## 🔧 Enhancements",
16 | "labels": ["enhancement"]
17 | }
18 | ],
19 | "ignore_labels": ["ignore"],
20 | "tag_resolver": {
21 | "method": "sort"
22 | }
23 | }
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | lint:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: Checkout repository
9 | uses: actions/checkout@v2
10 |
11 | - name: Install NodeJS
12 | uses: actions/setup-node@v3
13 | with:
14 | node-version: 16
15 |
16 | - name: Perform linting
17 | run: |
18 | npm install
19 | npm run prettier:check
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Build the extension
2 | on:
3 | push:
4 | tags:
5 | - '[0-9]+.[0-9]+.[0-9]+'
6 |
7 | jobs:
8 | create-release:
9 | name: create-release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 1
16 |
17 | - name: Install dependencies and check tag format
18 | run: |
19 | sudo apt update && sudo apt install -y jq
20 | PACKAGE_JSON_VERSION=$(jq -Mr '.version' package.json)
21 | EXTENSION_VERSION=${GITHUB_REF#refs/tags/}
22 | if [[ "${PACKAGE_JSON_VERSION}" != "${EXTENSION_VERSION}" ]]; then
23 | echo "Tag name does not match the version in package.json"
24 | echo "package.json: [${PACKAGE_JSON_VERSION}] | tag: [${EXTENSION_VERSION}]"
25 | exit 1
26 | fi
27 | echo "EXTENSION_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
28 |
29 | - name: Create artifacts directory
30 | run: mkdir artifacts
31 |
32 | - name: Build Changelog
33 | id: build_changelog
34 | uses: mikepenz/release-changelog-builder-action@v3
35 | with:
36 | configuration: ".github/changelog_configuration.json"
37 | env:
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 |
40 | - name: Prepare Slack notification contents
41 | run: |
42 | changelog=$(cat << EOH
43 | ${{ steps.build_changelog.outputs.changelog }}
44 | EOH
45 | )
46 | messageWithoutNewlines=$(echo "${changelog}" | awk '{printf "%s\\n", $0}')
47 | messageWithoutDoubleQuotes=$(echo "${messageWithoutNewlines}" | sed "s/\"/'/g")
48 | echo "${messageWithoutDoubleQuotes}"
49 |
50 | echo "${messageWithoutDoubleQuotes}" > artifacts/slack-changelog
51 |
52 | - name: Create GitHub release
53 | id: release
54 | uses: actions/create-release@v1
55 | env:
56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57 | with:
58 | tag_name: ${{ env.EXTENSION_VERSION }}
59 | release_name: ${{ env.EXTENSION_VERSION }}
60 | body: ${{ steps.build_changelog.outputs.changelog }}
61 |
62 | - name: Save release upload URL to artifact
63 | run: echo "${{ steps.release.outputs.upload_url }}" > artifacts/release-upload-url
64 |
65 | - name: Save version number to artifact
66 | run: echo "${{ env.EXTENSION_VERSION }}" > artifacts/release-version
67 |
68 | - name: Upload artifacts
69 | uses: actions/upload-artifact@v4
70 | with:
71 | name: artifacts
72 | path: artifacts
73 |
74 | build-release:
75 | name: build-release
76 | needs: ['create-release']
77 | runs-on: ubuntu-latest
78 | strategy:
79 | max-parallel: 4
80 | matrix:
81 | variant: ['full', 'lite', 'mac', 'beta', 'beta-firefox']
82 |
83 | steps:
84 | - name: Build info
85 | run: echo "Starting build process for variant = ${{ matrix.variant }}"
86 |
87 | - name: Checkout repository
88 | uses: actions/checkout@v4
89 | with:
90 | fetch-depth: 1
91 |
92 | - name: Install NodeJS
93 | uses: actions/setup-node@v3
94 | with:
95 | node-version: 16
96 |
97 | - name: Get release download URL
98 | uses: actions/download-artifact@v4
99 | with:
100 | name: artifacts
101 | path: artifacts
102 |
103 | - name: Read artifacts
104 | shell: bash
105 | run: |
106 | # Set the release_upload_url
107 | release_upload_url="$(cat artifacts/release-upload-url)"
108 | echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV
109 | echo "release upload url: $RELEASE_UPLOAD_URL"
110 |
111 | # Set the release_version
112 | release_version="$(cat artifacts/release-version)"
113 | echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
114 | echo "release version: $RELEASE_VERSION"
115 |
116 | - name: Generate buildConfig
117 | shell: bash
118 | env:
119 | ENABLE_LOGIN_WITH_PROTON: true
120 | run: |
121 | npm run generate:buildconfig
122 |
123 | - name: Build lite version
124 | if: matrix.variant == 'lite'
125 | shell: bash
126 | run: |
127 | npm install
128 | npm run build:lite
129 | SUFFIX=lite npm run build-zip
130 |
131 | - name: Build beta version for Chromium
132 | if: matrix.variant == 'beta'
133 | shell: bash
134 | run: |
135 | npm install
136 | npm run build:beta
137 | npm run build-zip
138 |
139 | - name: Build beta version for Firefox
140 | if: matrix.variant == 'beta-firefox'
141 | shell: bash
142 | run: |
143 | npm install
144 | npm run build:firefox:beta
145 |
146 | # dist-zip/simplelogin-extension-beta-firefox-v... can be submitted to firefox
147 | SUFFIX=firefox npm run build-zip
148 |
149 | - name: Build production version for Firefox
150 | if: matrix.variant == 'beta-firefox'
151 | shell: bash
152 | run: |
153 | npm install
154 | npm run build:firefox
155 |
156 | # dist-zip/simplelogin-extension-beta-firefox-v... can be submitted to firefox
157 | SUFFIX=firefox npm run build-zip
158 |
159 | - name: Build production version for Chromium
160 | if: matrix.variant == 'full'
161 | shell: bash
162 | run: |
163 | npm install
164 | npm run build
165 | npm run build-zip
166 |
167 | - name: Build Mac version
168 | if: matrix.variant == 'mac'
169 | shell: bash
170 | run: |
171 | npm install
172 | npm run build:mac
173 | SUFFIX=mac npm run build-zip
174 |
175 | - name: Package extension
176 | run: |
177 | ZIP_NAME=$(find dist-zip -type f -name '*.zip' | head -n 1)
178 | ASSET_NAME=$(basename "${ZIP_NAME}")
179 |
180 | echo "ASSET_PATH=${ZIP_NAME}" >> $GITHUB_ENV
181 | echo "ASSET_NAME=${ASSET_NAME}" >> $GITHUB_ENV
182 |
183 | - name: Upload release archive
184 | uses: actions/upload-release-asset@v1.0.1
185 | env:
186 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
187 | with:
188 | upload_url: ${{ env.RELEASE_UPLOAD_URL }}
189 | asset_path: ${{ env.ASSET_PATH }}
190 | asset_name: ${{ env.ASSET_NAME }}
191 | asset_content_type: application/octet-stream
192 |
193 | notify:
194 | name: notify
195 | needs: ['create-release', 'build-release']
196 | runs-on: ubuntu-latest
197 | steps:
198 | - name: Get artifacts
199 | uses: actions/download-artifact@v4
200 | with:
201 | name: artifacts
202 | path: artifacts
203 |
204 | - name: Read artifacts
205 | shell: bash
206 | run: |
207 | # Set the slack-changelog
208 | slack_changelog=$(cat artifacts/slack-changelog)
209 | echo "SLACK_CHANGELOG=$slack_changelog" >> $GITHUB_ENV
210 | echo "slack changelog: $SLACK_CHANGELOG"
211 |
212 | - name: Post notification to Slack
213 | uses: slackapi/slack-github-action@v1.19.0
214 | with:
215 | channel-id: ${{ secrets.SLACK_CHANNEL_ID }}
216 | payload: |
217 | {
218 | "blocks": [
219 | {
220 | "type": "header",
221 | "text": {
222 | "type": "plain_text",
223 | "text": "New tag created on browser-extension",
224 | "emoji": true
225 | }
226 | },
227 | {
228 | "type": "section",
229 | "text": {
230 | "type": "mrkdwn",
231 | "text": "*Tag: ${{ github.ref_name }}* (${{ github.sha }})"
232 | }
233 | },
234 | {
235 | "type": "section",
236 | "text": {
237 | "type": "mrkdwn",
238 | "text": "*Changelog:*\n${{ env.SLACK_CHANGELOG }}"
239 | }
240 | }
241 | ]
242 | }
243 | env:
244 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /*.log
3 | /dist
4 | /dist-zip
5 | .DS_Store
6 | .idea/
7 | .dev.json
8 | code-for-reviewer/
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esversion": 9
3 | }
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.20.2
2 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to SimpleLogin Chrome/Firefox extension will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [3.0.7] - 2025-03-20
11 |
12 | - Do not try to rerun extension setup if already logged in
13 |
14 | ## [3.0.6] - 2024-09-04
15 |
16 | - Fix onboarding on Safari
17 |
18 | ## [3.0.1-3.0.4] - 2024-04-11
19 |
20 | - Automate firefox review assets
21 |
22 | ## [3.0.0] - 2024-04-11
23 |
24 | - Use Manifest v3
25 |
26 | ## [2.11.1] - 2024-02-22
27 | - Fix alias list not displaying on Mac
28 |
29 | ## [2.11.0] - 2024-02-15
30 | - Fix upgrade button not working
31 | - Prepare the Safari extension
32 | - Enforce the reverse alias limit
33 |
34 | ## [2.10.2] - 2023-09-18
35 |
36 | - Use internal Sentry
37 |
38 | ## [2.10.0] - 2023-07-05
39 |
40 | - Fix sl-button class conflict
41 |
42 | ## [2.9.2] - 2022-11-10
43 | - Updated gecko ID for Firefox
44 |
45 | ## [2.9.1] - 2022-11-10
46 |
47 | - Support dark and OS theme
48 | - Support communication with future Mac app
49 |
50 | ## [2.7.0] - 2022-08-02
51 |
52 | - Add "Login with Proton"
53 | - Show user info on app settings page
54 | - Remove tabs permission
55 | - Fix the "create totally random alias" button includes hostname in alias
56 | - Upgrade dependencies
57 |
58 |
59 | ## [2.6.3] - 2022-05-20
60 |
61 | - Remove cookies permission
62 |
63 | ## [2.6.2] - 2022-05-20
64 |
65 | - Fixed CI
66 |
67 | ## [2.6.1] - 2022-05-20
68 |
69 | - Add version to settings page
70 | - Improve the CI
71 |
72 | ## [2.6.0] - 2022-05-20
73 |
74 | - Require previously optional permissions on install
75 | - Improve the onboarding
76 | - Update outdated packages
77 |
78 | ## [2.5.2] - 2021-10-28
79 |
80 | Fix icon not displayed
81 |
82 | ## [2.5.1] - 2021-10-28
83 |
84 | Provide additional size icons
85 |
86 | ## [2.5.0] - 2021-10-05
87 | - Support create reverse-alias
88 | - Improve UI
89 | - Update dependencies
90 |
91 | ## [2.4.3] - 2021-08-03
92 | - Support dot sign (.) in alias prefix
93 | - Update some wordings, improve UI
94 |
95 |
96 | ## [2.4.2] - 2020-09-26
97 | - Fix onboarding screen showing up on browser restart
98 |
99 | ## [2.4.1] - 2020-09-26
100 | - Reduce display glitch on Firefox
101 |
102 | ## [2.4.0] - 2020-09-26
103 | - Ctrl+Shift+S (or Cmd+Shift+S on MacOS) to open SimpleLogin pop-up
104 | - Ctrl+Shift+X (or Cmd+Shift+X on MacOS) to create a random alias
105 | - Fix window not displayed when a new alias is created
106 |
107 | ## [2.3.1] - 2020-09-07
108 | - Fix the hostname issue
109 |
110 | ## [2.3.0] - 2020-09-05
111 | - Better onboarding process
112 |
113 | ## [2.2.0] - 2020-09-03
114 | - Fix Firefox overflow menu
115 | - Better onboarding process
116 | - Do not require tabs permission.
117 |
118 | ## [2.1.0] - 2020-08-21
119 | - Improve error alert
120 |
121 | ## [2.1.0.0] - 2020-08-13 (Beta version)
122 | - Right click to create alias
123 | - Able to change alias from name, toggle PGP
124 | - Able to choose alias's mailbox(es)
125 | - UI improved
126 |
127 | ## [2.0.0.0] - 2020-07-30 (Beta version)
128 | - Create alias via the SimpleLogin button displayed in the email field
129 |
130 | ## [1.10.0] - 2020-07-16
131 | - Able to add and edit alias note
132 | - Lots of UI touches 🎨
133 |
134 | ## [1.9.1] - 2020-07-12
135 | - Take into account the case alias creation time is expired
136 |
137 | ## [1.9.0] - 2020-07-11
138 | - Able to enable/disable an alias
139 | - Able to delete an alias
140 |
141 | ## [1.8.0] - 2020-07-08
142 | - Big refactoring to make the code more modulable
143 | - Add some UI touches & fixes: log out button on top, navigation button, etc.
144 |
145 | ## [1.7.0] - 2020-07-04
146 | You can now login with email/password!
147 |
148 | ## [1.6.0] - 2020-06-24
149 | Handle 429 error.
150 | Fix Firefox scroll bar
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | # DEVELOPMENT
2 |
3 | This document contains an overview on how the extension is organized, which parts does it have and how does it work.
4 |
5 | ## General overview
6 |
7 | The extension consists of 2 main screens:
8 |
9 | - main screen: displays email alias recommendation, alias creation and existing alias.
10 | - new alias screen: when a new alias is created, user is redirected to this screen so they can copy it.
11 |
12 |
13 | ## How to change the domain where the extension is connecting to
14 |
15 | In order to change the backend URL, you will need to:
16 |
17 | 1. Copy the `.dev.sample.json` file into a `.dev.json` file.
18 | 2. Edit the `DEFAULT_API_URL` parameter and enter the URL you want to use.
19 | 3. You may need to run `npm start` again in order for the changes to take effect.
20 |
21 | ## How does the extension setup work
22 |
23 | The extension setup process works like the following:
24 |
25 | 1. The webpage sends a message (the code can be found [here](https://github.com/simple-login/app/blob/0e3be23acc7978f6e2b1127ed78dc2147cf43515/templates/onboarding/index.html#L41-L42) and [here](https://github.com/simple-login/app/blob/0e3be23acc7978f6e2b1127ed78dc2147cf43515/templates/onboarding/setup_done.html#L31-L32))
26 | 2. The extension has a listener for events on the page, and detects it [like this](https://github.com/simple-login/browser-extension/blob/55629849838b716dabcb008898c97c4ee1118da1/src/content_script/input_tools.js#L257)
27 | 3. Once the event has been detected, the extension sends it to the background context [with this call](https://github.com/simple-login/browser-extension/blob/55629849838b716dabcb008898c97c4ee1118da1/src/content_script/input_tools.js#L260)
28 | 4. The background context [detects the event](https://github.com/simple-login/browser-extension/blob/master/src/background/index.js#L119-L120) and performs the setup. This message can only come from one of the authorized domains (see the "Add custom allowed domains" section to see how this works).
29 | 5. The setup consists on a HTTP request that will use the cookies for the SimpleLogin domain, and it will receive an API Key in the response. This API Key will be stored on the `SLStorage` and be used from then on.
30 | 6. Once the setup has been done, the user will be redirected to a page where they will be able to test the extension.
31 |
32 | Here you have a full definition of the flow:
33 |
34 | 1. Once the extension is installed, the user will be prompted with a webpage (`/onboarding`) where two things can happen:
35 | 1. If the user is already logged in, the webpage will send the message for performing the extension setup.
36 | 1. Once the setup is done, they will be redirected to the `/onboarding/final` page.
37 | 2. If the user is not logged in, they will be prompted to log in.
38 | 1. After they log in, they will be redirected to the `/onboarding/setup_done` page.
39 | 2. The page will send the message for performing the extension setup.
40 | 3. Once the setup is done, they will be redirected to the `/onboarding/final` page.
41 | 2. Once the user reaches the `/onboarding/final` page, the extension will be correctly set up, the user will be able to use it, and the page will contain the extension version at the bottom of the page
42 |
43 |
44 | ## Add custom allowed domains
45 |
46 | The messages for both performing the extension setup and for checking if it's installed are only allowed if they come from a [predefined set of origins](https://github.com/simple-login/browser-extension/blob/55629849838b716dabcb008898c97c4ee1118da1/src/background/index.js#L72-L77).
47 |
48 | However, for testing purposes there is a parameter that can be added to your dev config. You can find it in your `.dev.json`, under the name `EXTRA_ALLOWED_DOMAINS`.
49 |
50 | Keep in mind that the domains you write here will be converted to regex, so if you want to allow `*.local` you may need to write it as `.*.local`. Also take into account that only the hostname portion will be used (that means, if your page is `someserver.com:1234` only the `someserver.com` portion will be evaluated).
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 SimpleLogin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SimpleLogin Chrome/Firefox extension
2 | ---
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | SimpleLogin is the **open-source** privacy-first email alias and Single Sign-On (SSO) Identity Provider.
19 |
20 | More info on our website at https://simplelogin.io
21 |
22 | The extension uses VueJS with https://github.com/Kocal/vue-web-extension boilerplate.
23 |
24 | ## How to get the extension
25 |
26 | You can directly install the extension by visiting the store page for your browser:
27 |
28 | - [Google Chrome / Brave / Opera / Chromium-based](https://chrome.google.com/webstore/detail/simpleloginreceive-send-e/dphilobhebphkdjbpfohgikllaljmgbn)
29 | - [Mozilla Firefox](https://addons.mozilla.org/firefox/addon/simplelogin/)
30 | - [Microsoft Edge](https://microsoftedge.microsoft.com/addons/detail/simpleloginreceive-sen/diacfpipniklenphgljfkmhinphjlfff)
31 |
32 | ## Development information
33 |
34 | You can find more information about how the extension works and which parts it has in [DEVELOPMENT.md](./DEVELOPMENT.md)
35 |
36 | ## Contributing Guide
37 |
38 | All work on SimpleLogin Chrome/Firefox extension happens directly on GitHub.
39 |
40 | This project has been tested with Node v20.2.0 and NPM 9.6.6
41 |
42 |
43 | To run the extension locally, please first install all dependencies with `npm install`.
44 |
45 | ## Chrome
46 |
47 | Run `npm start` to generate the `/dist` folder that can be installed into Chrome.
48 |
49 | In case of `Error: error:0308010C:digital envelope routines::unsupported` error, the workaround is to accept OPEN SSL by running this command before running `npm start`
50 |
51 | ```bash
52 | export NODE_OPTIONS=--openssl-legacy-provider
53 | ````
54 |
55 | ## Firefox
56 |
57 | Run `npm run start:firefox` to generate the `/dist` folder which can then be installed on Firefox, more info on https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#installing
58 |
59 | ## Code formatting
60 |
61 | The code is formatted using `prettier`, make sure to run it before creating the commit, otherwise the GitHub lint workflow will mark the check as not passing:
62 |
63 | ```bash
64 | npm run prettier:write
65 | ```
66 |
67 | ## How to generate a release
68 |
69 | 1. Increment the version in `package.json`.
70 | 2. Update CHANGELOG with the changes.
71 | 3. Create a tag and push it to the repository. The tag name must match the version set in `package.json`.
72 | 4. Wait until the CI process generates the extension ZIP and uploads it to GitHub. You will be able to find the generated zip as an artifact attached to the [GitHub release](https://github.com/simple-login/browser-extension/releases).
73 | 5. Upload the extension to the Chrome, Firefox and Edge stores.
74 |
75 | ### Firefox
76 |
77 | For Firefox, the code source must be submitted too. To faciliate the review process, the code source can be generated using the following script
78 |
79 | For beta version:
80 |
81 | ```bash
82 | # create the code source for firefox reviewer
83 | rm -rf code-for-reviewer && mkdir code-for-reviewer
84 |
85 | # copy the minimum files
86 | cp -r src LICENSE CHANGELOG scripts package.json package-lock.json webpack.config.js .dev.sample.json .babelrc code-for-reviewer
87 |
88 | # override the readme
89 | cp reviewers/firefox-beta.md code-for-reviewer/README.md
90 | ```
91 |
92 | For prod version
93 |
94 | ```bash
95 | # create the code source for firefox reviewer
96 | rm -rf code-for-reviewer && mkdir code-for-reviewer
97 |
98 | # copy the minimum files
99 | cp -r src LICENSE CHANGELOG scripts package.json package-lock.json webpack.config.js .dev.sample.json .babelrc code-for-reviewer
100 |
101 | # override the readme
102 | cp reviewers/firefox.md code-for-reviewer/README.md
103 | ```
104 |
105 |
106 | ## How to build the extension locally
107 |
108 | In order to build the extension yourself, please follow these steps:
109 |
110 | - Make sure you have the dependencies installed and up-to-date with `npm install`.
111 | - Run the build process with `npm run build`.
112 | - Create the zip package with `npm run build-zip`. You will find the extension in the `dist-zip/` directory.
113 | - If you want to use it on Firefox you will need to enter the `dist/` directory and run `web-ext build`. You will find the extension in the `dist/web-ext-artifacts/` directory.
114 |
115 | - (Optional, only useful for beta build) Build beta version: change `betaRev` in `package.json`, then generate zip file using
116 |
117 | ## How to build a version for Mac
118 |
119 | For the development, you can run `npm run start:mac` for the Mac app.
120 |
121 | For the production release, `npm run build:mac`
122 |
123 | ```bash
124 | npm run build:beta && npm run build-zip
125 | ```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simplelogin-extension",
3 | "version": "3.0.7",
4 | "betaRev": "0",
5 | "description": "SimpleLogin Browser Extension",
6 | "author": "extension@simplelogin.io",
7 | "license": "MIT",
8 | "scripts": {
9 | "prettier": "prettier \"src/**/*.{js,vue}\"",
10 | "prettier:write": "npm run prettier -- --write",
11 | "prettier:check": "prettier --check \"src/**/*.{js,vue}\"",
12 | "build": "cross-env NODE_ENV=production webpack",
13 | "build:dev": "cross-env NODE_ENV=development webpack",
14 | "build:firefox": "cross-env NODE_ENV=production FIREFOX=1 webpack",
15 | "build:firefox:beta": "cross-env NODE_ENV=production BETA=1 FIREFOX=1 webpack",
16 | "build:lite": "cross-env NODE_ENV=production LITE=1 webpack",
17 | "build:beta": "cross-env NODE_ENV=production BETA=1 webpack",
18 | "build:mac": "cross-env NODE_ENV=production MAC=1 webpack",
19 | "build-zip": "node scripts/build-zip.js",
20 | "generate:buildconfig": "node scripts/generateBuildConfig.js",
21 | "start": "cross-env HMR=true NODE_ENV=development BETA=1 webpack -- --watch",
22 | "start:mac": "cross-env HMR=true NODE_ENV=development BETA=1 MAC=1 webpack -- --watch",
23 | "start:firefox": "cross-env HMR=true NODE_ENV=development BETA=1 FIREFOX=1 webpack -- --watch"
24 | },
25 | "dependencies": {
26 | "@fortawesome/fontawesome-svg-core": "^1.2.36",
27 | "@fortawesome/free-solid-svg-icons": "^5.15.4",
28 | "@fortawesome/vue-fontawesome": "^0.1.10",
29 | "@sentry/browser": "^5.25.0",
30 | "@sentry/integrations": "^5.30.0",
31 | "bootstrap": "^4.6.1",
32 | "bootstrap-vue": "^2.21.2",
33 | "browser": "^0.2.6",
34 | "tippy.js": "^6.3.7",
35 | "v-clipboard": "^2.2.3",
36 | "vue": "^2.6.14",
37 | "vue-js-modal": "^1.3.35",
38 | "vue-js-toggle-button": "^1.3.3",
39 | "vue-router": "^3.5.2",
40 | "vue-toasted": "^1.1.28",
41 | "webextension-polyfill": "^0.9.0"
42 | },
43 | "devDependencies": {
44 | "@babel/core": "^7.1.2",
45 | "@babel/plugin-proposal-class-properties": "^7.10.4",
46 | "@babel/plugin-proposal-optional-chaining": "^7.0.0",
47 | "@babel/preset-env": "^7.1.0",
48 | "@babel/runtime-corejs3": "^7.4.0",
49 | "archiver": "^3.0.0",
50 | "babel-loader": "^8.0.2",
51 | "copy-webpack-plugin": "^6.4.1",
52 | "core-js": "^3.22.7",
53 | "cross-env": "^5.2.0",
54 | "css-loader": "^2.1.1",
55 | "ejs": "^3.1.8",
56 | "file-loader": "^6.2.0",
57 | "mini-css-extract-plugin": "^0.4.4",
58 | "prettier": "^2.6.2",
59 | "sass": "^1.52.1",
60 | "sass-loader": "^10.1.1",
61 | "vue-loader": "^15.4.2",
62 | "vue-template-compiler": "^2.6.14",
63 | "web-ext-types": "^2.1.0",
64 | "webpack": "^4.46.0",
65 | "webpack-cli": "^4.9.2"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/reviewers/firefox-beta.md:
--------------------------------------------------------------------------------
1 | SimpleLogin Chrome/Firefox extension
2 | ---
3 |
4 | Please find below the instructions for building the SimpleLogin extension from source.
5 |
6 | This project has been tested with Node v20.2.0 and NPM 9.6.6.
7 |
8 | Please run the following commands to install dependencies and generate a build
9 |
10 | ```bash
11 | export NODE_OPTIONS=--openssl-legacy-provider
12 | npm install
13 | npm run build:firefox:beta
14 | npm run build-zip
15 | ```
16 |
17 | After that the build should be available in `/dist` folder. Its zip file can be found in `dist-zip` folder.
--------------------------------------------------------------------------------
/reviewers/firefox.md:
--------------------------------------------------------------------------------
1 | SimpleLogin Chrome/Firefox extension
2 | ---
3 |
4 | Please find below the instructions for building the SimpleLogin extension from source.
5 |
6 | This project has been tested with Node v20.2.0 and NPM 9.6.6.
7 |
8 | Please run the following commands to install dependencies and generate a build
9 |
10 | ```bash
11 | export NODE_OPTIONS=--openssl-legacy-provider
12 | npm install
13 | npm run build:firefox
14 | npm run build-zip
15 | ```
16 |
17 | After that the build should be available in `/dist` folder. Its zip file can be found in `dist-zip` folder.
--------------------------------------------------------------------------------
/scripts/build-zip.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const process = require('process');
4 | const fs = require('fs');
5 | const path = require('path');
6 | const archiver = require('archiver');
7 |
8 | const DEST_DIR = path.join(__dirname, '../dist');
9 | const DEST_ZIP_DIR = path.join(__dirname, '../dist-zip');
10 |
11 | const extractExtensionData = () => {
12 | const extPackageJson = require('../package.json');
13 | const distManifestJson = require('../dist/manifest.json');
14 | const isBeta = distManifestJson.name.match(/beta/i);
15 | const betaRev = extPackageJson.betaRev;
16 |
17 | return {
18 | name: extPackageJson.name + (isBeta ? '-beta' : '-release'),
19 | version: extPackageJson.version + (isBeta ? ('.' + betaRev) : ''),
20 | };
21 | };
22 |
23 | const makeDestZipDirIfNotExists = () => {
24 | if(!fs.existsSync(DEST_ZIP_DIR)) {
25 | fs.mkdirSync(DEST_ZIP_DIR);
26 | }
27 | };
28 |
29 | const buildZip = (src, dist, zipFilename) => {
30 | console.info(`Building ${zipFilename}...`);
31 |
32 | const archive = archiver('zip', { zlib: { level: 9 }});
33 | const stream = fs.createWriteStream(path.join(dist, zipFilename));
34 |
35 | return new Promise((resolve, reject) => {
36 | archive
37 | .directory(src, false)
38 | .on('error', err => reject(err))
39 | .pipe(stream);
40 |
41 | stream.on('close', () => resolve());
42 | archive.finalize();
43 | });
44 | };
45 |
46 | const extractSuffix = () => {
47 | if (process.env.SUFFIX) {
48 | return `-${process.env.SUFFIX}`;
49 | }
50 | return '';
51 | };
52 |
53 | const main = () => {
54 | const {name, version} = extractExtensionData();
55 | const suffix = extractSuffix();
56 | const zipFilename = `${name}${suffix}-v${version}.zip`;
57 |
58 | makeDestZipDirIfNotExists();
59 |
60 | buildZip(DEST_DIR, DEST_ZIP_DIR, zipFilename)
61 | .then(() => console.info('OK'))
62 | .catch(console.err);
63 | };
64 |
65 | main();
66 |
--------------------------------------------------------------------------------
/scripts/generateBuildConfig.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | const PATH = path.join(__dirname, '../src', 'popup', 'buildConfig.json');
5 |
6 | const isLoginWithProtonEnabled = () => {
7 | const enableLoginWithProton = process.env.ENABLE_LOGIN_WITH_PROTON;
8 | if (enableLoginWithProton == undefined || enableLoginWithProton === 'true') {
9 | return true;
10 | }
11 | return false;
12 | };
13 |
14 | const config = {
15 | features: {
16 | loginWithProtonEnabled: isLoginWithProtonEnabled()
17 | },
18 | buildTime: new Date().getTime()
19 | };
20 |
21 | fs.writeFileSync(PATH, JSON.stringify(config, null, 2));
22 |
--------------------------------------------------------------------------------
/src/background/context-menu.js:
--------------------------------------------------------------------------------
1 | import { handleNewRandomAlias } from "./create-alias";
2 | import browser from "webextension-polyfill";
3 |
4 | function displayAndCopy(alias, error) {
5 | function copyTextToClipboard(text) {
6 | if (!text) return;
7 | var textArea = document.createElement("textarea");
8 | textArea.value = text;
9 |
10 | textArea.style.top = "0";
11 | textArea.style.left = "0";
12 | textArea.style.position = "fixed";
13 |
14 | document.body.appendChild(textArea);
15 | textArea.focus();
16 | textArea.select();
17 |
18 | try {
19 | document.execCommand("copy");
20 | } catch (err) {}
21 |
22 | document.body.removeChild(textArea);
23 | }
24 |
25 | function showSLDialog(message) {
26 | let slDialog = document.createElement("div");
27 | slDialog.style.position = "fixed";
28 | slDialog.style.bottom = "0";
29 | slDialog.style.right = "0";
30 | slDialog.style.margin = "0.7em";
31 | slDialog.style.padding = "0.7em";
32 | slDialog.style.fontFamily = "Verdana, Arial, Helvetica, sans-serif";
33 | slDialog.style.fontSize = "1em";
34 | slDialog.style.pointerEvents = "none";
35 | slDialog.style.zIndex = "999999";
36 | slDialog.style.background = "white";
37 | slDialog.style.border = "2px solid #777";
38 | slDialog.style.borderRadius = "5px";
39 | slDialog.innerText = JSON.stringify(message);
40 |
41 | document.body.appendChild(slDialog);
42 |
43 | setTimeout(function () {
44 | document.body.removeChild(slDialog);
45 | }, 3000);
46 | }
47 |
48 | showSLDialog(alias ? alias + " copied to clipboard" : "ERROR: " + error);
49 | copyTextToClipboard(alias);
50 | }
51 |
52 | function generateAliasHandlerJS(tab, res) {
53 | chrome.scripting
54 | .executeScript({
55 | target: { tabId: tab.id },
56 | func: displayAndCopy,
57 | args: [res.alias || null, res.error || null],
58 | })
59 | .then(() => console.log("injected a function"));
60 | }
61 |
62 | async function handleOnClickContextMenu(info, tab) {
63 | const res = await handleNewRandomAlias(info.pageUrl);
64 | generateAliasHandlerJS(tab, res);
65 | }
66 |
67 | export { handleOnClickContextMenu, generateAliasHandlerJS };
68 |
--------------------------------------------------------------------------------
/src/background/create-alias.js:
--------------------------------------------------------------------------------
1 | import {
2 | callAPI,
3 | API_ROUTE,
4 | API_ON_ERR,
5 | reloadSettings,
6 | } from "../popup/APIService";
7 | import Utils from "../popup/Utils";
8 |
9 | /**
10 | * Create random alias
11 | */
12 | async function handleNewRandomAlias(currentUrl) {
13 | await reloadSettings();
14 | const hostname = await Utils.getHostName(currentUrl);
15 | try {
16 | const res = await callAPI(
17 | API_ROUTE.NEW_RANDOM_ALIAS,
18 | {
19 | hostname,
20 | },
21 | {
22 | note: await Utils.getDefaultNote(),
23 | },
24 | API_ON_ERR.THROW
25 | );
26 |
27 | return res.data;
28 | } catch (err) {
29 | // rate limit reached
30 | if (err.response.status === 429) {
31 | return {
32 | error:
33 | "Rate limit exceeded - please wait 60s before creating new alias",
34 | };
35 | } else if (err.response.data.error) {
36 | return {
37 | error: err.response.data.error,
38 | };
39 | } else {
40 | return {
41 | error: "Unknown error",
42 | };
43 | }
44 | }
45 | }
46 |
47 | export { handleNewRandomAlias };
48 |
--------------------------------------------------------------------------------
/src/background/index.js:
--------------------------------------------------------------------------------
1 | import browser from "webextension-polyfill";
2 | import APIService, { API_ROUTE } from "../popup/APIService";
3 | import SLStorage from "../popup/SLStorage";
4 | import Onboarding from "./onboarding";
5 |
6 | import { handleNewRandomAlias } from "./create-alias";
7 | import {
8 | handleOnClickContextMenu,
9 | generateAliasHandlerJS,
10 | } from "./context-menu";
11 | import Utils from "../popup/Utils";
12 |
13 | /**
14 | * Get app settings
15 | */
16 | async function handleGetAppSettings() {
17 | const apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY);
18 | return {
19 | showSLButton:
20 | apiKey !== "" && (await SLStorage.get(SLStorage.SETTINGS.SHOW_SL_BUTTON)),
21 | isLoggedIn: apiKey !== "",
22 | url: await SLStorage.get(SLStorage.SETTINGS.API_URL),
23 | SLButtonPosition: await SLStorage.get(
24 | SLStorage.SETTINGS.SL_BUTTON_POSITION
25 | ),
26 | };
27 | }
28 |
29 | async function finalizeExtensionSetup(apiKey) {
30 | if (!apiKey) {
31 | return;
32 | }
33 |
34 | await SLStorage.set(SLStorage.SETTINGS.API_KEY, apiKey);
35 |
36 | const currentTab = await browser.tabs.query({
37 | active: true,
38 | currentWindow: true,
39 | });
40 |
41 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
42 | const url = `${apiUrl}/onboarding/final`;
43 | await browser.tabs.update(currentTab[0].id, {
44 | url,
45 | });
46 | }
47 |
48 | async function handleExtensionSetup() {
49 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
50 |
51 | const url = apiUrl + API_ROUTE.GET_API_KEY_FROM_COOKIE.path;
52 | const res = await fetch(url, {
53 | method: "POST",
54 | headers: {
55 | "Content-Type": "application/json",
56 | "X-Sl-Allowcookies": true,
57 | },
58 | body: JSON.stringify({
59 | device: Utils.getDeviceName(),
60 | }),
61 | });
62 |
63 | if (res.ok) {
64 | const apiRes = await res.json();
65 | const apiKey = apiRes.api_key;
66 | finalizeExtensionSetup(apiKey);
67 | } else {
68 | console.error("api error");
69 | }
70 | }
71 |
72 | /**
73 | * Check if a message comes from an authorized source
74 | * @param {string} url
75 | * @returns Promise
76 | */
77 | const isMessageAllowed = async (url) => {
78 | const requestUrl = new URL(url);
79 | const apiUrlValue = await SLStorage.get(SLStorage.SETTINGS.API_URL);
80 | const apiUrl = new URL(apiUrlValue);
81 |
82 | let allowedSources = [
83 | new RegExp(apiUrl.hostname),
84 | new RegExp("^app\\.simplelogin\\.io$"),
85 | new RegExp("^.*\\.protonmail\\.ch$"),
86 | new RegExp("^.*\\.protonmail\\.com$"),
87 | ];
88 |
89 | const extraAllowedDomains =
90 | SLStorage.DEFAULT_SETTINGS[SLStorage.SETTINGS.EXTRA_ALLOWED_DOMAINS];
91 | for (const extra of extraAllowedDomains) {
92 | allowedSources.push(new RegExp(extra));
93 | }
94 |
95 | for (const source of allowedSources) {
96 | if (source.test(requestUrl.host)) return true;
97 | }
98 | return false;
99 | };
100 |
101 | /**
102 | * Handle the event of a page querying if the SL extension is installed
103 | * @return {{data: {version: string}, tag: string}}
104 | */
105 | const handleExtensionInstalledQuery = () => {
106 | const manifest = browser.runtime.getManifest();
107 | return {
108 | tag: "EXTENSION_INSTALLED_RESPONSE",
109 | data: {
110 | version: manifest.version,
111 | },
112 | };
113 | };
114 |
115 | /**
116 | * Register onMessage listener
117 | */
118 | browser.runtime.onMessage.addListener(async function (request, sender) {
119 | // Check messages allowed from everywhere
120 | if (request.tag === "NEW_RANDOM_ALIAS") {
121 | return await handleNewRandomAlias(request.currentUrl);
122 | } else if (request.tag === "GET_APP_SETTINGS") {
123 | return await handleGetAppSettings();
124 | }
125 |
126 | // Check messages allowed only from authorized sources
127 | const messageAllowed = await isMessageAllowed(sender.url);
128 | if (!messageAllowed) return;
129 |
130 | if (request.tag === "EXTENSION_SETUP") {
131 | // On Safari the background script won't set cookies properly in API calls (see https://bugs.webkit.org/show_bug.cgi?id=260676),
132 | // so we will return the API URL to the content script which will make the API call with cookies properly set
133 | return process.env.MAC
134 | ? await SLStorage.get(SLStorage.SETTINGS.API_URL)
135 | : await handleExtensionSetup();
136 | } else if (request.tag === "EXTENSION_INSTALLED_QUERY") {
137 | return handleExtensionInstalledQuery();
138 | } else if (request.tag === "SAFARI_FINALIZE_EXTENSION_SETUP") {
139 | return await finalizeExtensionSetup(request.data);
140 | }
141 | });
142 |
143 | /**
144 | * Register context menu
145 | */
146 | browser.contextMenus.create({
147 | id: "sl-random",
148 | title: "Create random email alias (copied)",
149 | contexts: ["all"],
150 | });
151 |
152 | browser.contextMenus.onClicked.addListener(handleOnClickContextMenu);
153 |
154 | /**
155 | * Shortcuts and hotkeys listener
156 | */
157 | browser.commands.onCommand.addListener(async (command) => {
158 | if (command === "generate-random-alias") {
159 | const currentTab = (
160 | await browser.tabs.query({ active: true, currentWindow: true })
161 | )[0];
162 | const res = await handleNewRandomAlias(currentTab.url);
163 | generateAliasHandlerJS(currentTab, res);
164 | }
165 | });
166 |
167 | APIService.initService();
168 | Onboarding.initService();
169 |
--------------------------------------------------------------------------------
/src/background/onboarding.js:
--------------------------------------------------------------------------------
1 | import browser from "webextension-polyfill";
2 | import SLStorage from "../popup/SLStorage";
3 |
4 | function initService() {
5 | browser.runtime.onInstalled.addListener(async function ({ reason }) {
6 | if (reason === "install") {
7 | await SLStorage.clear();
8 |
9 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
10 | await browser.tabs.create({
11 | url: `${apiUrl}/onboarding`,
12 | });
13 | }
14 | });
15 | }
16 |
17 | export default { initService };
18 |
--------------------------------------------------------------------------------
/src/content_script/input_tools.css:
--------------------------------------------------------------------------------
1 | .simplelogin-extension--button-wrapper {
2 | position: absolute;
3 | z-index: 999999;
4 | }
5 |
6 | .simplelogin-extension--button {
7 | cursor: pointer;
8 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
9 | max-height: 30px;
10 | max-width: 30px;
11 | position: absolute;
12 | top: 50%;
13 | left: 50%;
14 | transform: translate(-50%, -50%);
15 | border-radius: 100px;
16 | background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 23.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-30 -68.5 345 286.5' style='enable-background:new 0 0 265 156.86; background-color: white;' xml:space='preserve'%3E%3Cstyle type='text/css'%3E .st0%7Bfill:%23BE1E2D;%7D .st1%7Bfill:url(%23SVGID_1_);%7D%0A%3C/style%3E%3Cg%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='0.1062' y1='77.7856' x2='696.6286' y2='77.7856'%3E%3Cstop offset='0' style='stop-color:%23EE307C'/%3E%3Cstop offset='1' style='stop-color:%23AA2990'/%3E%3C/linearGradient%3E%3Cpath class='st1' d='M66.82-0.04L66.77,0h0.08L66.82-0.04z M257.49,0H66.77l-2.42,2.5l-0.42,0.46c-1.83,1.92-3.75,3.75-5.66,5.5 C43.66,21.9,26.09,32.06,7.02,37.06c-0.54,0.17-1.08,0.29-1.62,0.46L2.98,38.1l-0.54,2.42c-0.21,0.87-0.37,1.75-0.54,2.67 c-1.17,6.16-1.79,12.49-1.79,18.78c0,20.57,6.2,40.31,17.95,57.09c10.58,15.12,24.9,26.9,41.64,34.23 c0.42,0.21,0.87,0.42,1.33,0.58c1,0.46,2,0.83,3,1.25l1.21,0.46l0.17,0.04l0.08-0.04c0.08,0.04,0.21,0.04,0.29,0.04h191.72 c4.16,0,7.5-3.37,7.5-7.5V7.45C264.99,3.33,261.65,0,257.49,0z M244.54,11.95l-79.83,69.58c-1.67,1.42-4.08,1.42-5.7-0.04 l-26.03-23.07c-0.12-5.58-0.71-11.12-1.79-16.53c-0.08-0.46-0.17-0.92-0.25-1.42l-0.54-2.42l-2.42-0.62 c-0.58-0.12-1.17-0.29-1.75-0.42c-13.24-3.54-25.78-9.58-37.06-17.45l-8.62-7.62H244.54z M137.11,77.62l-6.54,5.79 c0.75-3.29,1.33-6.62,1.75-10.04L137.11,77.62z M65.52,147.12c-2.46-1-4.87-2.12-7.25-3.33C27.92,128.25,8.1,96.52,8.1,61.96 c0-5.83,0.54-11.66,1.67-17.32c17.78-4.79,34.27-13.24,48.51-24.9c2.96-2.37,5.79-4.91,8.54-7.58 c13.49,13.12,29.52,23.11,47.1,29.36c3.16,1.17,6.37,2.17,9.66,3.04c0.37,2.08,0.71,4.21,0.92,6.33c0.37,3.37,0.58,6.83,0.58,10.29 c0,1.87-0.04,3.75-0.21,5.62c-0.54,9.24-2.5,18.24-5.7,26.69C110,117.76,90.68,137.67,65.52,147.12z M88.8,144.04 c14.12-9.49,25.53-22.36,33.19-37.31l23.94-21.15l12.99,11.62c1.62,1.46,4.12,1.46,5.75,0l13.24-11.74l66.46,58.51L88.8,144.04z M186.95,77.58l66.5-59.09l0.13,117.84L186.95,77.58z M83.47,124.92c3.16-2.33,6.16-4.96,8.83-7.79L83.47,124.92z M113.12,56.42 c-0.21-1.67-0.46-3.33-0.75-5c-2.62-0.71-5.21-1.54-7.75-2.5c0,0-0.04,0-0.04-0.04c-0.67-0.21-1.29-0.5-1.96-0.75 c-1.46-0.58-2.91-1.21-4.37-1.92c-1-0.46-2.04-0.92-3.04-1.46c-0.71-0.33-1.46-0.71-2.17-1.12c-2.29-1.21-4.54-2.54-6.75-3.96 c-1.37-0.83-2.71-1.75-4-2.67c-0.13-0.04-0.21-0.13-0.29-0.21c-1.42-0.96-2.79-2-4.16-3.04c-2.71-2.08-5.33-4.29-7.87-6.62 c-1.08-0.96-2.17-1.96-3.21-3c-2.71,2.62-5.54,5.16-8.49,7.58C47.2,40.73,34.54,47.85,20.93,51.51c-0.87,4.58-1.33,9.24-1.33,13.95 c0,5.62,0.67,11.16,1.87,16.53c4.75,20.65,18.2,38.98,36.81,49.18c2.42,1.33,4.87,2.5,7.45,3.54c1.5-0.58,3-1.21,4.46-1.87 c0.04,0,0.08-0.04,0.08-0.04c1.21-0.54,2.42-1.12,3.58-1.75c1.08-0.54,2.12-1.12,3.16-1.79c0.33-0.17,0.67-0.37,1-0.58 c0.54-0.33,1.08-0.67,1.58-1c0.13-0.08,0.25-0.12,0.33-0.25c0.96-0.58,1.83-1.21,2.71-1.87c0.29-0.17,0.58-0.37,0.83-0.62 l8.83-7.79c0.08,0,0.08,0,0.08-0.04c8.74-9.08,15.2-20.36,18.53-32.65c1.75-6.33,2.67-12.95,2.67-19.65 C113.58,62,113.41,59.21,113.12,56.42z M70.1,99.23l-6.79,6.79h-0.04L35.17,77.91l8.62-8.7l19.53,19.53l6.79-6.79L93.8,58.21 l8.66,8.66L70.1,99.23z'/%3E%3C/g%3E%3C/svg%3E");
17 | transition: all 0.2s ease;
18 | pointer-events: all;
19 | background-repeat: no-repeat;
20 | background-position: center center;
21 | background-color: white;
22 | background-size: contain;
23 | }
24 |
25 | .simplelogin-extension--button:hover {
26 | transform: translate(-50%, -50%) scale(1.2);
27 | }
28 |
29 | .simplelogin-extension--button.loading {
30 | background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='margin: auto; background: none; display: block; shape-rendering: auto;' width='200px' height='200px' viewBox='0 0 100 100' preserveAspectRatio='xMidYMid'%3E%3Ccircle cx='50' cy='50' fill='none' stroke='%23aa2990' stroke-width='10' r='35' stroke-dasharray='164.93361431346415 56.97787143782138' transform='rotate(178.465 50 50)'%3E%3CanimateTransform attributeName='transform' type='rotate' repeatCount='indefinite' dur='1s' values='0 50 50;360 50 50' keyTimes='0;1'%3E%3C/animateTransform%3E%3C/circle%3E%3C/svg%3E");
31 | }
32 |
33 | .simplelogin-extension--button.loading:hover {
34 | transform: translate(-50%, -50%);
35 | }
36 |
--------------------------------------------------------------------------------
/src/content_script/input_tools.js:
--------------------------------------------------------------------------------
1 | if (!window._hasExecutedSlExtension) {
2 | window._hasExecutedSlExtension = true;
3 |
4 | /**
5 | * Send message to background.js and resolve with the response
6 | * @param {string} tag
7 | * @param {object} data
8 | */
9 | const sendMessageToBackground = (tag, data = null) => {
10 | const _browser = window.chrome || browser;
11 | return new Promise((resolve) => {
12 | try {
13 | _browser.runtime.sendMessage(
14 | {
15 | tag,
16 | data,
17 | },
18 | function (response) {
19 | resolve(response);
20 | }
21 | );
22 | } catch (e) {
23 | // Extension context invalidated.
24 | console.error(e);
25 | }
26 | });
27 | };
28 |
29 | const slButtonLogic = async () => {
30 | if (!window.hasSLButton) {
31 | window.hasSLButton = true;
32 |
33 | const InputTools = {
34 | isLoading: false,
35 |
36 | // store tracked input elements
37 | trackedElements: [],
38 |
39 | init(target) {
40 | InputTools.queryEmailInputAndApply(target, (element) => {
41 | if (!InputTools.isValidEmailInput(element)) {
42 | return;
43 | }
44 |
45 | // ignore if this elements has already been tracked
46 | const i = InputTools.trackedElements.indexOf(element);
47 | if (i === -1) {
48 | InputTools.trackedElements.push(element);
49 | InputTools.addSLButtonToInput(element);
50 | }
51 | });
52 | },
53 |
54 | destroy(target) {
55 | InputTools.queryEmailInputAndApply(target, (element) => {
56 | // remove element from tracking list
57 | const i = InputTools.trackedElements.indexOf(element);
58 | if (i !== -1) {
59 | InputTools.trackedElements.splice(i, 1);
60 | }
61 | });
62 | },
63 |
64 | queryEmailInputAndApply(target, actionFunction) {
65 | if (!target.querySelectorAll) return;
66 | const elements = target.querySelectorAll(
67 | "input[type='email'],input[name*='email'],input[id*='email']"
68 | );
69 | for (const element of elements) {
70 | actionFunction(element);
71 | }
72 | },
73 |
74 | isValidEmailInput(element) {
75 | const style = getComputedStyle(element);
76 | return (
77 | // check if element is visible
78 | style.visibility !== "hidden" &&
79 | style.display !== "none" &&
80 | style.opacity !== "0" &&
81 | style.pointerEvents === "auto" &&
82 | // check if element is not disabled
83 | !element.disabled &&
84 | // for example, we must filter out a checkbox with name*=email
85 | // check if element is text or email input
86 | (element.type === "text" || element.type === "email")
87 | );
88 | },
89 |
90 | addSLButtonToInput(inputElem) {
91 | // create wrapper for SL button
92 | const btnWrapper = InputTools.newDiv(
93 | "simplelogin-extension--button-wrapper"
94 | );
95 | const inputSumHeight =
96 | inputElem.getBoundingClientRect().height + "px";
97 | btnWrapper.style.height = inputSumHeight;
98 | btnWrapper.style.width = inputSumHeight;
99 | document.body.appendChild(btnWrapper);
100 |
101 | // create the SL button
102 | const slButton = InputTools.newDiv("simplelogin-extension--button");
103 | slButton.addEventListener("click", function () {
104 | InputTools.handleOnClickSLButton(inputElem, slButton);
105 | });
106 | slButton.style.height = inputSumHeight;
107 | slButton.style.width = inputSumHeight;
108 | btnWrapper.appendChild(slButton);
109 |
110 | InputTools.placeBtnToTheRightOfElement(inputElem, btnWrapper);
111 | },
112 |
113 | newDiv(...className) {
114 | const div = document.createElement("div");
115 | div.classList.add(...className);
116 | return div;
117 | },
118 |
119 | placeBtnToTheRightOfElement(inputElem, btnWrapper) {
120 | let intervalId = 0;
121 |
122 | function updatePosition() {
123 | // check is element is removed from trackedElements
124 | const i = InputTools.trackedElements.indexOf(inputElem);
125 | if (i === -1) {
126 | btnWrapper.parentNode.removeChild(btnWrapper);
127 | clearInterval(intervalId);
128 | }
129 |
130 | // get dimension & position of input
131 | const inputCoords = inputElem.getBoundingClientRect();
132 | const inputStyle = getComputedStyle(inputElem);
133 | const elemWidth = InputTools.dimensionToInt(btnWrapper.style.width);
134 | const pageXOffset = window.pageXOffset;
135 | const pageYOffset = window.pageYOffset;
136 | const buttonXOffset =
137 | SLSettings.SLButtonPosition === "right-inside"
138 | ? -elemWidth * 1.02
139 | : elemWidth * 0.02;
140 |
141 | // calculate elem position
142 | const left =
143 | InputTools.sumPixel([
144 | inputCoords.left,
145 | pageXOffset,
146 | inputElem.offsetWidth,
147 | buttonXOffset,
148 | -inputStyle.paddingRight,
149 | ]) + "px";
150 |
151 | const top =
152 | InputTools.sumPixel([inputCoords.top, pageYOffset]) + "px";
153 |
154 | if (btnWrapper.style.left !== left) {
155 | btnWrapper.style.left = left;
156 | }
157 |
158 | if (btnWrapper.style.top !== top) {
159 | btnWrapper.style.top = top;
160 | }
161 | }
162 |
163 | intervalId = setInterval(updatePosition, 200);
164 | updatePosition();
165 | },
166 |
167 | async handleOnClickSLButton(inputElem, slButton) {
168 | if (InputTools.isLoading) {
169 | return;
170 | }
171 | InputTools.isLoading = true;
172 | slButton.classList.add("loading");
173 |
174 | let res = await sendMessageToBackground("NEW_RANDOM_ALIAS", {
175 | currentUrl: window.location.href,
176 | });
177 | if (res.error) {
178 | alert("SimpleLogin Error: " + res.error);
179 | res = { alias: "" };
180 | }
181 |
182 | InputTools.isLoading = false;
183 | slButton.classList.remove("loading");
184 |
185 | inputElem.value = res.alias;
186 | },
187 |
188 | sumPixel(dimensions) {
189 | let sum = 0;
190 | for (const dim of dimensions) {
191 | sum += !isNaN(dim) ? dim : InputTools.dimensionToInt(dim);
192 | }
193 | return sum;
194 | },
195 |
196 | dimensionToInt(dim) {
197 | try {
198 | const pixel = parseFloat(dim.replace(/[^0-9.]+/g, ""));
199 | return isNaN(pixel) ? 0 : pixel;
200 | } catch (e) {
201 | return 0;
202 | }
203 | },
204 | };
205 |
206 | const MutationObserver =
207 | window.MutationObserver ||
208 | window.WebKitMutationObserver ||
209 | window.MozMutationObserver;
210 |
211 | /**
212 | * Add DOM mutations listener
213 | */
214 | const addMutationObserver = () => {
215 | const mutationObserver = new MutationObserver(function (mutations) {
216 | mutations.forEach(function (mutation) {
217 | for (const addedNode of mutation.addedNodes) {
218 | // add SLButton for newly added nodes
219 | InputTools.init(addedNode);
220 | }
221 |
222 | for (const removedNode of mutation.removedNodes) {
223 | // destroy SLButton for removed nodes
224 | InputTools.destroy(removedNode);
225 | }
226 | });
227 | });
228 |
229 | const target = document.body;
230 | if (!target) return;
231 |
232 | mutationObserver.observe(target, {
233 | childList: true,
234 | subtree: true,
235 | });
236 | };
237 |
238 | const SLSettings = await sendMessageToBackground("GET_APP_SETTINGS");
239 | if (SLSettings.showSLButton) {
240 | InputTools.init(document);
241 | addMutationObserver();
242 | }
243 | }
244 | };
245 |
246 | const slRegisterListener = () => {
247 | if (!window.hasSlListenerRegistered) {
248 | window.hasSlListenerRegistered = true;
249 |
250 | let hasProcessedSetup = false;
251 |
252 | /**
253 | * Callback called for every event
254 | * @param {MessageEvent} event
255 | */
256 | const onEvent = async (event) => {
257 | if (event.source !== window) return;
258 | if (!event.data) return;
259 | if (!event.data.tag) return;
260 | if (event.data.tag === "PERFORM_EXTENSION_SETUP") {
261 | const SLSettings = await sendMessageToBackground("GET_APP_SETTINGS");
262 | if (SLSettings.isLoggedIn) {
263 | console.log(
264 | "Received PERFORM_EXTENSION_SETUP but extension is already logged in. Redirecting to dashboard"
265 | );
266 | window.location.href = `${SLSettings.url}/dashboard/`;
267 | return;
268 | }
269 |
270 | if (!hasProcessedSetup) {
271 | hasProcessedSetup = true;
272 | const apiUrl = await sendMessageToBackground("EXTENSION_SETUP");
273 | // if apiUrl is undefined then the Chromium/Firefox extension has already finished setup
274 | if (!apiUrl) {
275 | return;
276 | }
277 | // else if apiUrl is defined, we are in Safari and need to setup the Safari extension
278 | const url = apiUrl + "/api/api_key";
279 | const res = await fetch(url, {
280 | method: "POST",
281 | headers: {
282 | "Content-Type": "application/json",
283 | "X-Sl-Allowcookies": true,
284 | },
285 | body: JSON.stringify({
286 | device: "Safari extension",
287 | }),
288 | });
289 |
290 | if (res.ok) {
291 | const apiRes = await res.json();
292 | const apiKey = apiRes.api_key;
293 | await sendMessageToBackground(
294 | "SAFARI_FINALIZE_EXTENSION_SETUP",
295 | apiKey
296 | );
297 | }
298 | }
299 | } else if (event.data.tag === "EXTENSION_INSTALLED_QUERY") {
300 | const res = await sendMessageToBackground(
301 | "EXTENSION_INSTALLED_QUERY"
302 | );
303 | window.postMessage(res);
304 | }
305 | };
306 |
307 | window.addEventListener("message", onEvent);
308 | }
309 | };
310 |
311 | slButtonLogic();
312 | slRegisterListener();
313 | }
314 |
--------------------------------------------------------------------------------
/src/icons/icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_128.png
--------------------------------------------------------------------------------
/src/icons/icon_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_16.png
--------------------------------------------------------------------------------
/src/icons/icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_32.png
--------------------------------------------------------------------------------
/src/icons/icon_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_48.png
--------------------------------------------------------------------------------
/src/icons/icon_96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_96.png
--------------------------------------------------------------------------------
/src/icons/icon_beta_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_beta_128.png
--------------------------------------------------------------------------------
/src/icons/icon_beta_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/icons/icon_beta_48.png
--------------------------------------------------------------------------------
/src/images/arrow-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/arrow-up.png
--------------------------------------------------------------------------------
/src/images/back-button.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/chrome-permission-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/chrome-permission-screenshot.png
--------------------------------------------------------------------------------
/src/images/firefox-permission-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/firefox-permission-screenshot.png
--------------------------------------------------------------------------------
/src/images/horizontal-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
9 |
10 |
11 |
16 |
22 |
25 |
28 |
29 |
30 |
31 |
32 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/images/icon-copy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/images/icon-dropdown.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/images/icon-more.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/images/icon-puzzle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/icon-puzzle.png
--------------------------------------------------------------------------------
/src/images/icon-settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/images/icon-simplelogin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/icon-simplelogin.png
--------------------------------------------------------------------------------
/src/images/icon-trash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/images/loading-three-dots.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
12 |
13 |
14 |
18 |
22 |
23 |
24 |
28 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/images/loading.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/images/proton.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/images/sl-button-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/simple-login/browser-extension/d18b40fc774a3b477468bc608c3589a24f04c3ed/src/images/sl-button-demo.png
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SimpleLogin by Proton: Secure Email Aliases",
3 | "short_name": "SimpleLogin",
4 | "description": "Easily create a different email for each website to hide your real email. Protect your inbox against spams, phishing, data breaches",
5 | "version": null,
6 | "manifest_version": 3,
7 | "icons": {
8 | "16": "icons/icon_16.png",
9 | "32": "icons/icon_32.png",
10 | "48": "icons/icon_48.png",
11 | "96": "icons/icon_96.png",
12 | "128": "icons/icon_128.png"
13 | },
14 | "permissions": [
15 | "activeTab",
16 | "storage",
17 | "contextMenus",
18 | "scripting",
19 | "tabs"
20 | ],
21 | "host_permissions": [
22 | "https://*.simplelogin.io/*",
23 | "http://*/*",
24 | "https://*/*"
25 | ],
26 | "action": {
27 | "default_title": "SimpleLogin",
28 | "default_popup": "popup/popup.html",
29 | "default_icon": {
30 | "16": "icons/icon_16.png",
31 | "32": "icons/icon_32.png",
32 | "48": "icons/icon_48.png",
33 | "96": "icons/icon_96.png",
34 | "128": "icons/icon_128.png"
35 | }
36 | },
37 |
38 | "browser_specific_settings": {
39 | "gecko": {
40 | "id": "addon@simplelogin",
41 | "strict_min_version": "109.0"
42 | }
43 | },
44 | "commands": {
45 | "generate-random-alias": {
46 | "suggested_key": {
47 | "default": "Ctrl+Shift+X"
48 | },
49 | "description": "Generate a random email alias"
50 | },
51 | "_execute_browser_action": {
52 | "suggested_key": {
53 | "default": "Ctrl+Shift+S"
54 | },
55 | "description": "Open the extension action menu"
56 | }
57 | },
58 | "content_scripts": [
59 | {
60 | "js": ["content_script/input_tools.js"],
61 | "css": ["content_script/input_tools.css"],
62 | "matches": ["http://*/*", "https://*/*"],
63 | "exclude_matches" : ["https://app.simplelogin.io/dashboard/*"],
64 | "run_at": "document_idle"
65 | }
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------
/src/popup/APIService.js:
--------------------------------------------------------------------------------
1 | import EventManager from "./EventManager";
2 | import Navigation from "./Navigation";
3 | import SLStorage from "./SLStorage";
4 | import Utils from "./Utils";
5 |
6 | const API_ROUTE = {
7 | GET_USER_INFO: { method: "GET", path: "/api/user_info" },
8 | LOGOUT: { method: "GET", path: "/api/logout" },
9 | LOGIN: { method: "POST", path: "/api/auth/login" },
10 | MFA: { method: "POST", path: "/api/auth/mfa" },
11 | GET_ALIAS_OPTIONS: {
12 | method: "GET",
13 | path: "/api/v4/alias/options?hostname=:hostname",
14 | },
15 | GET_MAILBOXES: {
16 | method: "GET",
17 | path: "/api/mailboxes",
18 | },
19 | GET_ALIASES: { method: "POST", path: "/api/v2/aliases?page_id=:page_id" },
20 | NEW_ALIAS: {
21 | method: "POST",
22 | path: "/api/v2/alias/custom/new?hostname=:hostname",
23 | },
24 | NEW_RANDOM_ALIAS: {
25 | method: "POST",
26 | path: "/api/alias/random/new?hostname=:hostname",
27 | },
28 | TOGGLE_ALIAS: { method: "POST", path: "/api/aliases/:alias_id/toggle" },
29 | EDIT_ALIAS: { method: "PUT", path: "/api/aliases/:alias_id" },
30 | DELETE_ALIAS: { method: "DELETE", path: "/api/aliases/:alias_id" },
31 | CREATE_REVERSE_ALIAS: {
32 | method: "POST",
33 | path: "/api/aliases/:alias_id/contacts",
34 | },
35 | GET_API_KEY_FROM_COOKIE: { method: "POST", path: "/api/api_key" },
36 | };
37 |
38 | const API_ON_ERR = {
39 | IGNORE: 1,
40 | TOAST: 2,
41 | THROW: 3,
42 | };
43 |
44 | const SETTINGS = {
45 | apiKey: "",
46 | apiUrl: "",
47 | };
48 |
49 | const initService = async () => {
50 | await reloadSettings();
51 |
52 | EventManager.addListener(EventManager.EVENT.SETTINGS_CHANGED, reloadSettings);
53 | };
54 |
55 | const reloadSettings = async () => {
56 | SETTINGS.apiKey = await SLStorage.get(SLStorage.SETTINGS.API_KEY);
57 | SETTINGS.apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
58 | };
59 |
60 | const callAPI = async function (
61 | route,
62 | params = {},
63 | data = {},
64 | errHandlerMethod = API_ON_ERR.THROW
65 | ) {
66 | const { method, path } = route;
67 | const url = SETTINGS.apiUrl + bindQueryParams(path, params);
68 | const headers = {};
69 |
70 | if (SETTINGS.apiKey) {
71 | headers["Authentication"] = SETTINGS.apiKey;
72 | }
73 |
74 | let fetchParam = {
75 | method: method,
76 | headers: headers,
77 | };
78 | if (method === "POST" || method === "PUT") {
79 | fetchParam.body = JSON.stringify(data);
80 | headers["Content-Type"] = "application/json";
81 | }
82 |
83 | let res = await fetch(url, fetchParam);
84 | if (res.ok) {
85 | const apiRes = await res.json();
86 | // wrap apiRes in data to look like axios which was used before
87 | return {
88 | status: res.status,
89 | data: apiRes,
90 | };
91 | } else {
92 | if (errHandlerMethod !== API_ON_ERR.IGNORE) {
93 | console.error(res);
94 | }
95 |
96 | if (res.status === 401) {
97 | await handle401Error();
98 | return null;
99 | }
100 |
101 | if (errHandlerMethod === API_ON_ERR.TOAST) {
102 | let apiRes = await res.json();
103 | if (apiRes.error) {
104 | Utils.showError(apiRes.error);
105 | } else {
106 | Utils.showError("Unknown error");
107 | }
108 | return null;
109 | }
110 |
111 | if (errHandlerMethod === API_ON_ERR.THROW) {
112 | throw {
113 | response: {
114 | status: res.status,
115 | data: await res.json(),
116 | },
117 | };
118 | }
119 | }
120 | };
121 |
122 | async function handle401Error() {
123 | Utils.showError("Authentication error, please login again");
124 | await SLStorage.remove(SLStorage.SETTINGS.API_KEY);
125 | EventManager.broadcast(EventManager.EVENT.SETTINGS_CHANGED);
126 | Navigation.clearHistoryAndNavigateTo(Navigation.PATH.LOGIN);
127 | }
128 |
129 | function bindQueryParams(url, params) {
130 | for (const key of Object.keys(params)) {
131 | url = url.replace(`:${key}`, encodeURIComponent(params[key]));
132 | }
133 |
134 | return url;
135 | }
136 |
137 | export { callAPI, API_ROUTE, API_ON_ERR, reloadSettings };
138 | export default { initService };
139 |
--------------------------------------------------------------------------------
/src/popup/App-color.scss:
--------------------------------------------------------------------------------
1 | $primary: #b02a8f;
2 | $success: #b02a8f;
--------------------------------------------------------------------------------
/src/popup/App-scrollbar.scss:
--------------------------------------------------------------------------------
1 | .app .content {
2 | scrollbar-width: thin;
3 | }
4 |
5 | .content::-webkit-scrollbar-track {
6 | background: rgba(0,0,0,0);
7 | }
8 |
9 | .content::-webkit-scrollbar {
10 | width: 0.6em;
11 | }
12 |
13 | .content::-webkit-scrollbar-thumb {
14 | background: transparent;
15 | border: solid 2px transparent;
16 | box-shadow: inset 0 0 10px 10px rgba(0, 0, 0, 0.25);
17 | border-radius: 16px;
18 | }
--------------------------------------------------------------------------------
/src/popup/App.scss:
--------------------------------------------------------------------------------
1 | @import "./App-color.scss";
2 | @import "./App-scrollbar.scss";
3 | @import "./Theme.scss";
4 | @import "~bootstrap/scss/bootstrap.scss";
5 | @import "~bootstrap-vue/src/index.scss";
6 | @import "~tippy.js/dist/tippy.css";
7 |
8 | body {
9 | box-sizing: content-box !important;
10 | width: 470px;
11 | background-color: var(--bg-color);
12 | color: var(--text-color);
13 | }
14 |
15 | input.form-control,
16 | select.form-control,
17 | textarea.form-control,
18 | .dropdown-menu.show {
19 | color: var(--text-color);
20 | background-color: var(--input-bg-color);
21 | border-color: var(--input-border-color);
22 | }
23 |
24 | input.form-control:disabled,
25 | select.form-control:disabled,
26 | textarea.form-control:disabled {
27 | color: var(--text-color);
28 | background-color: var(--bg-color);
29 | border-color: var(--input-border-color);
30 | }
31 |
32 | input.form-control:focus,
33 | select.form-control:focus,
34 | textarea.form-control:focus {
35 | color: var(--text-color);
36 | background-color: var(--input-bg-focus);
37 | border-color: var(--input-border-color);
38 | }
39 |
40 | .v--modal-box.v--modal.vue-dialog div,
41 | .v--modal-box.v--modal.vue-dialog button {
42 | color: var(--text-color);
43 | background-color: var(--input-bg-color);
44 | border-color: var(--delimiter-color);
45 | }
46 |
47 | .header {
48 | height: 45px;
49 | width: 460px;
50 | position: absolute;
51 | top: 0;
52 | left: 0;
53 | z-index: 10;
54 | }
55 |
56 | .header .back {
57 | cursor: pointer;
58 | }
59 |
60 | .overlay {
61 | position: fixed;
62 | top: 0;
63 | left: 0;
64 | z-index: 20;
65 | height: 100vh;
66 | width: 100vw;
67 | min-height: 350px;
68 | background-color: var(--overlay-background-color);
69 | }
70 |
71 | .overlay-content {
72 | position: fixed;
73 | z-index: 21;
74 | top: 50%;
75 | left: 50%;
76 | transform: translate(-50%, -50%);
77 | text-align: center;
78 | }
79 |
80 | .app {
81 | width: 470px;
82 | box-sizing: content-box !important;
83 | overflow-y: hidden;
84 | }
85 |
86 | .app .content {
87 | box-sizing: border-box;
88 | margin-top: 45px;
89 | padding-top: 15px;
90 | padding-bottom: 40px;
91 | max-height: 500px;
92 | overflow-y: auto;
93 | }
94 |
95 | .splash .logo {
96 | width: 200px;
97 | }
98 |
99 | .splash .loading {
100 | width: 30px;
101 | padding-top: 20px;
102 | }
103 |
104 | .splash.overlay {
105 | background-color: var(--background-color);
106 | }
107 |
108 | em {
109 | font-style: normal;
110 | background-color: #ffff00;
111 | }
112 |
113 | .small-text {
114 | font-size: 12px;
115 | font-weight: lighter;
116 | }
117 |
118 | .tooltip-inner {
119 | max-width: 400px;
120 | }
121 |
122 | .card-rating {
123 | border-radius: 8px;
124 | border: 1px solid var(--brand-color);
125 | }
126 |
127 | /* list aliases */
128 | .vue-js-switch {
129 | margin-bottom: 0;
130 | }
131 |
132 | .list-item-alias .disabled {
133 | opacity: 0.7;
134 | }
135 |
136 | .list-item-email {
137 | margin-right: 30px !important;
138 | position: relative;
139 | overflow: hidden;
140 | cursor: pointer;
141 | }
142 |
143 | .list-item-email > a {
144 | white-space: nowrap;
145 | }
146 |
147 | .list-item-email .email-sub {
148 | font-size: 12px;
149 | }
150 |
151 | .list-item-email-fade {
152 | right: 0;
153 | width: 30px;
154 | height: 100%;
155 | background: linear-gradient(to right, transparent, var(--bg-color));
156 | top: 0;
157 | position: absolute;
158 | }
159 |
160 | .list-item-alias .alias-note-preview {
161 | font-size: 12px;
162 | max-height: calc(12px * 1.5 * 3); /* font-size * line-height */
163 | overflow-y: hidden;
164 | }
165 |
166 | .header {
167 | .actions-container {
168 | position: absolute;
169 | right: 0.5rem;
170 | }
171 |
172 | .header-button {
173 | height: 20px;
174 | margin-top: 2px;
175 | margin-left: 10px;
176 | margin-right: 7px;
177 | color: var(--brand-color);
178 | cursor: pointer;
179 | }
180 | }
181 |
182 | .app-header-menu {
183 | left: auto !important;
184 | float: right !important;
185 | right: 10px;
186 | margin-top: 5px !important;
187 | }
188 |
189 | /* toasted: white close button */
190 | .toasted.toasted-primary > .action.ripple {
191 | color: white !important;
192 | }
193 |
194 | /* button img */
195 | .btn-svg {
196 | cursor: pointer;
197 | padding: 5px;
198 | display: inline;
199 | }
200 |
201 | /* send btn*/
202 | .btn-send {
203 | width: 14px;
204 | height: 14px;
205 | color: var(--brand-color);
206 | }
207 |
208 | /* more options */
209 | .btn-delete:hover,
210 | .btn-svg:hover {
211 | background-color: var(--delete-button-hover-color);
212 | }
213 |
214 | .btn-delete {
215 | float: right;
216 | }
217 |
218 | .btn-delete > img {
219 | vertical-align: unset;
220 | height: 14px;
221 | }
222 |
223 | .more-options {
224 | margin-top: 10px;
225 | }
226 |
227 | .cursor {
228 | cursor: pointer;
229 | }
230 |
231 | span.link {
232 | color: var(--brand-color);
233 | }
234 |
235 | .more-options > .action {
236 | margin-top: 10px;
237 | min-height: 25px;
238 | }
239 |
240 | .more-options > label {
241 | margin-bottom: 0;
242 | font-size: 12px;
243 | }
244 |
245 | /* BETA badge */
246 | .beta-badge {
247 | padding: 0.1em 0.4em;
248 | font-size: 0.65em;
249 | margin-left: 1em;
250 | border: 0.7px solid var(--brand-color);
251 | display: inline-block;
252 | color: var(--brand-color);
253 | }
254 |
255 | /* App Settings */
256 | table.settings-list > tr {
257 | border-bottom: 1px solid var(--delimiter-color);
258 | }
259 |
260 | table.settings-list > tr > td {
261 | vertical-align: top;
262 | padding: 6px;
263 | }
264 |
265 | table.settings-list {
266 | margin-bottom: 3em;
267 |
268 | tr.disabled {
269 | opacity: 0.7;
270 | pointer-events: none;
271 | }
272 | }
273 |
274 | /* Quick fix for Firefox Overflow Menu */
275 | .app.ff-overflow-menu {
276 | width: auto;
277 | font-size: 88%;
278 |
279 | .content {
280 | max-height: 500px;
281 | overflow-y: auto;
282 | scrollbar-width: thin;
283 | }
284 |
285 | textarea,
286 | input {
287 | font-size: 88%;
288 | }
289 |
290 | .header {
291 | width: 100%;
292 |
293 | .dashboard-btn {
294 | height: 20px;
295 | margin-top: 2px;
296 | padding: 0 0.5rem !important;
297 | }
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/src/popup/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
76 |
--------------------------------------------------------------------------------
/src/popup/EventManager.js:
--------------------------------------------------------------------------------
1 | const listeners = {};
2 |
3 | class EventManager {
4 | static EVENT = {
5 | SETTINGS_CHANGED: "settings_changed",
6 | };
7 |
8 | static addListener(eventName, callback) {
9 | if (!listeners[eventName]) {
10 | listeners[eventName] = [];
11 | }
12 | if (listeners[eventName].indexOf(callback) === -1) {
13 | // make sure the callback function is added only once
14 | listeners[eventName].push(callback);
15 | }
16 | }
17 |
18 | static removeListener(callback) {
19 | for (const eventCallbacks of Object.values(listeners)) {
20 | const index = eventCallbacks.indexOf(callback);
21 | if (index !== -1) {
22 | eventCallbacks.splice(index, 1);
23 | }
24 | }
25 | }
26 |
27 | static broadcast(eventName, data) {
28 | if (listeners[eventName]) {
29 | for (const callback of listeners[eventName]) {
30 | callback(data);
31 | }
32 | }
33 | }
34 | }
35 |
36 | export default EventManager;
37 |
--------------------------------------------------------------------------------
/src/popup/Navigation.js:
--------------------------------------------------------------------------------
1 | let router = null;
2 |
3 | const PATH = {
4 | ROOT: "/",
5 | MAIN: "/main",
6 | NEW_ALIAS_RESULT: "/new-alias-result/",
7 | LOGIN: "/login",
8 | API_KEY_SETTING: "/api-key-setting",
9 | SELF_HOST_SETTING: "/self-host-setting",
10 | REVERSE_ALIAS: "/reverse-alias",
11 | APP_SETTINGS: "/app-settings",
12 | };
13 |
14 | class Navigation {
15 | static PATH = PATH;
16 |
17 | static getRoutes(components) {
18 | return [
19 | {
20 | path: Navigation.PATH.ROOT,
21 | component: components.SplashScreen,
22 | },
23 | {
24 | path: Navigation.PATH.LOGIN,
25 | component: components.Login,
26 | },
27 | {
28 | path: Navigation.PATH.API_KEY_SETTING,
29 | component: components.ApiKeySetting,
30 | },
31 | {
32 | path: Navigation.PATH.SELF_HOST_SETTING,
33 | component: components.SelfHostSetting,
34 | },
35 | {
36 | path: Navigation.PATH.MAIN,
37 | component: components.Main,
38 | },
39 | {
40 | path: Navigation.PATH.NEW_ALIAS_RESULT,
41 | component: components.NewAliasResult,
42 | },
43 | {
44 | path: Navigation.PATH.REVERSE_ALIAS,
45 | component: components.ReverseAlias,
46 | },
47 | {
48 | path: Navigation.PATH.APP_SETTINGS,
49 | component: components.AppSettings,
50 | },
51 | ];
52 | }
53 |
54 | static setRouter($router) {
55 | router = $router;
56 | }
57 |
58 | static navigateTo(path, canGoBack) {
59 | if (canGoBack) {
60 | router.push(path);
61 | } else {
62 | router.replace(path);
63 | }
64 | }
65 |
66 | static canGoBack() {
67 | return router.history.index > 0;
68 | }
69 |
70 | static navigateBack() {
71 | router.go(-1);
72 | }
73 |
74 | static clearHistoryAndNavigateTo(path) {
75 | router.history.stack = [];
76 | router.history.index = -1;
77 | setTimeout(() => router.push(path), 10);
78 | }
79 | }
80 |
81 | export default Navigation;
82 |
--------------------------------------------------------------------------------
/src/popup/SLStorage.js:
--------------------------------------------------------------------------------
1 | import Utils from "./Utils";
2 | import browser from "webextension-polyfill";
3 | import { THEME_SYSTEM } from "./Theme";
4 |
5 | const TEMP = {};
6 |
7 | class SLStorage {
8 | static SETTINGS = {
9 | API_URL: "apiUrl",
10 | API_KEY: "apiKey",
11 | NOT_ASKING_RATE: "notAskingRate",
12 | SHOW_SL_BUTTON: "showSLButton",
13 | SL_BUTTON_POSITION: "SLButtonPosition",
14 | THEME: "SLTheme",
15 | EXTRA_ALLOWED_DOMAINS: [],
16 | };
17 |
18 | static DEFAULT_SETTINGS = {
19 | [SLStorage.SETTINGS.API_URL]: devConfig
20 | ? devConfig.DEFAULT_API_URL
21 | : "https://app.simplelogin.io",
22 | [SLStorage.SETTINGS.API_KEY]: "",
23 | [SLStorage.SETTINGS.NOT_ASKING_RATE]: false,
24 | [SLStorage.SETTINGS.SHOW_SL_BUTTON]: true,
25 | [SLStorage.SETTINGS.SL_BUTTON_POSITION]: "right-inside",
26 | [SLStorage.SETTINGS.THEME]: THEME_SYSTEM,
27 | [SLStorage.SETTINGS.EXTRA_ALLOWED_DOMAINS]: devConfig
28 | ? devConfig.EXTRA_ALLOWED_DOMAINS
29 | : [],
30 | };
31 |
32 | static set(key, value) {
33 | return browser.storage.sync.set({ [key]: value });
34 | }
35 |
36 | static async get(key) {
37 | const data = await browser.storage.sync.get(key);
38 |
39 | if (data[key] === undefined || data[key] === null) {
40 | return SLStorage.DEFAULT_SETTINGS[key] || "";
41 | } else {
42 | return data[key];
43 | }
44 | }
45 |
46 | static remove(key) {
47 | return browser.storage.sync.remove(key);
48 | }
49 |
50 | static clear() {
51 | return browser.storage.sync.clear();
52 | }
53 |
54 | static setTemporary(key, value) {
55 | TEMP[key] = Utils.cloneObject(value);
56 | }
57 |
58 | static getTemporary(key) {
59 | return TEMP[key];
60 | }
61 | }
62 |
63 | export default SLStorage;
64 |
--------------------------------------------------------------------------------
/src/popup/Theme.js:
--------------------------------------------------------------------------------
1 | import SLStorage from "./SLStorage";
2 |
3 | export const THEME_LIGHT = "theme-light";
4 | export const THEME_DARK = "theme-dark";
5 | export const THEME_SYSTEM = "theme-system";
6 |
7 | export const THEMES = [THEME_LIGHT, THEME_DARK, THEME_SYSTEM];
8 |
9 | export const THEME_LABELS = {
10 | [THEME_LIGHT]: "Light",
11 | [THEME_DARK]: "Dark",
12 | [THEME_SYSTEM]: "System",
13 | };
14 |
15 | export async function getSavedTheme() {
16 | return (await SLStorage.get(SLStorage.SETTINGS.THEME)) ?? THEME_SYSTEM;
17 | }
18 |
19 | export async function setThemeClass(nextTheme, prevTheme) {
20 | await SLStorage.set(SLStorage.SETTINGS.THEME, nextTheme);
21 |
22 | if (prevTheme === undefined) {
23 | return document.body.classList.add(nextTheme);
24 | }
25 |
26 | document.body.classList.replace(prevTheme, nextTheme);
27 | }
28 |
--------------------------------------------------------------------------------
/src/popup/Theme.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --brand-color: #b02a8f;
3 | --muted-brand-color: rgba(176, 42, 143, 0.7);
4 | }
5 |
6 | .theme-light {
7 | --bg-color: white;
8 | --input-bg-color: white;
9 | --overlay-bg-color: rgba(255, 255, 255, 0.8);
10 | --text-color: black;
11 | --delimiter-color: #bbb;
12 | --delete-button-hover-color: rgba(0, 0, 0, 0.1);
13 | }
14 |
15 | .theme-dark {
16 | --bg-color: #222;
17 | --input-bg-color: #333;
18 | --input-bg-focus: #444;
19 | --input-border-color: black;
20 | --overlay-bg-color: rgba(0, 0, 0, 0.8);
21 | --text-color: #ddd;
22 | --delimiter-color: #555;
23 | --delete-button-hover-color: rgba(255, 255, 255, 0.1);
24 |
25 | // For switching colors of monochrome images
26 | .invertable, .btn.dropdown-toggle {
27 | filter: invert(0.8);
28 | }
29 |
30 | .btn-primary.btn-primary-muted {
31 | background-color: var(--muted-brand-color);
32 | }
33 | }
34 |
35 | @media (prefers-color-scheme: light) {
36 | // copy of .theme-light
37 | .theme-system {
38 | --bg-color: white;
39 | --input-bg-color: white;
40 | --overlay-bg-color: rgba(255, 255, 255, 0.8);
41 | --text-color: black;
42 | --delimiter-color: #bbb;
43 | --delete-button-hover-color: rgba(0, 0, 0, 0.1);
44 | }
45 | }
46 |
47 | @media (prefers-color-scheme: dark) {
48 | // copy of .theme-dark
49 | .theme-system {
50 | --bg-color: #222;
51 | --input-bg-color: #333;
52 | --input-bg-focus: #444;
53 | --input-border-color: black;
54 | --overlay-bg-color: rgba(0, 0, 0, 0.8);
55 | --text-color: #ddd;
56 | --delimiter-color: #555;
57 | --delete-button-hover-color: rgba(255, 255, 255, 0.1);
58 |
59 | // For switching colors of monochrome images
60 | .invertable, .btn.dropdown-toggle {
61 | filter: invert(0.8);
62 | }
63 |
64 | .btn-primary.btn-primary-muted {
65 | background-color: var(--muted-brand-color);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/popup/Utils.js:
--------------------------------------------------------------------------------
1 | const browser = require("webextension-polyfill");
2 | const buildConfig = require("./buildConfig.json");
3 |
4 | let toasted = null;
5 |
6 | class Utils {
7 | static getRandomIntBetween(min, max) {
8 | return Math.floor(min + Math.random() * Math.floor(max));
9 | }
10 |
11 | static async getHostName(currentUrl) {
12 | try {
13 | if (currentUrl) {
14 | const url = new URL(currentUrl);
15 | return url.hostname;
16 | } else {
17 | const result = await browser.tabs.query({
18 | active: true,
19 | currentWindow: true,
20 | });
21 | const url = new URL(result[0].url);
22 | return url.hostname;
23 | }
24 | } catch (error) {
25 | console.log(error);
26 | }
27 | }
28 |
29 | static async getDefaultNote() {
30 | const hostName = await Utils.getHostName();
31 | let note = "";
32 |
33 | // ignore hostName that doesn't look like an url
34 | if (hostName && hostName.indexOf(".") > -1) {
35 | note = `Used on ${hostName}`;
36 | }
37 |
38 | return note;
39 | }
40 |
41 | static getDeviceName() {
42 | const isFirefox = typeof InstallTrigger !== "undefined";
43 | const browserName = isFirefox ? "Firefox" : "Chrome";
44 | return `${browserName} (${navigator.platform})`;
45 | }
46 |
47 | static getExtensionURL() {
48 | const isFirefox = typeof InstallTrigger !== "undefined",
49 | firefoxExtensionUrl =
50 | "https://addons.mozilla.org/en-GB/firefox/addon/simplelogin/",
51 | chromeExtensionUrl =
52 | "https://chrome.google.com/webstore/detail/simplelogin-your-anti-spa/dphilobhebphkdjbpfohgikllaljmgbn";
53 | return isFirefox ? firefoxExtensionUrl : chromeExtensionUrl;
54 | }
55 |
56 | static setToasted($toasted) {
57 | toasted = $toasted;
58 | }
59 |
60 | static showSuccess(message) {
61 | if (toasted) {
62 | toasted.show(message, {
63 | type: "success",
64 | duration: 2500,
65 | });
66 | }
67 | }
68 |
69 | static showError(message) {
70 | if (toasted) {
71 | toasted.show(message, {
72 | type: "error",
73 | duration: 3000,
74 | action: {
75 | text: "×",
76 | onClick: (e, toastObject) => {
77 | toastObject.goAway(0);
78 | },
79 | },
80 | });
81 | }
82 | }
83 |
84 | static cloneObject(obj) {
85 | return JSON.parse(JSON.stringify(obj));
86 | }
87 |
88 | static getBuildConfig() {
89 | return buildConfig;
90 | }
91 | }
92 |
93 | export default Utils;
94 |
--------------------------------------------------------------------------------
/src/popup/buildConfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "features": {
3 | "loginWithProtonEnabled": true
4 | },
5 | "buildTime": 1655462531232
6 | }
--------------------------------------------------------------------------------
/src/popup/components/AliasMoreOptions.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
Mailboxes
12 |
13 |
14 |
15 |
23 | {{ mailbox.email }}
24 |
25 |
26 |
27 |
28 | {{
29 | moreOptions.mailboxes.length > 0
30 | ? moreOptions.mailboxes.map((mb) => mb.email).join(", ")
31 | : "Please select at least one mailbox"
32 | }}
33 |
34 |
35 |
Alias Note
36 |
43 |
44 |
45 | From Name
46 |
52 |
53 |
58 |
59 |
60 | Enable PGP
65 |
66 |
67 |
68 |
73 |
74 | {{ btnSaveLabel || "Save" }}
75 |
76 |
77 |
83 |
84 | Delete
85 |
86 |
87 |
88 |
89 |
90 |
91 |
279 |
--------------------------------------------------------------------------------
/src/popup/components/ApiKeySetting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
To get started, please follow these 3 simple steps:
5 |
6 |
7 |
1
8 | Create your SimpleLogin account
9 |
here
10 | if this is not already done.
11 |
12 |
13 |
14 |
2
15 | Create and copy your
16 |
API Key
17 |
here .
18 |
19 |
20 |
21 | 3
22 | Paste the
23 | API Key here 👇🏽
24 |
25 |
26 |
33 |
34 |
35 | Set API Key
36 |
37 |
38 |
39 |
40 |
41 |
85 |
--------------------------------------------------------------------------------
/src/popup/components/AppSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | App Settings ({{ userEmail }})
6 |
7 |
8 |
9 |
10 | Currently you have a free SimpleLogin account. Upgrade your account to
11 | create unlimited aliases, add more mailboxes, create aliases
12 | on-the-fly with your domain or SimpleLogin subdomain and more.
13 |
14 |
15 | Upgrade your SimpleLogin account
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
31 |
32 |
33 | Show SimpleLogin button on email input fields
34 |
35 | If enabled, you can quickly create a random alias by clicking on
36 | the SimpleLogin button placed next to the email field.
37 |
42 |
43 |
44 | Report an issue
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
60 |
61 |
62 | Place SimpleLogin button outside the input
63 |
64 | Display the SimpleLogin button next to the email field instead of
65 | inside the field. This can avoid having overlapping buttons with
66 | other extensions like Dashlane, LastPass, etc
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | SimpleLogin extension Theme
75 |
76 | System theme automatically switches between Light and Dark -
77 | according to system preference.
78 |
79 |
83 |
84 |
89 | {{ THEME_LABELS[themeOption] }}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
101 | Logout
102 |
103 |
104 |
108 | Version: {{ extension_version }}
109 |
110 |
111 |
112 |
113 |
114 |
241 |
--------------------------------------------------------------------------------
/src/popup/components/ExpandTransition.vue:
--------------------------------------------------------------------------------
1 |
70 |
71 |
79 |
80 |
92 |
--------------------------------------------------------------------------------
/src/popup/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
71 |
72 |
73 |
146 |
--------------------------------------------------------------------------------
/src/popup/components/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Welcome to
7 | SimpleLogin
9 | , the most powerful email alias solution!
13 |
14 |
15 |
35 |
36 |
37 |
49 |
50 |
51 |
52 | Sign in with API Key
53 |
54 |
55 |
56 |
57 | Don't have an account yet?
58 |
62 | Sign Up
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | Your account is protected with Two Factor Authentication.
73 |
74 |
75 |
76 |
Token
77 |
Please enter the 2FA code from your 2FA authenticator
78 |
79 |
80 |
81 |
88 |
89 | Submit
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
190 |
191 |
206 |
--------------------------------------------------------------------------------
/src/popup/components/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | You created this alias on this website before:
10 |
11 |
23 |
24 |
25 |
26 |
27 |
82 |
83 |
84 | You're about to create alias
85 | {{ aliasPrefix }}{{ signedSuffix[0] }}
86 |
87 |
88 |
89 |
90 |
96 |
97 | OR create a totally random alias
98 |
99 |
100 |
101 |
102 |
103 | You have reached limit number of email aliases in free plan, please
104 | upgrade
109 | or reuse one of the existing aliases.
110 |
111 |
112 |
113 |
114 |
115 |
116 | OR use an existing alias
117 |
118 |
119 |
120 |
126 |
127 |
128 | Type enter to search.
129 |
135 | Reset
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
156 |
157 |
164 |
165 |
169 |
170 |
171 |
172 |
181 |
182 |
183 |
184 |
188 | {{ alias.note }}
189 |
190 |
191 |
192 | {{ alias.nb_forward }} forwards, {{ alias.nb_reply }} replies,
193 | {{ alias.nb_block }} blocks.
194 |
195 |
196 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
625 |
--------------------------------------------------------------------------------
/src/popup/components/NewAliasResult.vue:
--------------------------------------------------------------------------------
1 |
2 |
50 |
51 |
52 |
108 |
--------------------------------------------------------------------------------
/src/popup/components/ReverseAlias.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Send emails from
8 | {{ alias.email }}
9 |
10 |
11 | To send an email from your alias to a contact, you need to create a
12 | reverse-alias , a special email address. When you send an email
13 | to the reverse-alias, the email will be sent from your alias to the
14 | contact.
15 | This Youtube video can also quickly walk you through the steps:
16 | How to send emails from an alias
22 |
23 |
24 |
25 | Receiver:
26 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 | {{
43 | createdReverseAlias.existed
44 | ? "You have created this reverse-alias before:"
45 | : "Reverse-alias is created:"
46 | }}
47 |
48 |
49 |
57 |
58 | {{ createdReverseAlias.reverse_alias }}
59 |
60 |
61 |
62 |
63 | You can send email from one of these mailbox(es):
64 |
65 |
66 | {{ mailbox.email }}
67 |
68 |
69 | The email will be forwarded to
70 | {{ createdReverseAlias.contact }} .
72 | The receiver will see {{ alias.email }} as your email
73 | address.
74 |
75 |
76 |
77 |
78 |
84 | Create a reverse-alias
85 |
86 |
87 |
88 |
89 | Back
90 |
91 |
92 |
93 |
94 |
95 |
96 |
145 |
--------------------------------------------------------------------------------
/src/popup/components/SelfHostSetting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | If you self-host SimpleLogin, you can change the API URL to your server
6 | address.
7 |
8 |
The default API URL is https://app.simplelogin.io
9 |
10 |
11 |
18 |
19 | Set API URL
20 |
21 |
22 |
23 |
24 |
25 |
26 |
59 |
--------------------------------------------------------------------------------
/src/popup/components/SplashScreen.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
75 |
--------------------------------------------------------------------------------
/src/popup/components/TextareaAutosize.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
121 |
--------------------------------------------------------------------------------
/src/popup/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SimpleLogin Extension
6 |
7 |
8 |
9 | <% if (NODE_ENV === 'development') { %>
10 |
11 | <% } %>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/popup/popup.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import App from "./App";
3 | import Clipboard from "v-clipboard";
4 | import Toasted from "vue-toasted";
5 | import BootstrapVue from "bootstrap-vue";
6 | import SLStorage from "./SLStorage";
7 |
8 | import * as Sentry from "@sentry/browser";
9 | import * as Integrations from "@sentry/integrations";
10 | import VModal from "vue-js-modal";
11 | import VueRouter from "vue-router";
12 | import ToggleButton from "vue-js-toggle-button";
13 |
14 | import { library } from "@fortawesome/fontawesome-svg-core";
15 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
16 | import {
17 | faRandom,
18 | faExternalLinkAlt,
19 | faTrash,
20 | faLongArrowAltUp,
21 | faChevronLeft,
22 | faCopy,
23 | faStar,
24 | faSave,
25 | faBug,
26 | faQuestionCircle,
27 | faCog,
28 | faPaperPlane,
29 | faArrowLeft,
30 | } from "@fortawesome/free-solid-svg-icons";
31 |
32 | library.add(
33 | faRandom,
34 | faExternalLinkAlt,
35 | faTrash,
36 | faLongArrowAltUp,
37 | faChevronLeft,
38 | faCopy,
39 | faStar,
40 | faSave,
41 | faBug,
42 | faQuestionCircle,
43 | faCog,
44 | faPaperPlane,
45 | faArrowLeft
46 | );
47 |
48 | global.browser = require("webextension-polyfill");
49 | Vue.prototype.$browser = global.browser;
50 |
51 | // async wrapper
52 | async function initApp() {
53 | const apiUrl = await SLStorage.get(SLStorage.SETTINGS.API_URL);
54 |
55 | if (
56 | // only enable Sentry for non self-hosting users
57 | apiUrl === SLStorage.DEFAULT_SETTINGS[SLStorage.SETTINGS.API_URL] &&
58 | // and not in development mode
59 | process.env.NODE_ENV !== "development"
60 | ) {
61 | Sentry.init({
62 | dsn: "https://6990c2b0a6e94b57a2b80587efcb4354@api.protonmail.ch/core/v4/reports/sentry/51",
63 | integrations: [
64 | new Integrations.Vue({ Vue, attachProps: true, logErrors: true }),
65 | ],
66 | environment: process.env.BETA ? "beta" : "prod",
67 | });
68 | }
69 |
70 | Vue.use(Clipboard);
71 | Vue.use(Toasted, { duration: 1000, position: "bottom-right" });
72 | Vue.use(BootstrapVue);
73 | Vue.use(VModal, { dialog: true });
74 | Vue.use(VueRouter);
75 | Vue.use(ToggleButton);
76 | Vue.component("font-awesome-icon", FontAwesomeIcon);
77 |
78 | /* eslint-disable no-new */
79 | new Vue({
80 | el: "#app",
81 | render: (h) => h(App),
82 | });
83 | }
84 |
85 | initApp();
86 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const ejs = require('ejs');
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4 | const CopyWebpackPlugin = require('copy-webpack-plugin');
5 | const { VueLoaderPlugin } = require('vue-loader');
6 | const { version, betaRev } = require('./package.json');
7 | const fs = require('fs');
8 |
9 |
10 | const loadDevConfig = () => {
11 | if (fs.existsSync('./.dev.json')) {
12 | return JSON.parse(fs.readFileSync('./.dev.json').toString());
13 | } else {
14 | return JSON.parse(fs.readFileSync('./.dev.sample.json').toString());
15 | }
16 | };
17 |
18 | const devConfig = loadDevConfig();
19 |
20 | const config = {
21 | mode: process.env.NODE_ENV,
22 | context: __dirname + '/src',
23 | entry: {
24 | 'background': './background/index.js',
25 | 'popup/popup': './popup/popup.js',
26 | },
27 | output: {
28 | path: __dirname + '/dist',
29 | filename: '[name].js',
30 | },
31 | resolve: {
32 | extensions: ['.js', '.vue'],
33 | },
34 | module: {
35 | rules: [
36 | {
37 | test: /\.vue$/,
38 | use: 'vue-loader',
39 | },
40 | {
41 | test: /\.js$/,
42 | loader: 'babel-loader',
43 | exclude: /node_modules/,
44 | options: {
45 | presets: [
46 | {'plugins': ['@babel/plugin-proposal-class-properties']
47 | }
48 | ]
49 | }
50 | },
51 | {
52 | test: /\.css$/,
53 | use: [MiniCssExtractPlugin.loader, 'css-loader'],
54 | },
55 | {
56 | test: /\.scss$/,
57 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
58 | },
59 | {
60 | test: /\.sass$/,
61 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader?indentedSyntax'],
62 | },
63 | {
64 | test: /\.(png|jpg|jpeg|gif|svg|ico)$/,
65 | loader: 'file-loader',
66 | options: {
67 | name: '[name].[ext]',
68 | outputPath: '/images/',
69 | emitFile: false,
70 | },
71 | },
72 | {
73 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
74 | loader: 'file-loader',
75 | options: {
76 | name: '[name].[ext]',
77 | outputPath: '/fonts/',
78 | emitFile: false,
79 | },
80 | },
81 | ],
82 | },
83 | plugins: [
84 | new webpack.DefinePlugin({
85 | global: 'window',
86 | }),
87 | new VueLoaderPlugin(),
88 | new MiniCssExtractPlugin({
89 | filename: '[name].css',
90 | }),
91 | new CopyWebpackPlugin({
92 | patterns: [
93 | { from: 'icons', to: 'icons', globOptions: { ignoreFiles: ['icon.xcf'] } },
94 | { from: 'images', to: 'images' },
95 | { from: 'content_script', to: 'content_script' },
96 | { from: 'popup/popup.html', to: 'popup/popup.html', transform: transformHtml },
97 | {
98 | from: 'manifest.json',
99 | to: 'manifest.json',
100 | transform: (content) => {
101 | const jsonContent = JSON.parse(content);
102 | jsonContent.version = version;
103 |
104 | if (config.mode === 'development') {
105 | jsonContent.permissions = jsonContent.permissions.concat(devConfig.permissions);
106 | }
107 |
108 | if (process.env.BETA) {
109 | const geckoId = jsonContent.browser_specific_settings.gecko.id;
110 | jsonContent.name = 'SimpleLogin BETA';
111 | jsonContent.icons = {
112 | '48': 'icons/icon_beta_48.png',
113 | '128': 'icons/icon_beta_128.png'
114 | };
115 | jsonContent.version = version + '.' + betaRev;
116 | jsonContent.browser_specific_settings.gecko.id = geckoId.replace('@', '-beta@');
117 | }
118 |
119 | if (process.env.FIREFOX) {
120 | jsonContent.background = {
121 | "scripts": ["background.js"]
122 | };
123 | } else { // CHROME
124 | jsonContent.background = {
125 | "service_worker": "background.js",
126 | "type": "module"
127 | };
128 | }
129 |
130 | if (process.env.LITE) {
131 | // Remove "All sites" permissions
132 | const PERMISSIONS_TO_REMOVE = [
133 | "https://*/*",
134 | "http://*/*"
135 | ];
136 |
137 | const finalPermissions = [];
138 | for (const perm of jsonContent.permissions) {
139 | if (!PERMISSIONS_TO_REMOVE.includes(perm)) {
140 | finalPermissions.push(perm);
141 | }
142 | }
143 | jsonContent.permissions = finalPermissions;
144 |
145 | // Change metadata
146 | jsonContent.name = "SimpleLogin Without SL icon";
147 | jsonContent.short_name = "SimpleLogin Without SL icon";
148 | }
149 |
150 | if (process.env.MAC) {
151 | jsonContent.permissions.push("nativeMessaging");
152 | }
153 |
154 | return JSON.stringify(jsonContent, null, 2);
155 | },
156 | },
157 | ]
158 | }),
159 | ],
160 | };
161 |
162 | console.log(`[Build] Using config.mode = ${config.mode}`);
163 |
164 | if (config.mode === 'development') {
165 | const pluginConfig = {
166 | 'devConfig': JSON.stringify(devConfig),
167 | 'process.env.BETA': JSON.stringify(!!process.env.BETA),
168 | };
169 |
170 | console.log(`[development] Using pluginConfig: ${JSON.stringify(pluginConfig)}`);
171 | config.plugins = (config.plugins || []).concat([
172 | new webpack.DefinePlugin(pluginConfig),
173 | ]);
174 | }
175 |
176 | if (process.env.MAC){
177 | config.plugins = (config.plugins || []).concat([
178 | new webpack.DefinePlugin({
179 | 'process.env.MAC': JSON.stringify(!!process.env.MAC),
180 | }),
181 | ]);
182 | }
183 |
184 | if (config.mode === 'production') {
185 | const pluginConfig = {
186 | 'devConfig': 'null',
187 | 'process.env': {
188 | 'NODE_ENV': '"production"',
189 | 'BETA': JSON.stringify(!!process.env.BETA),
190 | }
191 | };
192 | console.log(`[production] Using pluginConfig: ${JSON.stringify(pluginConfig)}`);
193 | config.plugins = (config.plugins || []).concat([
194 | new webpack.DefinePlugin(pluginConfig),
195 | ]);
196 | }
197 |
198 | function transformHtml(content) {
199 | return ejs.render(content.toString(), {
200 | ...process.env,
201 | devConfig,
202 | });
203 | }
204 |
205 | if (config.mode === 'production') {
206 | config.devtool="source-map";
207 | } else {
208 | config.devtool="cheap-source-map";
209 | }
210 |
211 | module.exports = config;
--------------------------------------------------------------------------------