├── src
├── browser_action
│ ├── css
│ │ ├── browser_action.scss
│ │ ├── _variables.scss
│ │ ├── _base.scss
│ │ └── _search.scss
│ ├── templates
│ │ ├── departments.hbs
│ │ ├── search_results.hbs
│ │ └── finder.hbs
│ └── js
│ │ ├── list_selection.coffee
│ │ ├── browser_action.coffee
│ │ ├── lead_button.coffee
│ │ ├── email_finder.coffee
│ │ └── domain_search.coffee
├── shared
│ ├── img
│ │ ├── icon16.png
│ │ ├── icon19.png
│ │ ├── icon38.png
│ │ ├── icon48.png
│ │ ├── icon128.png
│ │ ├── icon19_grey.png
│ │ ├── icon38_grey.png
│ │ ├── search_grey.png
│ │ ├── location_icon.png
│ │ ├── default_lead_icon.png
│ │ ├── orange_transparent_logo.png
│ │ └── white_transparent_logo.png
│ ├── fonts
│ │ ├── Inter-Medium.woff2
│ │ ├── fa-solid-900.woff2
│ │ ├── Inter-Regular.woff2
│ │ ├── Inter-SemiBold.woff2
│ │ ├── fa-brands-400.woff2
│ │ └── fa-regular-400.woff2
│ ├── js
│ │ ├── use-counter.coffee
│ │ ├── analytics.coffee
│ │ ├── api.coffee
│ │ ├── account.coffee
│ │ ├── utilities.coffee
│ │ └── lib
│ │ │ └── purify.min.js
│ └── css
│ │ ├── fonts.css
│ │ └── lib
│ │ ├── bootstrap-tooltip.min.css
│ │ └── fontawesome-all.css
├── background.coffee
├── content_script
│ ├── js
│ │ ├── hunter-extension-detection.coffee
│ │ ├── hunter-authentication.coffee
│ │ └── websites-source.coffee
│ └── css
│ │ └── websites-sources.scss
├── source_popup
│ ├── css
│ │ └── source-popup.scss
│ ├── popup.html
│ └── js
│ │ └── source-popup.coffee
├── background
│ ├── open-pages.coffee
│ └── icon.coffee
├── manifest.json
└── _locales
│ ├── en
│ └── messages.json
│ └── fr
│ └── messages.json
├── .gitignore
├── config
├── watch.js
├── _edge.js
├── _firefox.js
└── _chrome.js
├── package.json
├── Gruntfile.js
├── README.md
└── LICENSE
/src/browser_action/css/browser_action.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 | @import 'base';
3 | @import 'search';
4 |
--------------------------------------------------------------------------------
/src/shared/img/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/icon16.png
--------------------------------------------------------------------------------
/src/shared/img/icon19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/icon19.png
--------------------------------------------------------------------------------
/src/shared/img/icon38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/icon38.png
--------------------------------------------------------------------------------
/src/shared/img/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/icon48.png
--------------------------------------------------------------------------------
/src/shared/img/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/icon128.png
--------------------------------------------------------------------------------
/src/background.coffee:
--------------------------------------------------------------------------------
1 | try
2 | importScripts '/js/shared.js', '/js/background.js'
3 | catch e
4 | console.error e
5 |
--------------------------------------------------------------------------------
/src/shared/img/icon19_grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/icon19_grey.png
--------------------------------------------------------------------------------
/src/shared/img/icon38_grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/icon38_grey.png
--------------------------------------------------------------------------------
/src/shared/img/search_grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/search_grey.png
--------------------------------------------------------------------------------
/src/shared/img/location_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/location_icon.png
--------------------------------------------------------------------------------
/src/shared/fonts/Inter-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/fonts/Inter-Medium.woff2
--------------------------------------------------------------------------------
/src/shared/fonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/fonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/src/shared/fonts/Inter-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/fonts/Inter-Regular.woff2
--------------------------------------------------------------------------------
/src/shared/fonts/Inter-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/fonts/Inter-SemiBold.woff2
--------------------------------------------------------------------------------
/src/shared/fonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/fonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/src/shared/fonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/fonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/src/shared/img/default_lead_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/default_lead_icon.png
--------------------------------------------------------------------------------
/src/shared/img/orange_transparent_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/orange_transparent_logo.png
--------------------------------------------------------------------------------
/src/shared/img/white_transparent_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hunter-io/browser-extension/HEAD/src/shared/img/white_transparent_logo.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build-chrome
2 | build-chrome.zip
3 | /build-firefox
4 | build-firefox.zip
5 | /build-edge
6 | build-edge.zip
7 | Archive.zip
8 | /node_modules
9 | .DS_Store
10 | .sass-cache
11 | yarn.lock
12 | .ruby-version
13 |
--------------------------------------------------------------------------------
/src/content_script/js/hunter-extension-detection.coffee:
--------------------------------------------------------------------------------
1 | # We send 2 data points to the web app:
2 | # - That the extension is installed.
3 | # - The version currently installed. Can be used to debug with support.
4 | #
5 | $("#is-extension-installed").val true
6 | $("#extension-version").val chrome.runtime.getManifest().version
7 |
--------------------------------------------------------------------------------
/src/shared/js/use-counter.coffee:
--------------------------------------------------------------------------------
1 | # Every time a user make a successful search, we count it in Chrome local storage.
2 | # This is used to display a notification to rate the extension or give feedback.
3 | #
4 | countCall = ->
5 | chrome.storage.sync.get { 'calls_count': 0 }, (value) ->
6 | value.calls_count++
7 | chrome.storage.sync.set 'calls_count': value.calls_count
8 |
--------------------------------------------------------------------------------
/src/shared/js/analytics.coffee:
--------------------------------------------------------------------------------
1 | # Counts the main actions made with the extension to know which features
2 | # are the most successful
3 | #
4 | Analytics = trackEvent: (eventName) ->
5 | url = 'https://hunter.io/events?name=' + eventName
6 | $.ajax
7 | url: url
8 | type: 'POST'
9 | dataType: 'json'
10 | jsonp: false
11 | success: (json) ->
12 | # Done!
13 |
14 | analytics = Object.create(Analytics)
15 |
--------------------------------------------------------------------------------
/src/source_popup/css/source-popup.scss:
--------------------------------------------------------------------------------
1 | html {
2 | overflow: hidden;
3 | }
4 |
5 | body {
6 | font-family: "Open Sans", sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | margin: 0;
10 | }
11 |
12 | #logo {
13 | height: 18px;
14 | }
15 |
16 | #message {
17 | font-size: 14px;
18 | line-height: 21px;
19 | margin-top: 15px;
20 | }
21 |
22 | strong {
23 | font-weight: 600;
24 | }
25 |
--------------------------------------------------------------------------------
/config/watch.js:
--------------------------------------------------------------------------------
1 | module.exports.tasks = {
2 | // Watch any modification to the package
3 | watch: {
4 | scripts: {
5 | files: ["src/**/*.js", "src/**/*.css", "src/**/*.scss", "src/**/*.html", "src/**/*.json", "src/**/*/*.coffee", "src/**/*/*.hbs"],
6 | tasks: ["coffee", "handlebars", "compass", "cssmin", "copy", "replace", "editmanifestforfirefox"],
7 | options: {
8 | spawn: false,
9 | }
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/browser_action/templates/departments.hbs:
--------------------------------------------------------------------------------
1 | {{#each departments}}
2 | {{#ifGreaterThanZero this.[1]}}
3 |
4 | {{#departmentName}}{{this.[0]}}{{/departmentName}}: {{this.[1]}}
5 |
6 | {{/ifGreaterThanZero}}
7 | {{/each}}
8 | {{#ifGreaterThanZero departments.[3].[1]}}
9 | ●●●
10 | {{/ifGreaterThanZero}}
11 |
--------------------------------------------------------------------------------
/src/content_script/js/hunter-authentication.coffee:
--------------------------------------------------------------------------------
1 | if location.pathname == "/chrome/welcome" or location.pathname == "/firefox/welcome" or location.pathname == "/search"
2 |
3 | # Hunter extension can be used without authentification but the data returned by
4 | # the API is limited. When the quota is reached, the user is invited to log in.
5 | # The extention will read the API key on the website and store it in
6 | # Chrome local storage.
7 | #
8 | api_key = document.getElementById("api_key").innerHTML.trim()
9 | Account.setApiKey api_key
10 |
--------------------------------------------------------------------------------
/src/background/open-pages.coffee:
--------------------------------------------------------------------------------
1 | # Open a tab when it's installed
2 | #
3 | chrome.runtime.onInstalled.addListener (object) ->
4 | if object.reason == "install"
5 | chrome.tabs.create url: "https://hunter.io/users/sign_up?from=chrome_extension&utm_source=chrome_extension&utm_medium=chrome_extension&utm_campaign=extension&utm_content=new_install"
6 | return
7 |
8 | # Open another tab when it's uninstalled
9 | #
10 | chrome.runtime.setUninstallURL "https://hunter.io/chrome/uninstall?from=chrome_extension&utm_source=chrome_extension&utm_medium=chrome_extension&utm_campaign=extension&utm_content=uninstall"
11 |
--------------------------------------------------------------------------------
/src/source_popup/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hunter-extension",
3 | "version": "1.0.0",
4 | "description": "Hunter's browser extension",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/hunter-io/browser-extension.git"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/hunter-io/browser-extension/issues"
11 | },
12 | "homepage": "https://github.com/hunter-io/browser-extension#readme",
13 | "devDependencies": {
14 | "grunt": "^1.5.3",
15 | "grunt-contrib-coffee": "^2.1.0",
16 | "grunt-contrib-compass": "^0.8.0",
17 | "grunt-contrib-copy": "^1.0.0",
18 | "grunt-contrib-cssmin": "^2.2.1",
19 | "grunt-contrib-handlebars": "^3.0.0",
20 | "grunt-contrib-watch": "^1.0.0",
21 | "grunt-text-replace": "^0.4.0",
22 | "load-grunt-configs": "^1.0.0"
23 | },
24 | "dependencies": {
25 | "lodash": "^4.17.21"
26 | },
27 | "license": "Apache-2.0"
28 | }
29 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 | // Load the plugins
3 | grunt.loadNpmTasks("grunt-contrib-watch");
4 | grunt.loadNpmTasks("grunt-contrib-coffee");
5 | grunt.loadNpmTasks("grunt-contrib-compass");
6 | grunt.loadNpmTasks("grunt-contrib-cssmin");
7 | grunt.loadNpmTasks("grunt-contrib-copy");
8 | grunt.loadNpmTasks("grunt-text-replace");
9 | grunt.loadNpmTasks("grunt-contrib-handlebars");
10 |
11 | // Load the various task configuration files
12 | var configs = require("load-grunt-configs")(grunt);
13 | grunt.initConfig(configs);
14 |
15 | grunt.registerTask("editmanifestforfirefox", function() {
16 | var manifest = grunt.file.readJSON("./src/manifest.json");
17 |
18 | // On Firefox, with Manifest V3, we use "background" files and not service workers
19 | manifest.background.scripts =["js/lib/jquery.min.js", "js/shared.js", "js/background.js"]
20 | delete manifest.background.service_worker;
21 |
22 | // On Firefox, with Manifest V3, we have to sign the extension with our unique ID
23 | manifest.browser_specific_settings = { "gecko": { "id": "firefox@hunter.io" } };
24 |
25 | grunt.file.write("./build-firefox/manifest.json", JSON.stringify(manifest, null, 2));
26 | grunt.log.writeln("Manifest updated for Firefox");
27 | });
28 |
29 | // Default task
30 | grunt.registerTask("default", ["watch"]);
31 | }
32 |
--------------------------------------------------------------------------------
/src/content_script/css/websites-sources.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Hightlight of the email address found on the page
3 | */
4 |
5 | .hunter-email {
6 | background-color: yellow;
7 | color: #222;
8 | display: inline-block;
9 | padding: 0 2px;
10 | }
11 |
12 | /*
13 | Pointer of Hunter that shows the email address found
14 | */
15 |
16 | #hunter-email-pointer {
17 | display: none;
18 | position: absolute;
19 | width: 50px;
20 | filter: drop-shadow(0px 1px 2px rgba(0,0,0,0.5));
21 | z-index: 1000001;
22 | border: 0;
23 | margin: 0;
24 | padding: 0;
25 | }
26 |
27 | /*
28 | Message that indicates the status of the search of the email address on the
29 | page.
30 | */
31 |
32 | #hunter-email-status {
33 | display: none;
34 | position: fixed;
35 | box-sizing: content-box;
36 | right: 20px;
37 | top: 20px;
38 | background-color: #fff;
39 | color: #fff;
40 | border: 0;
41 | border-radius: 8px;
42 | padding: 25px;
43 | font-size: 13px;
44 | width: 280px;
45 | height: 100px;
46 | box-shadow: 0 0px 40px 0px rgba(0,0,0,0.3);
47 | z-index: 1000000001;
48 | }
49 |
50 | #hunter-email-status-close {
51 | display: none;
52 | position: fixed;
53 | right: 47px;
54 | top: 39px;
55 | font-size: 28px;
56 | color: #aaa;
57 | z-index: 1000000002;
58 | }
59 |
60 | #hunter-email-status-close:hover {
61 | cursor: pointer;
62 | color: #888;
63 | }
64 |
--------------------------------------------------------------------------------
/src/source_popup/js/source-popup.coffee:
--------------------------------------------------------------------------------
1 | findGetParameter = (parameterName) ->
2 | result = null
3 | tmp = []
4 | location.search.substr(1).split("&").forEach (item) ->
5 | tmp = item.split("=")
6 | if tmp[0] == parameterName
7 | result = decodeURIComponent(tmp[1])
8 | result
9 |
10 | email = DOMPurify.sanitize(findGetParameter("email"))
11 | count = DOMPurify.sanitize(findGetParameter("count"))
12 | message = DOMPurify.sanitize(findGetParameter("message"))
13 |
14 | if findGetParameter("message") == "found"
15 | if findGetParameter("count") == "1"
16 | popup_message = "" + DOMPurify.sanitize(email) + " has been found on the page."
17 | else
18 | popup_message = "" + DOMPurify.sanitize(email) + " has been found " + DOMPurify.sanitize(count) + " times on the page."
19 | else if findGetParameter("message") == "mailto"
20 | popup_message = "" + DOMPurify.sanitize(email) + " found in the \"mailto:\" link."
21 | else if findGetParameter("message") == "code"
22 | popup_message = "" + DOMPurify.sanitize(email) + " isn't visible but is publicly accessible in the code of the page."
23 | else
24 | popup_message = "The email address couldn\'t be found on the page. This probably means this page has been updated since our latest visit."
25 |
26 | document.getElementById("message").innerHTML = popup_message
27 |
--------------------------------------------------------------------------------
/src/shared/css/fonts.css:
--------------------------------------------------------------------------------
1 | /*
2 | Font Awesome
3 | */
4 |
5 | @font-face {
6 | font-family: 'Font Awesome 6 Brands';
7 | font-style: normal;
8 | font-weight: normal;
9 | src: url("chrome-extension://__MSG_@@extension_id__/fonts/fa-brands-400.woff2") format("woff2");
10 | }
11 |
12 | @font-face {
13 | font-family: 'Font Awesome 6 Pro';
14 | font-style: normal;
15 | font-weight: 400;
16 | src: url("chrome-extension://__MSG_@@extension_id__/fonts/fa-regular-400.woff2") format("woff2");
17 | }
18 |
19 | @font-face {
20 | font-family: 'Font Awesome 6 Pro';
21 | font-style: normal;
22 | font-weight: 900;
23 | src: url("chrome-extension://__MSG_@@extension_id__/fonts/fa-solid-900.woff2") format("woff2");
24 | }
25 |
26 |
27 | /*
28 | Inter
29 | */
30 |
31 | @font-face {
32 | font-family: 'Inter UI';
33 | font-style: normal;
34 | font-weight: normal;
35 | src: url('chrome-extension://__MSG_@@extension_id__/fonts/Inter-Regular.woff2') format('woff2');
36 | }
37 |
38 | @font-face {
39 | font-family: 'Inter UI';
40 | font-style: normal;
41 | font-weight: 500;
42 | src: url('chrome-extension://__MSG_@@extension_id__/fonts/Inter-Medium.woff2') format('woff2');
43 | }
44 |
45 | @font-face {
46 | font-family: 'Inter UI';
47 | font-style: normal;
48 | font-weight: 600;
49 | src: url('chrome-extension://__MSG_@@extension_id__/fonts/Inter-SemiBold.woff2') format('woff2');
50 | }
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hunter's browser extension
2 | Hunter's browser extension is an easy way to find email addresses from anywhere on the web, with just one click.
3 |
4 | * [Chrome Web Store](https://chrome.google.com/webstore/detail/hunter/hgmhmanijnjhaffoampdlllchpolkdnj)
5 | * [Add-on for Edge](https://microsoftedge.microsoft.com/addons/detail/hunter-email-finder-ext/dmgcgojogkfomifjfeeafajhmgilkofk)
6 | * [Add-on for Firefox](https://addons.mozilla.org/en-US/firefox/addon/hunterio/)
7 |
8 | [Hunter's API](https://hunter.io/api) is used to fetch the data and users' accounts information.
9 |
10 | ## Generate the builds
11 |
12 | The builds are automatically generated with Grunt for each browser. To install it globally, run:
13 |
14 | ```shell
15 | npm install -g grunt-cli
16 | ```
17 |
18 | To watch the changes and launch the builds, change to the project's root directory and run:
19 |
20 | ```shell
21 | grunt
22 | ```
23 |
24 | ## Test the extension locally
25 |
26 | On Chrome:
27 |
28 | 1. Go to the extensions page (chrome://extensions)
29 | 2. Click **Load unpacked extension...**
30 | 3. Select the folder `build-chrome`
31 |
32 | On Edge:
33 |
34 | 1. Go to the extensions page (edge://extensions/)
35 | 2. Click **Load unpacked**
36 | 3. Select the folder `build-edge`
37 |
38 | On Firefox:
39 |
40 | 1. Go to the debugging page (about:debugging)
41 | 2. Go to the tab **This Firefox**
42 | 3. Click **Load Temporary Add-on...**
43 | 4. Select a file inside `build-firefox`
44 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__MSG_extension_name__",
3 | "short_name": "Hunter",
4 | "version": "3.1.4",
5 | "manifest_version": 3,
6 | "description": "__MSG_extension_description__",
7 | "homepage_url": "https://hunter.io",
8 | "icons": {
9 | "16": "img/icon16.png",
10 | "48": "img/icon48.png",
11 | "128": "img/icon128.png"
12 | },
13 | "default_locale": "en",
14 | "action": {
15 | "default_icon": {
16 | "19": "img/icon19.png",
17 | "38": "img/icon38.png"
18 | },
19 | "default_title": "Hunter",
20 | "default_popup": "html/browser_popup.html"
21 | },
22 | "permissions": [
23 | "tabs",
24 | "storage"
25 | ],
26 | "host_permissions": [
27 | "https://*.hunter.io/"
28 | ],
29 | "background": {
30 | "service_worker": "background.js"
31 | },
32 | "content_scripts": [
33 | {
34 | "matches": [
35 | "*://*.hunter.io/*"
36 | ],
37 | "js": [
38 | "js/lib/jquery.min.js",
39 | "js/shared.js",
40 | "js/hunter_content_script.js"
41 | ]
42 | },
43 | {
44 | "matches": [
45 | "*://*/*"
46 | ],
47 | "js": [
48 | "js/lib/jquery.min.js",
49 | "js/lib/purify.min.js",
50 | "js/shared.js",
51 | "js/websites_content_script.js"
52 | ],
53 | "css": [
54 | "css/websites-sources.css"
55 | ]
56 | }
57 | ],
58 | "web_accessible_resources": [{
59 | "resources": ["img/location_icon.png", "html/source_popup.html"],
60 | "matches": ["*://*/*"]
61 | }]
62 | }
63 |
--------------------------------------------------------------------------------
/src/shared/css/lib/bootstrap-tooltip.min.css:
--------------------------------------------------------------------------------
1 | .tooltip.top .tooltip-arrow,.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:0;border-width:5px 5px 0;border-top-color:#000}.tooltip{position:absolute;z-index:1070;display:block;font-family:Lato,Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{left:50%;margin-left:-5px}.tooltip.top-left .tooltip-arrow{right:5px;margin-bottom:-5px}.tooltip.top-right .tooltip-arrow{left:5px;margin-bottom:-5px}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{top:0;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom .tooltip-arrow{left:50%;margin-left:-5px}.tooltip.bottom-left .tooltip-arrow{right:5px;margin-top:-5px}.tooltip.bottom-right .tooltip-arrow{left:5px;margin-top:-5px}
2 |
--------------------------------------------------------------------------------
/src/background/icon.coffee:
--------------------------------------------------------------------------------
1 | window = self
2 |
3 | # When an URL changes
4 | #
5 | # Check if email addresses are available for the current domain and update the
6 | # color of the browser icon
7 | #
8 | LaunchColorChange = ->
9 | chrome.tabs.query {
10 | currentWindow: true
11 | active: true
12 | }, (tabArray) ->
13 | if tabArray[0]["url"] != window.currentDomain
14 | try
15 | hostname = new URL(tabArray[0]['url'])
16 | Utilities.findRelevantDomain hostname.host, (domain) ->
17 | window.currentDomain = domain
18 | updateIconColor()
19 | catch e
20 | # The tab is not a valid URL
21 | setGreyIcon()
22 |
23 | # API call to check if there is at least one email address
24 | # We use a special API call for this task to minimize the ressources.
25 | #
26 | # Endpoint: https://extension-api.hunter.io/data-for-domain?domain=site.com
27 | #
28 | updateIconColor = ->
29 | if window.currentDomain.indexOf(".") == -1
30 | setGreyIcon()
31 | else
32 | Utilities.dataFoundForDomain window.currentDomain, (results) ->
33 | if results
34 | setColoredIcon()
35 | else
36 | setGreyIcon()
37 |
38 |
39 | setGreyIcon = ->
40 | chrome.action.setIcon path:
41 | "19": chrome.runtime.getURL("../img/icon19_grey.png")
42 | "38": chrome.runtime.getURL("../img/icon38_grey.png")
43 | return
44 |
45 | setColoredIcon = ->
46 | chrome.action.setIcon path:
47 | "19": chrome.runtime.getURL("../img/icon19.png")
48 | "38": chrome.runtime.getURL("../img/icon38.png")
49 | return
50 |
51 | # When an URL change
52 | chrome.tabs.onUpdated.addListener (tabid, changeinfo, tab) ->
53 | if tab != undefined
54 | if tab.url != undefined and changeinfo.status == "complete"
55 | LaunchColorChange()
56 |
57 | # When the active tab changes
58 | chrome.tabs.onActivated.addListener ->
59 | LaunchColorChange()
60 |
--------------------------------------------------------------------------------
/config/_edge.js:
--------------------------------------------------------------------------------
1 | module.exports.tasks = {
2 | // Copy the build for Chrome before making the adaptations
3 | copy: {
4 | build_edge: {
5 | expand: true,
6 | cwd: "build-chrome/",
7 | src: ["**"],
8 | dest: "build-edge/"
9 | }
10 | },
11 |
12 | // Do all the replacements to adapt from Chrome to Edge
13 | replace: {
14 | tracking_parameters_edge: {
15 | src: ["build-edge/**/*.js", "build-edge/**/*.html"],
16 | overwrite: true,
17 | replacements: [{
18 | from: "utm_source=chrome_extension",
19 | to: "utm_source=edge_addon"
20 | },
21 | {
22 | from: "utm_medium=chrome_extension",
23 | to: "utm_medium=edge_addon"
24 | },
25 | {
26 | from: "from=chrome_extension",
27 | to: "from=edge_addon"
28 | }]
29 | },
30 |
31 | website_welcome_url_edge: {
32 | src: ["build-edge/**/*.js", "build-edge/**/*.html"],
33 | overwrite: true,
34 | replacements: [{
35 | from: "/chrome/welcome",
36 | to: "/edge/welcome"
37 | }]
38 | },
39 |
40 | website_uninstall_url_edge: {
41 | src: ["build-edge/**/*.js", "build-edge/**/*.html"],
42 | overwrite: true,
43 | replacements: [{
44 | from: "/chrome/uninstall",
45 | to: "/edge/uninstall"
46 | }]
47 | },
48 |
49 | origin_header_edge: {
50 | src: ["build-edge/**/*.js"],
51 | overwrite: true,
52 | replacements: [{
53 | from: "\"Email-Hunter-Origin\": \"chrome_extension\"",
54 | to: "\"Email-Hunter-Origin\": \"edge_addon\"",
55 | }]
56 | },
57 |
58 | store_link_edge: {
59 | src: ["build-edge/html/browser_popup.html"],
60 | overwrite: true,
61 | replacements: [{
62 | from: "https://chrome.google.com/webstore/detail/email-hunter/hgmhmanijnjhaffoampdlllchpolkdnj/reviews",
63 | to: "https://microsoftedge.microsoft.com/addons/detail/hunter-email-finder-ext/dmgcgojogkfomifjfeeafajhmgilkofk"
64 | }]
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/shared/js/api.coffee:
--------------------------------------------------------------------------------
1 | Api =
2 | # Domain Search
3 | domainSearch: (domain, department, type, api_key) ->
4 | if department != null && typeof department != 'undefined'
5 | department = '&department=' + department
6 | else
7 | department = ''
8 |
9 | if type != null && typeof type != 'undefined'
10 | type = '&type=' + type
11 | else
12 | type = ''
13 |
14 | if api_key
15 | auth = '&api_key=' + api_key
16 | 'https://api.hunter.io/v2/domain-search?limit=10&offset=0&domain=' + domain + department + type + auth
17 | else
18 | 'https://api.hunter.io/trial/v2/domain-search?domain=' + domain
19 |
20 | # Email Finder
21 | emailFinder: (domain, full_name, api_key) ->
22 | if full_name != null && typeof full_name != 'undefined'
23 | full_name = '&full_name=' + encodeURIComponent(full_name)
24 | else
25 | full_name = ''
26 |
27 | if api_key
28 | auth = '&api_key=' + api_key
29 | 'https://api.hunter.io/v2/email-finder?domain=' + domain + full_name + auth
30 | else
31 | 'https://api.hunter.io/trial/v2/email-finder?domain=' + domain + full_name
32 |
33 | # Email Verifier
34 | emailVerifier: (email, api_key) ->
35 | if api_key
36 | auth = '&api_key=' + api_key
37 | 'https://api.hunter.io/v2/email-verifier?email=' + encodeURIComponent(email) + auth
38 | else
39 | 'https://api.hunter.io/trial/v2/email-finder?domain=' + encodeURIComponent(email)
40 |
41 | # Email count
42 | emailCount: (domain) ->
43 | 'https://api.hunter.io/v2/email-count?domain=' + domain
44 |
45 | # Leads
46 | leads: (api_key) ->
47 | 'https://api.hunter.io/v2/leads?api_key=' + api_key
48 |
49 | # Leads exist
50 | leadsExist: (email, api_key) ->
51 | 'https://api.hunter.io/v2/leads/exist?email=' + encodeURIComponent(email) + '&api_key=' + api_key
52 |
53 | # Leads lists
54 | leadsList: (api_key) ->
55 | 'https://api.hunter.io/v2/leads_lists?limit=100&api_key=' + api_key
56 |
57 | # Check if there is any email for a domain name
58 | dataForDomain: (domain) ->
59 | 'https://extension-api.hunter.io/data-for-domain?domain=' + domain
60 |
--------------------------------------------------------------------------------
/src/browser_action/js/list_selection.coffee:
--------------------------------------------------------------------------------
1 | ListSelection =
2 | appendSelector: ->
3 | _this = this
4 | _this.getLeadsLists (json) ->
5 | if json != "none"
6 | $(".list-select-container").append ""
7 |
8 | # We determine the selected list
9 | if window.current_leads_list_id
10 | selected_list_id = window.current_leads_list_id
11 | else
12 | selected_list_id = json.data.leads_lists[0].id
13 |
14 | # We add all the lists in the select field
15 | json.data.leads_lists.forEach (val, i) ->
16 | if parseInt(selected_list_id) == parseInt(val.id)
17 | selected = "selected='selected'"
18 | else
19 | selected = ""
20 | $(".leads-manager__list").append ""
21 |
22 | # We add a link to the current list
23 | $(".leads-manager__link").attr "href", "https://hunter.io/leads?leads_list_id=" + selected_list_id + "&utm_source=chrome_extension&utm_medium=chrome_extension&utm_campaign=extension&utm_content=browser_popup"
24 | $(".leads-manager__list").append ""
25 |
26 | _this.updateCurrent()
27 |
28 | updateCurrent: ->
29 | $(".leads-manager__list").on "change", ->
30 | if $(this).val() == "new_list"
31 | Utilities.openInNewTab "https://hunter.io/leads-lists/new?utm_source=chrome_extension&utm_medium=chrome_extension&utm_campaign=extension"
32 | else
33 | chrome.storage.sync.set "current_leads_list_id": $(this).val()
34 | window.current_leads_list_id = $(this).val()
35 | $(".leads-manager__link").attr "href", "https://hunter.io/leads?leads_list_id="+$(this).val()+"&utm_source=chrome_extension&utm_medium=chrome_extension&utm_campaign=extension&utm_content=browser_popup"
36 |
37 | getLeadsLists: (callback) ->
38 | Account.getApiKey (api_key) ->
39 | if api_key != ""
40 | $.ajax
41 | url: Api.leadsList(window.api_key)
42 | headers: "Email-Hunter-Origin": "chrome_extension"
43 | type: "GET"
44 | dataType: "json"
45 | jsonp: false
46 | success: (json) ->
47 | callback json
48 | else
49 | callback "none"
50 |
--------------------------------------------------------------------------------
/src/shared/js/account.coffee:
--------------------------------------------------------------------------------
1 | Account =
2 | get: (callback) ->
3 | @getApiKey (api_key) ->
4 | if api_key != ''
5 | url = 'https://api.hunter.io/v2/account?api_key=' + api_key
6 | $.ajax
7 | url: url
8 | headers: 'Email-Hunter-Origin': 'chrome_extension'
9 | type: 'GET'
10 | dataType: 'json'
11 | jsonp: false
12 | success: (json) ->
13 | callback json
14 | else
15 | callback 'none'
16 |
17 | setApiKey: (api_key) ->
18 | chrome.storage.sync.set { 'api_key': api_key }, ->
19 | console.info 'Hunter extension successfully installed.'
20 |
21 | getApiKey: (fn) ->
22 | chrome.storage.sync.get { 'api_key': false }, (value) ->
23 | if value.api_key
24 | api_key = value.api_key
25 | else
26 | api_key = ''
27 | fn api_key
28 |
29 | returnRequestsError: (fn) ->
30 | $.ajax
31 | url: "https://api.hunter.io/v2/account?api_key=" + window.api_key
32 | headers: 'Email-Hunter-Origin': 'chrome_extension'
33 | type: 'GET'
34 | data: format: 'json'
35 | dataType: 'json'
36 | error: ->
37 | return chrome.i18n.getMessage("something_went_wrong_on_our_side")
38 | success: (result) ->
39 | # the user account hasn't be validated yet. The phone is probably
40 | # missing.
41 | if result.data.requests.searches.available == 0 && result.data.requests.verifications.available == 0 && result.data.plan_level == 0
42 | fn(chrome.i18n.getMessage("please_complete_your_registration"))
43 |
44 | # Otherwise the user has probably been frozen.
45 | else if result.data.requests.searches.available == 0 && result.data.requests.verifications.available == 0 && result.data.plan_level > 0
46 | fn(chrome.i18n.getMessage("your_account_has_been_restricted"))
47 |
48 | # the user has a free account, so it means he consumed all his
49 | # free calls.
50 | else if result.data.plan_level == 0
51 | fn(chrome.i18n.getMessage("you_have_reached_your_daily_quota"))
52 |
53 | # the user account has been soft frozen.
54 | else if result.data.requests.searches.available == 250 && result.data.requests.verifications.available == 250
55 | fn(chrome.i18n.getMessage("you_have_reached_your_temporary_quota"))
56 |
57 | # the user is on a premium plan and reached his quota
58 | else if result.data.plan_level < 4
59 | fn(chrome.i18n.getMessage("your_have_reached_your_monthly_quota"))
60 |
61 | else
62 | fn(chrome.i18n.getMessage("your_have_reached_your_monthly_enterprise_quota"))
63 |
--------------------------------------------------------------------------------
/src/browser_action/js/browser_action.coffee:
--------------------------------------------------------------------------------
1 | loadAccountInformation = ->
2 | Account.get (json) ->
3 | if json == "none"
4 | $(".account-loading").hide()
5 | $(".account-not-logged").show()
6 | else
7 | $(".account-loading").hide()
8 |
9 | $(".account-calls-used").text Utilities.numberWithCommas(json.data.requests.searches.used)
10 | $(".account-calls-available").text Utilities.numberWithCommas(json.data.requests.searches.available)
11 | $(".account-avatar__img").attr "src", "https://ui-avatars.com/api/?name=" + json.data.first_name + "+" + json.data.last_name + "&background=0F8DF4&color=fff&rounded=true"
12 | $(".account-avatar__img").attr "alt", json.data.first_name + " " + json.data.last_name
13 |
14 | if json.data.plan_level == 0
15 | $(".account-upgrade-cta").show()
16 |
17 | $(".account-logged").show()
18 |
19 | displayError = (html) ->
20 | $("#error-message").html html
21 | $("html, body").animate({ scrollTop: 0 }, 300);
22 | $("#error-message-container").delay(300).slideDown()
23 |
24 | # Prepare what will be diplayed depending on the current page and domain
25 | # - If it's not on a domain name, a default page explain how it works
26 | # - If on LinkedIn, a page explains the feature is no longer available
27 | # - Otherwise, the Domain Search is launched
28 | #
29 | chrome.tabs.query {
30 | active: true
31 | currentWindow: true
32 | }, (tabs) ->
33 |
34 | # We track the event
35 | Analytics.trackEvent "Open browser popup"
36 |
37 | # We do the localization for the text in the HTML
38 | $("[data-locale]").each () ->
39 | $(this).text(chrome.i18n.getMessage($(this).data("locale")))
40 |
41 | $("[data-locale-title]").each () ->
42 | $(this).prop("title", chrome.i18n.getMessage($(this).data("localeTitle")))
43 |
44 | Account.getApiKey (api_key) ->
45 | # Get account information
46 | loadAccountInformation()
47 |
48 | chrome.storage.sync.get 'current_leads_list_id', (value) ->
49 | window.current_leads_list_id = value.current_leads_list_id
50 | ListSelection.appendSelector()
51 |
52 | window.api_key = api_key
53 | window.url = tabs[0].url
54 |
55 | currentDomain = new URL(tabs[0].url).hostname
56 |
57 | # We clean the subdomains when relevant
58 | Utilities.findRelevantDomain currentDomain, (domain) ->
59 | window.domain = domain
60 |
61 | # We display a special message on LinkedIn
62 | if window.domain == "linkedin.com"
63 | $("#linkedin-notification").show()
64 | $("#loading-placeholder").hide()
65 |
66 | # We display a soft 404 if there is no domain name
67 | else if window.domain == "" or window.domain.indexOf(".") == -1
68 | $("#empty-notification").show()
69 | $("#loading-placeholder").hide()
70 |
71 | else
72 | # Launch the Domain Search
73 | domainSearch = new DomainSearch
74 | domainSearch.launch()
75 |
--------------------------------------------------------------------------------
/src/browser_action/js/lead_button.coffee:
--------------------------------------------------------------------------------
1 | LeadButton = ->
2 | {
3 | first_name: @first_name
4 | last_name: @last_name
5 | position: @position
6 | email: @email
7 | company: @organization
8 | website: window.domain
9 | source: "Hunter (Domain Search)"
10 | phone_number: @phone_number
11 | linkedin_url: @linkedin
12 | twitter: @twitter
13 |
14 | saveButtonListener: (selector) ->
15 | _this = this
16 | $(selector).unbind("click").click ->
17 | lead_button = $(this)
18 | lead = lead_button.data()
19 |
20 | lead_button.prop "disabled", true
21 | lead_button.html("")
22 |
23 | attributes = [
24 | "first_name"
25 | "last_name"
26 | "position"
27 | "email"
28 | "company"
29 | "website"
30 | "source"
31 | "phone_number"
32 | "linkedin_url"
33 | "twitter"
34 | "email"
35 | ]
36 |
37 | lead = {}
38 | attributes.forEach (attribute) ->
39 | if _this[attribute] == undefined
40 | lead[attribute] = lead_button.data(attribute)
41 | else
42 | lead[attribute] = _this[attribute]
43 |
44 | if window.current_leads_list_id
45 | lead["leads_list_id"] = window.current_leads_list_id
46 |
47 | _this.save(lead, lead_button)
48 |
49 | save: (lead, button) ->
50 | $.ajax
51 | url: Api.leads(api_key)
52 | headers: "Email-Hunter-Origin": "chrome_extension"
53 | type: "POST"
54 | data: lead
55 | dataType: "json"
56 | jsonp: false
57 | error: (xhr, statusText, err) ->
58 | button.replaceWith(" " + chrome.i18n.getMessage("failed") + "")
59 | displayError DOMPurify.sanitize(xhr.responseJSON["errors"][0]["details"])
60 |
61 | if xhr.status == 422
62 | window.current_leads_list_id = undefined
63 |
64 | success: (response) ->
65 | button.replaceWith(" " + chrome.i18n.getMessage("saved") + "")
66 |
67 | disableSaveLeadButtonIfLeadExists: (selector) ->
68 | $(selector).each ->
69 | lead_button = $(this)
70 | lead = $(this).data()
71 | $.ajax
72 | url: Api.leadsExist(lead.email, window.api_key)
73 | headers: "Email-Hunter-Origin": "chrome_extension"
74 | type: "GET"
75 | data: format: "json"
76 | dataType: "json"
77 | jsonp: false
78 | success: (response) ->
79 | if response.data.id != null
80 | lead_button.replaceWith(" " + chrome.i18n.getMessage("saved") + "")
81 | }
82 |
--------------------------------------------------------------------------------
/src/shared/css/lib/fontawesome-all.css:
--------------------------------------------------------------------------------
1 | /* Font Awesome v6 */
2 | .fa,
3 | .fas,
4 | .fa-solid,
5 | .far,
6 | .fa-regular,
7 | .fab,
8 | .fa-brands {
9 | -moz-osx-font-smoothing: grayscale;
10 | -webkit-font-smoothing: antialiased;
11 | display: var(--fa-display, inline-block);
12 | font-family: "Font Awesome 6 Pro";
13 | font-style: normal;
14 | font-variant: normal;
15 | line-height: 1;
16 | text-rendering: auto; }
17 |
18 | .far {
19 | font-weight: 400;
20 | }
21 |
22 | .fas {
23 | font-weight: 900;
24 | }
25 |
26 | .fa-shield::before {
27 | content: "\f132"; }
28 |
29 | .fa-shield-check::before {
30 | content: "\f2f7"; }
31 |
32 | .fa-shield-exclamation::before {
33 | content: "\e247"; }
34 |
35 | .fa-shield-halved::before {
36 | content: "\f3ed"; }
37 |
38 | .fa-shield-xmark::before {
39 | content: "\e24c"; }
40 |
41 | .fa-shield-slash::before {
42 | content: "\e24b"; }
43 |
44 | .fa-user-shield::before {
45 | content: "\f505"; }
46 |
47 | .fa-xmark::before {
48 | content: "\f00d"; }
49 |
50 | .fa-spinner-third::before {
51 | content: "\f3f4"; }
52 |
53 |
54 | /* Brands */
55 | .fab,
56 | .fa-brands {
57 | font-family: 'Font Awesome 6 Brands';
58 | font-weight: 400;
59 | }
60 |
61 | .fa-linkedin:before {
62 | content: "\f08c"; }
63 |
64 | .fa-linkedin-in:before {
65 | content: "\f0e1"; }
66 |
67 | .fa-twitter:before {
68 | content: "\f099"; }
69 |
70 | .fa-twitter-square:before {
71 | content: "\f081"; }
72 |
73 | .fa-external-link::before {
74 | content: "\f08e"; }
75 |
76 | .fa-plus::before {
77 | content: "\2b"; }
78 |
79 | .fa-angle-up::before {
80 | content: "\f106"; }
81 |
82 | .fa-angle-down::before {
83 | content: "\f107"; }
84 |
85 | .fa-times::before {
86 | content: "\f00d"; }
87 |
88 | .fa-exclamation-triangle::before {
89 | content: "\f071"; }
90 |
91 | .fa-question::before {
92 | content: "\3f"; }
93 |
94 | .fa-browser::before {
95 | content: "\f37e"; }
96 |
97 | .fa-warning::before {
98 | content: "\f071"; }
99 |
100 | .fa-check::before {
101 | content: "\f00c"; }
102 |
103 | .fa-thumbs-down::before {
104 | content: "\f165"; }
105 |
106 | .fa-thumbs-up::before {
107 | content: "\f164"; }
108 |
109 | .fa-briefcase::before {
110 | content: "\f0b1"; }
111 |
112 | .fa-phone::before {
113 | content: "\f095"; }
114 |
115 | .fa-circle-user::before {
116 | content: "\f2bd"; }
117 |
118 | .fa-user-lock::before {
119 | content: "\f502"; }
120 |
121 | .fa-search::before {
122 | content: "\f002"; }
123 |
124 | /* Animation */
125 | .fa-spin {
126 | -webkit-animation-name: fa-spin;
127 | animation-name: fa-spin;
128 | -webkit-animation-delay: var(--fa-animation-delay, 0);
129 | animation-delay: var(--fa-animation-delay, 0);
130 | -webkit-animation-direction: var(--fa-animation-direction, normal);
131 | animation-direction: var(--fa-animation-direction, normal);
132 | -webkit-animation-duration: var(--fa-animation-duration, 2s);
133 | animation-duration: var(--fa-animation-duration, 2s);
134 | -webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
135 | animation-iteration-count: var(--fa-animation-iteration-count, infinite);
136 | -webkit-animation-timing-function: var(--fa-animation-timing, linear);
137 | animation-timing-function: var(--fa-animation-timing, linear); }
138 |
139 | @keyframes fa-spin {
140 | 0% { transform: rotate(0deg); }
141 | 100% { transform: rotate(360deg); }
142 | }
143 |
--------------------------------------------------------------------------------
/src/content_script/js/websites-source.coffee:
--------------------------------------------------------------------------------
1 | # When the page loads, if it comes from a source in Hunter products, there is
2 | # a hash at the end with the email address to highlight. It will search it in
3 | # this order:
4 | # 1. Visible email address?
5 | # 2. Email address in a mailto link?
6 | # 3. Email address visible elsewhere in the code?
7 |
8 | PageContent =
9 | getEmailInHash: ->
10 | if window.location.hash
11 | hash = window.location.hash
12 | if hash.indexOf(":") != -1 and hash.split(":")[0]
13 | email = hash.split(":")[1]
14 | if @validateEmail(email)
15 | email
16 | else
17 | false
18 | else
19 | false
20 | else
21 | false
22 |
23 | validateEmail: (email) ->
24 | re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
25 | re.test email
26 |
27 | highlightEmail: (email) ->
28 | containers = @getVisibleEmailContainers(email)
29 | if containers.length > 0
30 | # We add a tag around the matching visible email addresses to highlight them
31 | $(containers).html($(containers[0]).html().replace(email, "" + email + ""))
32 | @scrollToEmail()
33 | @addLocationIcon()
34 | @displayMessage "found", email, containers.length
35 | else
36 | # Next check: is it in a mailto address?
37 | @highlightMailto email
38 |
39 | highlightMailto: (email) ->
40 | if $("a[href=\"mailto:" + email + "\"]:visible").length
41 | $("a[href=\"mailto:" + email + "\"]").addClass "hunter-email"
42 | @scrollToEmail()
43 | @addLocationIcon()
44 | @displayMessage "mailto", email, 1
45 | else
46 | @searchCode(email)
47 |
48 | searchCode: (email) ->
49 | if $("html").html().indexOf(email) != -1
50 | @displayMessage "code", email, 0
51 | else
52 | @displayMessage "notfound", email, 0
53 |
54 | scrollToEmail: ->
55 | $("html, body").animate { scrollTop: $(".hunter-email:first").offset().top - 300 }, 500
56 |
57 | addLocationIcon: ->
58 | setTimeout (->
59 | $(".hunter-email").each (index) ->
60 | emailEl = $(this)
61 | position = emailEl.offset()
62 | emailWidth = emailEl.outerWidth()
63 | emailHeight = emailEl.outerHeight()
64 | $("body").prepend "
"
65 | $("#hunter-email-pointer").css
66 | "top": position.top - 63
67 | "left": position.left + emailWidth / 2 - 25
68 | $("#hunter-email-pointer").fadeIn 300
69 | ), 1500
70 |
71 | displayMessage: (message, email, count) ->
72 | src = chrome.runtime.getURL("/html/source_popup.html") + "?email=" + email + "&count=" + count + "&message=" + message
73 | $("body").prepend ""
74 | $("body").prepend "×
"
75 | $("#hunter-email-status, #hunter-email-status-close").fadeIn 300
76 |
77 | $("#hunter-email-status-close").on "click", ->
78 | $("#hunter-email-status, #hunter-email-status-close, #hunter-email-pointer").fadeOut()
79 |
80 | getVisibleEmailContainers: (email) ->
81 | return $("body, body *").contents().filter(->
82 | @nodeType == 3 and @nodeValue.indexOf(email) >= 0
83 | ).parent ":visible"
84 |
85 | email = PageContent.getEmailInHash()
86 | if email
87 | PageContent.highlightEmail email
88 |
--------------------------------------------------------------------------------
/config/_firefox.js:
--------------------------------------------------------------------------------
1 | module.exports.tasks = {
2 | // Copy the build for Chrome before making the adaptations
3 | copy: {
4 | build_firefox: {
5 | expand: true,
6 | cwd: "build-chrome/",
7 | src: ["**"],
8 | dest: "build-firefox/"
9 | }
10 | },
11 |
12 | // Do all the replacements to adapt from Chrome to Firefox
13 | replace: {
14 | browser_api_firefox: {
15 | src: ["build-firefox/**/*.js"],
16 | overwrite: true,
17 | replacements: [{
18 | from: "chrome.storage.sync",
19 | to: "browser.storage.local"
20 | },
21 | {
22 | from: "chrome.runtime",
23 | to: "browser.runtime"
24 | },
25 | {
26 | from: "chrome.extension.onMessage.addListener",
27 | to: "browser.runtime.onMessage.addListener"
28 | },
29 | {
30 | from: "chrome.extension.getURL",
31 | to: "browser.extension.getURL"
32 | },
33 | {
34 | from: "chrome.action.setIcon",
35 | to: "browser.action.setIcon"
36 | },
37 | {
38 | from: "chrome.tabs",
39 | to: "browser.tabs"
40 | },
41 | {
42 | from: "chrome.contextMenus",
43 | to: "browser.contextMenus"
44 | }]
45 | },
46 |
47 | tracking_parameters_firefox: {
48 | src: ["build-firefox/**/*.js", "build-firefox/**/*.html"],
49 | overwrite: true,
50 | replacements: [{
51 | from: "utm_source=chrome_extension",
52 | to: "utm_source=firefox_addon"
53 | },
54 | {
55 | from: "utm_medium=chrome_extension",
56 | to: "utm_medium=firefox_addon"
57 | },
58 | {
59 | from: "from=chrome_extension",
60 | to: "from=firefox_addon"
61 | }]
62 | },
63 |
64 | website_welcome_url_firefox: {
65 | src: ["build-firefox/**/*.js", "build-firefox/**/*.html"],
66 | overwrite: true,
67 | replacements: [{
68 | from: "/chrome/welcome",
69 | to: "/firefox/welcome"
70 | }]
71 | },
72 |
73 | website_uninstall_url_firefox: {
74 | src: ["build-firefox/**/*.js", "build-firefox/**/*.html"],
75 | overwrite: true,
76 | replacements: [{
77 | from: "/chrome/uninstall",
78 | to: "/firefox/uninstall"
79 | }]
80 | },
81 |
82 | origin_header_firefox: {
83 | src: ["build-firefox/**/*.js"],
84 | overwrite: true,
85 | replacements: [{
86 | from: "\"Email-Hunter-Origin\": \"chrome_extension\"",
87 | to: "\"Email-Hunter-Origin\": \"firefox_addon\"",
88 | }]
89 | },
90 |
91 | store_link_firefox: {
92 | src: ["build-firefox/html/browser_popup.html"],
93 | overwrite: true,
94 | replacements: [{
95 | from: "https://chrome.google.com/webstore/detail/email-hunter/hgmhmanijnjhaffoampdlllchpolkdnj/reviews",
96 | to: "https://addons.mozilla.org/firefox/addon/hunterio/reviews/add"
97 | }]
98 | },
99 |
100 | fonts_path_firefox: {
101 | src: ["build-firefox/**/*.css"],
102 | overwrite: true,
103 | replacements: [{
104 | from: "chrome-extension://__MSG_@@extension_id__/",
105 | to: "../"
106 | }]
107 | },
108 |
109 | images_path_firefox: {
110 | src: ["build-firefox/**/*.js"],
111 | overwrite: true,
112 | replacements: [{
113 | from: "getURL(\"shared/img/",
114 | to: "getURL(\"img/"
115 | }]
116 | }
117 | },
118 |
119 | // Add the application ID in the manifest.json
120 | editmanifestforfirefox: {}
121 | }
122 |
--------------------------------------------------------------------------------
/src/browser_action/templates/search_results.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#if full_name}}
5 |
{{full_name}}
6 | {{/if}}
7 |
{{value}}
8 |
9 | {{#ifIsVerified verification.status}}
10 |
11 |
12 |
13 | {{confidence}}%
14 |
15 |
16 | {{else}}
17 | {{#ifIsAcceptAll}}
18 |
19 |
20 |
21 | {{confidence}}%
22 |
23 |
24 | {{else}}
25 |
26 |
27 |
28 |
29 | {{confidence}}%
30 |
31 |
32 |
33 |
36 | {{/ifIsAcceptAll}}
37 | {{/ifIsVerified}}
38 |
39 |
40 |
41 | {{#if phone_number}}
42 |
43 |
44 | {{phone_number}}
45 |
46 | {{/if}}
47 | {{#if position}}
48 |
49 |
50 | {{position}}
51 |
52 | {{else if department}}
53 |
54 |
55 | {{department}}
56 |
57 |
58 | {{/if}}
59 |
60 | {{#if linkedin}}
61 |
62 |
63 |
64 | {{/if}}
65 | {{#if twitter}}
66 |
67 |
68 |
69 | {{/if}}
70 |
71 |
72 |
73 | {{{lead_button}}}
74 |
75 |
79 |
80 |
81 | {{#if sources}}
82 |
83 |
84 | {{#each sources}}
85 | -
86 | {{#unless this.still_on_page}}
87 |
88 |
89 | Removed
90 |
91 |
92 | {{/unless}}
93 |
94 | {{this.uri}}
95 |
96 |
97 |
98 | {{/each}}
99 |
100 |
101 | {{/if}}
102 |
103 |
--------------------------------------------------------------------------------
/config/_chrome.js:
--------------------------------------------------------------------------------
1 | module.exports.tasks = {
2 | // Compile coffee
3 | coffee: {
4 | background_load: {
5 | options: { bare: true },
6 | src: "src/background.coffee",
7 | dest: "build-chrome/background.js"
8 | },
9 | background: {
10 | options: { bare: true, join: true },
11 | src: "src/background/*.coffee",
12 | dest: "build-chrome/js/background.js"
13 | },
14 | browser_action: {
15 | options: { bare: true },
16 | src: "src/browser_action/js/*.coffee",
17 | dest: "build-chrome/js/browser_action.js"
18 | },
19 | shared: {
20 | options: { bare: true, join: true },
21 | src: "src/shared/js/*.coffee",
22 | dest: "build-chrome/js/shared.js"
23 | },
24 | hunter_content_script: {
25 | options: { bare: true },
26 | src: "src/content_script/js/hunter-*.coffee",
27 | dest: "build-chrome/js/hunter_content_script.js"
28 | },
29 | websites_content_script: {
30 | options: { bare: true },
31 | src: "src/content_script/js/websites-*.coffee",
32 | dest: "build-chrome/js/websites_content_script.js"
33 | },
34 | source_popup: {
35 | src: "src/source_popup/js/source-popup.coffee",
36 | dest: "build-chrome/js/source_popup.js"
37 | }
38 | },
39 |
40 | // Process the Handlebars template
41 | handlebars: {
42 | compile: {
43 | options: {
44 | namespace: "JST"
45 | },
46 | files: {
47 | "build-chrome/js/templates.js": ["src/**/*.hbs"]
48 | }
49 | }
50 | },
51 |
52 | // SASS compilation
53 | compass: {
54 | browser_action: {
55 | options: {
56 | sourcemap: false,
57 | noLineComments: true,
58 | outputStyle: "compact",
59 | sassDir: "src/browser_action/css",
60 | cssDir: "build-chrome/css"
61 | }
62 | },
63 |
64 | websites_content_script: {
65 | options: {
66 | sourcemap: false,
67 | noLineComments: true,
68 | outputStyle: "compact",
69 | sassDir: "src/content_script/css",
70 | cssDir: "build-chrome/css"
71 | }
72 | },
73 |
74 | source_popup: {
75 | options: {
76 | sourcemap: false,
77 | noLineComments: true,
78 | outputStyle: "compact",
79 | sassDir: "src/source_popup/css",
80 | cssDir: "build-chrome/css"
81 | }
82 | }
83 | },
84 |
85 | // Copy the libraries already in CSS
86 | cssmin: {
87 | shared: {
88 | src: "src/shared/css/*.css",
89 | dest: "build-chrome/css/shared.min.css"
90 | }
91 | },
92 |
93 | // Copy other files (HTML, libraries, images and fonts)
94 | copy: {
95 | browser_action: {
96 | src: "src/browser_action/popup.html",
97 | dest: "build-chrome/html/browser_popup.html"
98 | },
99 | source_popup: {
100 | src: "src/source_popup/popup.html",
101 | dest: "build-chrome/html/source_popup.html"
102 | },
103 | images: {
104 | expand: true,
105 | cwd: "src/shared/img/",
106 | src: ["**"],
107 | dest: "build-chrome/img/"
108 | },
109 | fonts: {
110 | expand: true,
111 | cwd: "src/shared/fonts/",
112 | src: ["**"],
113 | dest: "build-chrome/fonts/",
114 | options: {
115 | processContentExclude: ["*.{ttf,woff,woff2}"]
116 | }
117 | },
118 | js_libs: {
119 | expand: true,
120 | cwd: "src/shared/js/lib",
121 | src: ["**"],
122 | dest: "build-chrome/js/lib"
123 | },
124 | css_libs: {
125 | expand: true,
126 | cwd: "src/shared/css/lib",
127 | src: ["**"],
128 | dest: "build-chrome/css/lib"
129 | },
130 | locale_files: {
131 | expand: true,
132 | cwd: "src/_locales",
133 | src: ["**"],
134 | dest: "build-chrome/_locales"
135 | },
136 | manifest: {
137 | src: "src/manifest.json",
138 | dest: "build-chrome/manifest.json"
139 | }
140 | },
141 |
142 | replace: {
143 | // This prevents an error to be displayed in Chrome console because the
144 | // source map doesn't exist. For the Firefox add-on, we don't do any
145 | // modification because Mozilla required to use unaltered library files.
146 | source_map_removal: {
147 | src: ["build-chrome/js/lib/purify.min.js"],
148 | overwrite: true,
149 | replacements: [{
150 | from: "//# sourceMappingURL=purify.min.js.map",
151 | to: ""
152 | }]
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/browser_action/templates/finder.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
{{first_name}} {{last_name}}
10 |
{{email}}
11 |
12 | {{#ifIsVerified verification.status}}
13 |
14 |
15 |
16 | {{score}}%
17 |
18 |
19 | {{else}}
20 | {{#ifIsAcceptAll}}
21 |
22 |
23 |
24 | {{score}}%
25 |
26 |
27 | {{else}}
28 |
29 |
30 |
31 |
32 | {{score}}%
33 |
34 |
35 |
36 | {{/ifIsAcceptAll}}
37 | {{/ifIsVerified}}
38 |
39 |
40 |
41 | {{#if phone_number}}
42 |
43 |
44 | {{phone_number}}
45 |
46 | {{/if}}
47 | {{#if position}}
48 |
49 |
50 | {{position}}
51 |
52 | {{else if department}}
53 |
54 |
55 | {{department}}
56 |
57 |
58 | {{/if}}
59 |
60 | {{#if linkedin}}
61 |
62 |
63 |
64 | {{/if}}
65 | {{#if twitter}}
66 |
67 |
68 |
69 | {{/if}}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | {{#if sources}}
78 |
79 |
{{{method}}}
80 |
81 | {{#each sources}}
82 | -
83 | {{#unless this.still_on_page}}
84 |
85 |
86 | Removed
87 |
88 |
89 | {{/unless}}
90 |
91 | {{this.uri}}
92 |
93 |
94 |
95 | {{/each}}
96 |
97 |
98 | {{/if}}
99 |
100 |
--------------------------------------------------------------------------------
/src/browser_action/js/email_finder.coffee:
--------------------------------------------------------------------------------
1 | EmailFinder = ->
2 | {
3 | full_name: @full_name
4 | domain: @domain
5 | email: @email
6 | score: @score
7 | sources: @sources
8 |
9 | validateForm: ->
10 | _this = @
11 |
12 | $("#email-finder-search").unbind().submit ->
13 | if (
14 | $("#full-name-field").val().indexOf(" ") == -1 ||
15 | $("#full-name-field").val().length <= 4
16 | )
17 | $("#full-name-field").tooltip(title: chrome.i18n.getMessage("enter_full_name_to_find_email")).tooltip("show")
18 | else
19 | _this.submit()
20 |
21 | false
22 |
23 | submit: ->
24 | $(".ds-finder-form__submit .far").removeClass("fa-search").addClass("fa-spinner-third fa-spin")
25 | @domain = window.domain
26 | @full_name = $("#full-name-field").val()
27 | @fetch()
28 |
29 | fetch: ->
30 | @cleanFinderResults()
31 |
32 | if typeof $("#full-name-field").data("bs.tooltip") != "undefined"
33 | $("#full-name-field").tooltip("destroy")
34 |
35 | _this = @
36 | $.ajax
37 | url: Api.emailFinder(_this.domain, _this.full_name, window.api_key)
38 | headers: "Email-Hunter-Origin": "chrome_extension"
39 | type: "GET"
40 | data: format: "json"
41 | dataType: "json"
42 | jsonp: false
43 | error: (xhr, statusText, err) ->
44 | $(".ds-finder-form__submit .far").removeClass("fa-spinner-third fa-spin").addClass("fa-search")
45 |
46 | if xhr.status == 400
47 | displayError chrome.i18n.getMessage("something_went_wrong_with_the_query")
48 | else if xhr.status == 401
49 | $(".connect-again-container").show()
50 | else if xhr.status == 403
51 | $("#domain-search").hide()
52 | $("#blocked-notification").show()
53 | else if xhr.status == 429
54 | unless _this.trial
55 | Account.returnRequestsError (e) ->
56 | displayError e
57 | else
58 | $(".connect-container").show()
59 | else
60 | displayError DOMPurify.sanitize(xhr.responseJSON["errors"][0]["details"])
61 |
62 | success: (result) ->
63 | if result.data.email == null
64 | $(".ds-finder-form__submit .far").removeClass("fa-spinner-third fa-spin").addClass("fa-search")
65 | $(".no-finder-result .person-name").text(_this.full_name)
66 | $(".no-finder-result").slideDown 200
67 | else
68 | _this.domain = result.data.domain
69 | _this.email = result.data.email
70 | _this.score = result.data.score
71 | _this.accept_all = result.data.accept_all
72 | _this.verification = result.data.verification
73 | _this.position = result.data.position
74 | _this.company = result.data.company
75 | _this.twitter = result.data.twitter
76 | _this.linkedin = result.data.linkedin
77 | _this.sources = result.data.sources
78 | _this.first_name = Utilities.toTitleCase(result.data.first_name)
79 | _this.last_name = Utilities.toTitleCase(result.data.last_name)
80 |
81 | _this.render()
82 |
83 | render: ->
84 | $(".ds-finder-form__submit .far").removeClass("fa-spinner-third fa-spin").addClass("fa-search")
85 |
86 | # Display: complete search link or Sign up CTA
87 | unless @trial
88 | $(".header-search-link").attr "href", "https://hunter.io/search/" + @domain + "?utm_source=chrome_extension&utm_medium=chrome_extension&utm_campaign=extension&utm_content=browser_popup"
89 | $(".header-search-link").show()
90 |
91 | # Confidence score color
92 | if @score < 30
93 | @confidence_score_class = "low-score"
94 | else if @score > 70
95 | @confidence_score_class = "high-score"
96 | else
97 | @confidence_score_class = "average-score"
98 |
99 | # Display: the method used
100 | if @sources.length > 0
101 | s = if @sources.length == 1 then "" else "s"
102 | @method = chrome.i18n.getMessage("we_found_this_email_on_the_web", [@sources.length, s])
103 | else
104 | @method = "This email address is our best guess for this person. We haven't found it on the web."
105 |
106 | # Prepare the template
107 | Handlebars.registerHelper "ifIsVerified", (verification_status, options) ->
108 | if verification_status == "valid"
109 | return options.fn(this)
110 | options.inverse this
111 |
112 | Handlebars.registerHelper "md5", (options) ->
113 | new Handlebars.SafeString(Utilities.MD5(options.fn(this)))
114 |
115 | template = JST["src/browser_action/templates/finder.hbs"]
116 | finder_html = $(Utilities.localizeHTML(template(@)))
117 |
118 | # Generate the DOM with the template and display it
119 | $("#email-finder").html finder_html
120 | $("#email-finder").slideDown 200
121 |
122 | # Display: the sources if any
123 | if @sources.length > 0
124 | $(".ds-result__sources").show()
125 |
126 | # Display: the tooltips
127 | $("[data-toggle='tooltip']").tooltip()
128 |
129 | # Event: the copy action
130 | Utilities.copyEmailListener()
131 |
132 | # Display: the button to save the lead
133 | lead_button = $(".ds-result--single .save-lead-button")
134 | lead_button.data
135 | first_name: @first_name
136 | last_name: @last_name
137 | email: @email
138 | confidence_score: @score
139 |
140 | lead = new LeadButton
141 | lead.saveButtonListener(lead_button)
142 | lead.disableSaveLeadButtonIfLeadExists(lead_button)
143 |
144 | cleanFinderResults: ->
145 | $("#email-finder").html ""
146 | $("#email-finder").slideUp 200
147 | $(".no-finder-result").slideUp 200
148 | }
149 |
--------------------------------------------------------------------------------
/src/browser_action/css/_variables.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | /**
3 | * COLORS
4 | */
5 | /* Brand Colors
6 | -------------------------------------------------------*/
7 | /* Primary - Orange */
8 | --colors-primary-100: #FEF0EC;
9 | --colors-primary-200: #FFDCD1;
10 | --colors-primary-300: #FFB59E;
11 | --colors-primary-400: #FC9272;
12 | --colors-primary-500: #FF784F;
13 | --colors-primary-600: #FA5320;
14 | --colors-primary-700: #D44519;
15 | --colors-primary-800: #903013;
16 | --colors-primary-900: #5D2514;
17 | --colors-primary-1000: #4B1A0B;
18 |
19 | /* Secondary - Blue */
20 | --colors-secondary-100: #F2F9FF;
21 | --colors-secondary-200: #E6F4FF;
22 | --colors-secondary-300: #C8E6FF;
23 | --colors-secondary-400: #9BD2FF;
24 | --colors-secondary-500: #62B3F6;
25 | --colors-secondary-600: #0F8DF4;
26 | --colors-secondary-700: #0578D6;
27 | --colors-secondary-800: #00569C;
28 | --colors-secondary-900: #003F73;
29 | --colors-secondary-1000: #002A4D;
30 |
31 | /* Neutral - Greys */
32 | --colors-grey-100: #F7F9FA;
33 | --colors-grey-200: #EEF0F3;
34 | --colors-grey-300: #DFE3E8;
35 | --colors-grey-400: #C7CDD4;
36 | --colors-grey-500: #A1A7AD;
37 | --colors-grey-600: #707880;
38 | --colors-grey-700: #4C555E;
39 | --colors-grey-800: #3A444F;
40 | --colors-grey-900: #28323B;
41 | --colors-grey-1000: #1D262E;
42 |
43 |
44 | /* Notifications Colors
45 | -------------------------------------------------------*/
46 | /* Success - Green */
47 | --colors-success-100: #F2FFF2;
48 | --colors-success-200: #E6FCE5;
49 | --colors-success-300: #C6F2C5;
50 | --colors-success-400: #94DE93;
51 | --colors-success-500: #57C555;
52 | --colors-success-600: #24A522;
53 | --colors-success-700: #078605;
54 | --colors-success-800: #056704;
55 | --colors-success-900: #035502;
56 | --colors-success-1000: #024201;
57 |
58 | /* Danger - Red */
59 | --colors-danger-100: #FFF6F6;
60 | --colors-danger-200: #FFE1E1;
61 | --colors-danger-300: #FFC8C7;
62 | --colors-danger-400: #FEA6A5;
63 | --colors-danger-500: #FB7C79;
64 | --colors-danger-600: #EE413F;
65 | --colors-danger-700: #D91F1C;
66 | --colors-danger-800: #B60A07;
67 | --colors-danger-900: #860604;
68 | --colors-danger-1000: #640E0C;
69 |
70 | /* Warning - Orangish */
71 | --colors-warning-100: #FFFCEB;
72 | --colors-warning-200: #FFF6C9;
73 | --colors-warning-300: #FFEFA1;
74 | --colors-warning-400: #FDE34E;
75 | --colors-warning-500: #F6D028;
76 | --colors-warning-600: #E0B115;
77 | --colors-warning-700: #B78B06;
78 | --colors-warning-800: #8E6506;
79 | --colors-warning-900: #694907;
80 | --colors-warning-1000: #4B3207;
81 |
82 | /* Support colors */
83 | --colors-magenta-200: #F1ABFC;
84 | --colors-magenta-500: #D946EF;
85 | --colors-magenta-700: #BC26D3;
86 |
87 | --colors-emerald-200: #A7F3D0;
88 | --colors-emerald-500: #10B981;
89 | --colors-emerald-700: #047857;
90 |
91 | --colors-cardinal-200: #FDA4AF;
92 | --colors-cardinal-500: #F43F5E;
93 | --colors-cardinal-700: #BE123C;
94 |
95 | --colors-cyan-200: #A5F3FC;
96 | --colors-cyan-500: #06B6D4;
97 | --colors-cyan-700: #0E7490;
98 |
99 |
100 | /* Other brands
101 | -------------------------------------------------------*/
102 | --colors-brand-twitter-blue: #1da1f2;
103 | --colors-brand-linkedin-blue: #0077b5;
104 | --colors-brand-facebook-blue: #3b5998;
105 | --colors-brand-youtube-red: #cd201f;
106 | --colors-brand-instagram-purple: #c13584;
107 | --colors-brand-android-green: #a4c639;
108 | --colors-brand-itunes-black: #050708;
109 | --colors-brand-github-black: #333;
110 |
111 | /* Integrations */
112 | --colors-brand-salesforce: #009edb;
113 | --colors-brand-salesforce--rgb: 0, 158, 219;
114 | --colors-brand-hubspot: #ff7a59;
115 | --colors-brand-hubspot--rgb: 255, 122, 89;
116 | --colors-brand-pipedrive: #26292c;
117 | --colors-brand-pipedrive--rgb: 38, 41, 44;
118 | --colors-brand-zoho: #ce2232;
119 | --colors-brand-zoho--rgb: 206, 34, 50;
120 | --colors-brand-zapier: #ff4a00;
121 | --colors-brand-zapier--rgb: 255, 74, 0;
122 | --colors-brand-outlook: #0078d4;
123 | --colors-brand-outlook--rgb: 0, 120, 212;
124 | --colors-brand-gmail: #4286f5;
125 | --colors-brand-gmail--rgb: 66, 134, 245;
126 |
127 | /**
128 | * SPACING
129 | */
130 | --spacing-base: 4px;
131 | --spacing-005: calc(var(--spacing-base) / 2); // 2px
132 | --spacing-01: calc(var(--spacing-base)); // 4px
133 | --spacing-02: calc(var(--spacing-base) * 2); // 8px
134 | --spacing-03: calc(var(--spacing-base) * 3); // 12px
135 | --spacing-04: calc(var(--spacing-base) * 4); // 16px
136 | --spacing-05: calc(var(--spacing-base) * 5); // 20px
137 | --spacing-06: calc(var(--spacing-base) * 6); // 24px
138 | --spacing-08: calc(var(--spacing-base) * 8); // 32px
139 | --spacing-10: calc(var(--spacing-base) * 10); // 40px
140 | --spacing-12: calc(var(--spacing-base) * 12); // 48px
141 | --spacing-14: calc(var(--spacing-base) * 14); // 56px
142 | --spacing-16: calc(var(--spacing-base) * 16); // 64px
143 | --spacing-20: calc(var(--spacing-base) * 20); // 80px
144 | --spacing-24: calc(var(--spacing-base) * 24); // 96px
145 | --spacing-30: calc(var(--spacing-base) * 30); // 120px
146 |
147 | /**
148 | * TYPOGRAPHY
149 | */
150 | --fonts-family-body: "Inter UI", "Inter", -apple-system,system-ui, "Segoe UI", Roboto, Noto, Oxygen-Sans, Ubuntu, Cantrell, "Helvetica Neue", sans-serif;
151 | --fonts-family-feature-settings: "cv01", "cv02", "cv03", "cv04", "cv11";
152 | }
153 |
--------------------------------------------------------------------------------
/src/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_name": {
3 | "message": "Hunter - Email Finder Extension"
4 | },
5 | "extension_description": {
6 | "message": "Find email addresses from anywhere on the web, with just one click."
7 | },
8 | "see_results": {
9 | "message": "See results on hunter.io"
10 | },
11 | "searches": {
12 | "message": "searches"
13 | },
14 | "upgrade": {
15 | "message": "Upgrade"
16 | },
17 | "sign_in": {
18 | "message": "Sign in"
19 | },
20 | "create_free_account": {
21 | "message": "Create a free account"
22 | },
23 | "are_you_statisfied": {
24 | "message": "Are you satisfied with Hunter extension?"
25 | },
26 | "yes": {
27 | "message": "Yes"
28 | },
29 | "no": {
30 | "message": "No"
31 | },
32 | "hunter_extension_is_free": {
33 | "message": "Hunter extension is free."
34 | },
35 | "please_let_review": {
36 | "message": "Please help us spread the word by letting a review!"
37 | },
38 | "add_a_review": {
39 | "message": "Add a review"
40 | },
41 | "we_are_sorry": {
42 | "message": "We are sorry to hear this."
43 | },
44 | "please_contact_us_for_feedback": {
45 | "message": "Please contact us at contact@hunter.io if you want to give us feedback or ask any question. We would be happy to help."
46 | },
47 | "you_reached_daily_limit": {
48 | "message": "You've reached your daily limit!"
49 | },
50 | "please_connect_account": {
51 | "message": "Please connect your Hunter account to use the extension. It's free."
52 | },
53 | "you_are_not_authenticated": {
54 | "message": "You are no longer authenticated"
55 | },
56 | "please_sign_in_to_continue": {
57 | "message": "Please sign in to continue"
58 | },
59 | "not_enough_data": {
60 | "message": "Not enough data"
61 | },
62 | "not_enough_data_for": {
63 | "message": "There is not enough data available online for"
64 | },
65 | "find_out_more": {
66 | "message": "Find out more information"
67 | },
68 | "webmail_domain": {
69 | "message": "Webmail domain"
70 | },
71 | "domain_used_for_personal_emails": {
72 | "message": "This domain is used to create personal email addresses. These email addresses are not returned in Hunter which is designed for professional use only."
73 | },
74 | "email_pattern": {
75 | "message": "Email pattern"
76 | },
77 | "type": {
78 | "message": "Type"
79 | },
80 | "personal": {
81 | "message": "Personal"
82 | },
83 | "generic": {
84 | "message": "Generic"
85 | },
86 | "apply_filters": {
87 | "message": "Apply filters"
88 | },
89 | "departments": {
90 | "message": "Departments"
91 | },
92 | "department_executive": {
93 | "message": "Executive"
94 | },
95 | "department_it": {
96 | "message": "IT / Engineering"
97 | },
98 | "department_finance": {
99 | "message": "Finance"
100 | },
101 | "department_management": {
102 | "message": "Management"
103 | },
104 | "department_sales": {
105 | "message": "Sales"
106 | },
107 | "department_legal": {
108 | "message": "Legal"
109 | },
110 | "department_support": {
111 | "message": "Support"
112 | },
113 | "department_hr": {
114 | "message": "Human Resources"
115 | },
116 | "department_marketing": {
117 | "message": "Marketing"
118 | },
119 | "department_communication": {
120 | "message": "Writing & Communication"
121 | },
122 | "department_education": {
123 | "message": "Education"
124 | },
125 | "department_design": {
126 | "message": "Arts & Design"
127 | },
128 | "department_health": {
129 | "message": "Medical & Health"
130 | },
131 | "department_operations": {
132 | "message": "Operations & Logistics"
133 | },
134 | "clear_filters": {
135 | "message": "Clear filters"
136 | },
137 | "find_by_name": {
138 | "message": "Find by name"
139 | },
140 | "no_results_found": {
141 | "message": "No results found"
142 | },
143 | "we_could_not_find_with_filters": {
144 | "message": "We couldn't find any results matching your criterias. Update or clear your filters."
145 | },
146 | "no_results_for": {
147 | "message": "No results for"
148 | },
149 | "could_not_find_email_for_person": {
150 | "message": "We couldn't find the email address for this person."
151 | },
152 | "save_leads_in": {
153 | "message": "Save leads in"
154 | },
155 | "go_to_my_leads": {
156 | "message": "Go to my leads"
157 | },
158 | "not_available_on_linkedin": {
159 | "message": "Hunter is no longer available on LinkedIn"
160 | },
161 | "find_emails_with_email_finder": {
162 | "message": "To find the email address of people you have identified, please use the Email Finder."
163 | },
164 | "email_finder": {
165 | "message": "Email Finder"
166 | },
167 | "find_emails_from_websites": {
168 | "message": "Find email addresses from websites"
169 | },
170 | "open_hunter_on_websites": {
171 | "message": "Open Hunter when you visit websites to find email addresses."
172 | },
173 | "dashboard": {
174 | "message": "Dashboard"
175 | },
176 | "connection_refused": {
177 | "message": "The connection to Hunter has been refused"
178 | },
179 | "visit_website_for_information": {
180 | "message": "Please visit the website for more information."
181 | },
182 | "go_to_hunter": {
183 | "message": "Go to hunter.io"
184 | },
185 | "something_went_wrong_with_the_query": {
186 | "message": "Sorry, something went wrong with the query."
187 | },
188 | "results_for_domain": {
189 | "message": "$COUNT$ result$S$ for $DOMAIN$",
190 | "placeholders": {
191 | "count": {
192 | "content": "$1",
193 | "example": "1,120"
194 | },
195 | "s": {
196 | "content": "$2",
197 | "example": "s"
198 | },
199 | "domain": {
200 | "content": "$3",
201 | "example": "hunter.io"
202 | }
203 | }
204 | },
205 | "see_all_the_results": {
206 | "message": "See all the results ($COUNT$ more)",
207 | "placeholders": {
208 | "count": {
209 | "content": "$1",
210 | "example": "1,120"
211 | }
212 | }
213 | },
214 | "save": {
215 | "message": "Save"
216 | },
217 | "sign_up_to_uncover_more_emails": {
218 | "message": "Please sign up to uncover the email addresses"
219 | },
220 | "verifying": {
221 | "message": "Verifying"
222 | },
223 | "retry": {
224 | "message": "Retry"
225 | },
226 | "email_verification_takes_longer": {
227 | "message": "The email verification is taking longer than expected. Please try again later."
228 | },
229 | "valid": {
230 | "message": "Valid"
231 | },
232 | "invalid": {
233 | "message": "Invalid"
234 | },
235 | "accept_all": {
236 | "message": "Accept all"
237 | },
238 | "unknown": {
239 | "message": "Unknown"
240 | },
241 | "first_name": {
242 | "message": "First name"
243 | },
244 | "last_name": {
245 | "message": "Last name"
246 | },
247 | "first_name_initial": {
248 | "message": "First name initial"
249 | },
250 | "last_name_initial": {
251 | "message": "Last name initial"
252 | },
253 | "enter_full_name_to_find_email": {
254 | "message": "Please enter the full name of the person to find the email address."
255 | },
256 | "we_found_this_email_on_the_web": {
257 | "message": "We found this email address $COUNT$ time$S$ on the web.",
258 | "placeholders": {
259 | "count": {
260 | "content": "$1",
261 | "example": "12"
262 | },
263 | "s": {
264 | "content": "$2",
265 | "example": "s"
266 | }
267 | }
268 | },
269 | "loading": {
270 | "message": "Loading"
271 | },
272 | "saved": {
273 | "message": "Saved"
274 | },
275 | "failed": {
276 | "message": "Failed"
277 | },
278 | "create_a_new_list": {
279 | "message": "Create a new list"
280 | },
281 | "click_to_copy": {
282 | "message": "Click to copy"
283 | },
284 | "save_as_lead": {
285 | "message": "Save as lead"
286 | },
287 | "january": {
288 | "message": "January"
289 | },
290 | "february": {
291 | "message": "February"
292 | },
293 | "march": {
294 | "message": "March"
295 | },
296 | "april": {
297 | "message": "April"
298 | },
299 | "june": {
300 | "message": "June"
301 | },
302 | "july": {
303 | "message": "July"
304 | },
305 | "august": {
306 | "message": "August"
307 | },
308 | "september": {
309 | "message": "September"
310 | },
311 | "october": {
312 | "message": "October"
313 | },
314 | "november": {
315 | "message": "November"
316 | },
317 | "december": {
318 | "message": "December"
319 | },
320 | "jan": {
321 | "message": "Jan"
322 | },
323 | "feb": {
324 | "message": "Feb"
325 | },
326 | "mar": {
327 | "message": "Mar"
328 | },
329 | "apr": {
330 | "message": "Apr"
331 | },
332 | "may": {
333 | "message": "May"
334 | },
335 | "jun": {
336 | "message": "Jun"
337 | },
338 | "jul": {
339 | "message": "Jul"
340 | },
341 | "aug": {
342 | "message": "Aug"
343 | },
344 | "sep": {
345 | "message": "Sep"
346 | },
347 | "oct": {
348 | "message": "Oct"
349 | },
350 | "nov": {
351 | "message": "Nov"
352 | },
353 | "dec": {
354 | "message": "Dec"
355 | },
356 | "verify_email": {
357 | "message": "Verify email"
358 | },
359 | "removed": {
360 | "message": "Removed"
361 | },
362 | "something_went_wrong_on_our_side": {
363 | "message": "Something went wrong on our side. Please try again later."
364 | },
365 | "please_complete_your_registration": {
366 | "message": "Please complete your registration on the website to use the extension."
367 | },
368 | "your_account_has_been_restricted": {
369 | "message": "Your account has been restricted. Please log in for more information."
370 | },
371 | "you_have_reached_your_daily_quota": {
372 | "message": "You have reached your free monthly quota. Please upgrade to a premium plan to do more searches."
373 | },
374 | "you_have_reached_your_temporary_quota": {
375 | "message": "You have reached your temporary quota. You will get your full access as soon as we validate your account."
376 | },
377 | "your_have_reached_your_monthly_quota": {
378 | "message": "You have reached your monthly quota. Please upgrade to do more searches."
379 | },
380 | "your_have_reached_your_monthly_enterprise_quota": {
381 | "message": "You have reached your monthly quota. Please contact your account manager to update your plan."
382 | }
383 | }
384 |
--------------------------------------------------------------------------------
/src/_locales/fr/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extension_name": {
3 | "message": "Hunter - Extension Email Finder"
4 | },
5 | "extension_description": {
6 | "message": "Trouvez des adresses email n'importe où sur le web, en un seul clic."
7 | },
8 | "see_results": {
9 | "message": "Voir les résultats sur hunter.io"
10 | },
11 | "searches": {
12 | "message": "recherches"
13 | },
14 | "upgrade": {
15 | "message": "Mettre à niveau"
16 | },
17 | "sign_in": {
18 | "message": "Se connecter"
19 | },
20 | "create_free_account": {
21 | "message": "Créer un compte gratuit"
22 | },
23 | "are_you_statisfied": {
24 | "message": "Êtes-vous satisfait de l'extension Hunter?"
25 | },
26 | "yes": {
27 | "message": "Oui"
28 | },
29 | "no": {
30 | "message": "Non"
31 | },
32 | "hunter_extension_is_free": {
33 | "message": "L'extension Hunter est gratuite."
34 | },
35 | "please_let_review": {
36 | "message": "Aidez-nous à faire connaître notre produit en laissant un commentaire !"
37 | },
38 | "add_a_review": {
39 | "message": "Laisser un avis"
40 | },
41 | "we_are_sorry": {
42 | "message": "Nous sommes désolés d'apprendre cela."
43 | },
44 | "please_contact_us_for_feedback": {
45 | "message": "Veuillez nous contacter à l'adresse contact@hunter.io si vous souhaitez nous donner votre avis ou poser une question. Nous serions ravis de vous aider."
46 | },
47 | "you_reached_daily_limit": {
48 | "message": "Vous avez atteint votre limite quotidienne !"
49 | },
50 | "please_connect_account": {
51 | "message": "Veuillez connecter votre compte Hunter pour utiliser l'extension. C'est gratuit."
52 | },
53 | "you_are_not_authenticated": {
54 | "message": "Vous n'êtes plus authentifié"
55 | },
56 | "please_sign_in_to_continue": {
57 | "message": "Veuillez vous connecter pour continuer"
58 | },
59 | "not_enough_data": {
60 | "message": "Pas assez de données"
61 | },
62 | "not_enough_data_for": {
63 | "message": "Il n'y a pas suffisamment de données disponibles en ligne pour"
64 | },
65 | "find_out_more": {
66 | "message": "Trouver plus d'informations"
67 | },
68 | "webmail_domain": {
69 | "message": "Domaine de messagerie Web"
70 | },
71 | "domain_used_for_personal_emails": {
72 | "message": "Ce domaine est utilisé pour créer des adresses email personnelles. Ces adresses email ne sont pas retournées dans Hunter, qui est conçu uniquement pour un usage professionnel."
73 | },
74 | "email_pattern": {
75 | "message": "Modèle d'adresse email"
76 | },
77 | "type": {
78 | "message": "Type"
79 | },
80 | "personal": {
81 | "message": "Personnel"
82 | },
83 | "generic": {
84 | "message": "Générique"
85 | },
86 | "apply_filters": {
87 | "message": "Filtrer"
88 | },
89 | "departments": {
90 | "message": "Départements"
91 | },
92 | "department_executive": {
93 | "message": "Direction"
94 | },
95 | "department_it": {
96 | "message": "IT / Ingénierie"
97 | },
98 | "department_finance": {
99 | "message": "Finance"
100 | },
101 | "department_management": {
102 | "message": "Gestion"
103 | },
104 | "department_sales": {
105 | "message": "Ventes"
106 | },
107 | "department_legal": {
108 | "message": "Juridique"
109 | },
110 | "department_support": {
111 | "message": "Assistance"
112 | },
113 | "department_hr": {
114 | "message": "Ressources humaines"
115 | },
116 | "department_marketing": {
117 | "message": "Marketing"
118 | },
119 | "department_communication": {
120 | "message": "Rédaction et communication"
121 | },
122 | "department_education": {
123 | "message": "Éducation"
124 | },
125 | "department_design": {
126 | "message": "Arts et design"
127 | },
128 | "department_health": {
129 | "message": "Médical et santé"
130 | },
131 | "department_operations": {
132 | "message": "Opérations et logistique"
133 | },
134 | "clear_filters": {
135 | "message": "Effacer les filtres"
136 | },
137 | "find_by_name": {
138 | "message": "Chercher par nom"
139 | },
140 | "no_results_found": {
141 | "message": "Aucun résultat trouvé"
142 | },
143 | "we_could_not_find_with_filters": {
144 | "message": "Nous n'avons trouvé aucun résultat correspondant à vos critères de recherche. Mettez à jour ou effacez vos filtres."
145 | },
146 | "no_results_for": {
147 | "message": "Aucun résultat pour"
148 | },
149 | "could_not_find_email_for_person": {
150 | "message": "Nous n'avons pas pu trouver l'adresse email pour cette personne."
151 | },
152 | "save_leads_in": {
153 | "message": "Enregistrer les prospects dans"
154 | },
155 | "go_to_my_leads": {
156 | "message": "Ouvrir la liste"
157 | },
158 | "not_available_on_linkedin": {
159 | "message": "Hunter n'est plus disponible sur LinkedIn"
160 | },
161 | "find_emails_with_email_finder": {
162 | "message": "Pour trouver l'adresse email des personnes que vous avez identifiées, utilisez le Email Finder."
163 | },
164 | "email_finder": {
165 | "message": "Recherche d'adresse email"
166 | },
167 | "find_emails_from_websites": {
168 | "message": "Trouver des adresses email à partir de sites web"
169 | },
170 | "open_hunter_on_websites": {
171 | "message": "Ouvrez Hunter lorsque vous visitez des sites web pour trouver des adresses email."
172 | },
173 | "dashboard": {
174 | "message": "Tableau de bord"
175 | },
176 | "connection_refused": {
177 | "message": "La connexion à Hunter a été refusée"
178 | },
179 | "visit_website_for_information": {
180 | "message": "Veuillez consulter le site web pour plus d'informations."
181 | },
182 | "go_to_hunter": {
183 | "message": "Aller sur hunter.io"
184 | },
185 | "something_went_wrong_with_the_query": {
186 | "message": "Désolé, quelque chose s'est mal passé avec la requête."
187 | },
188 | "results_for_domain": {
189 | "message": "$COUNT$ résultat$S$ pour $DOMAIN$",
190 | "placeholders": {
191 | "count": {
192 | "content": "$1",
193 | "example": "1 120"
194 | },
195 | "s": {
196 | "content": "$2",
197 | "example": "s"
198 | },
199 | "domain": {
200 | "content": "$3",
201 | "example": "hunter.io"
202 | }
203 | }
204 | },
205 | "see_all_the_results": {
206 | "message": "Voir tous les résultats ($COUNT$ de plus)",
207 | "placeholders": {
208 | "count": {
209 | "content": "$1",
210 | "example": "1 120"
211 | }
212 | }
213 | },
214 | "save": {
215 | "message": "Enregistrer"
216 | },
217 | "sign_up_to_uncover_more_emails": {
218 | "message": "Veuillez vous inscrire pour découvrir les adresses email"
219 | },
220 | "verifying": {
221 | "message": "Vérification en cours"
222 | },
223 | "retry": {
224 | "message": "Réessayer"
225 | },
226 | "email_verification_takes_longer": {
227 | "message": "La vérification de l'adresse email prend plus de temps que prévu. Veuillez réessayer plus tard."
228 | },
229 | "valid": {
230 | "message": "Valide"
231 | },
232 | "invalid": {
233 | "message": "Invalide"
234 | },
235 | "accept_all": {
236 | "message": "Tout accepter"
237 | },
238 | "unknown": {
239 | "message": "Inconnu"
240 | },
241 | "first_name": {
242 | "message": "Prénom"
243 | },
244 | "last_name": {
245 | "message": "Nom de famille"
246 | },
247 | "first_name_initial": {
248 | "message": "Initiale du prénom"
249 | },
250 | "last_name_initial": {
251 | "message": "Initiale du nom de famille"
252 | },
253 | "enter_full_name_to_find_email": {
254 | "message": "Veuillez saisir le nom complet de la personne pour trouver l'adresse email."
255 | },
256 | "we_found_this_email_on_the_web": {
257 | "message": "Nous avons trouvé cette adresse email $COUNT$ fois sur le web.",
258 | "placeholders": {
259 | "count": {
260 | "content": "$1",
261 | "example": "12"
262 | },
263 | "s": {
264 | "content": "$2",
265 | "example": "s"
266 | }
267 | }
268 | },
269 | "loading": {
270 | "message": "Chargement"
271 | },
272 | "saved": {
273 | "message": "Enregistré"
274 | },
275 | "failed": {
276 | "message": "Échec"
277 | },
278 | "create_a_new_list": {
279 | "message": "Créer une nouvelle liste"
280 | },
281 | "click_to_copy": {
282 | "message": "Cliquez pour copier"
283 | },
284 | "save_as_lead": {
285 | "message": "Enregistrer"
286 | },
287 | "january": {
288 | "message": "Janvier"
289 | },
290 | "february": {
291 | "message": "Février"
292 | },
293 | "march": {
294 | "message": "Mars"
295 | },
296 | "april": {
297 | "message": "Avril"
298 | },
299 | "may": {
300 | "message": "Mai"
301 | },
302 | "june": {
303 | "message": "Juin"
304 | },
305 | "july": {
306 | "message": "Juillet"
307 | },
308 | "august": {
309 | "message": "Août"
310 | },
311 | "september": {
312 | "message": "Septembre"
313 | },
314 | "october": {
315 | "message": "Octobre"
316 | },
317 | "november": {
318 | "message": "Novembre"
319 | },
320 | "december": {
321 | "message": "Décembre"
322 | },
323 | "jan": {
324 | "message": "Jan"
325 | },
326 | "feb": {
327 | "message": "Fév"
328 | },
329 | "mar": {
330 | "message": "Mar"
331 | },
332 | "apr": {
333 | "message": "Avr"
334 | },
335 | "jun": {
336 | "message": "Juin"
337 | },
338 | "jul": {
339 | "message": "Juil"
340 | },
341 | "aug": {
342 | "message": "Août"
343 | },
344 | "sep": {
345 | "message": "Sep"
346 | },
347 | "oct": {
348 | "message": "Oct"
349 | },
350 | "nov": {
351 | "message": "Nov"
352 | },
353 | "dec": {
354 | "message": "Déc"
355 | },
356 | "verify_email": {
357 | "message": "Vérifier l'email"
358 | },
359 | "removed": {
360 | "message": "Supprimé"
361 | },
362 | "something_went_wrong_on_our_side": {
363 | "message": "Un problème est survenu de notre côté. Veuillez réessayer plus tard."
364 | },
365 | "please_complete_your_registration": {
366 | "message": "Veuillez compléter votre inscription sur le site web pour utiliser l'extension."
367 | },
368 | "your_account_has_been_restricted": {
369 | "message": "Votre compte a été restreint. Veuillez vous connecter pour plus d'informations."
370 | },
371 | "you_have_reached_your_daily_quota": {
372 | "message": "Vous avez atteint votre quota mensuel gratuit. Veuillez passer à un abonnement premium pour effectuer plus de recherches."
373 | },
374 | "you_have_reached_your_temporary_quota": {
375 | "message": "Vous avez atteint votre quota temporaire. Vous aurez un accès complet dès que nous aurons validé votre compte."
376 | },
377 | "your_have_reached_your_monthly_quota": {
378 | "message": "Vous avez atteint votre quota mensuel. Veuillez passer à un abonnement supérieur pour effectuer plus de recherches."
379 | },
380 | "your_have_reached_your_monthly_enterprise_quota": {
381 | "message": "Vous avez atteint votre quota mensuel. Veuillez contacter votre responsable de compte pour mettre à jour votre plan."
382 | }
383 | }
384 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2018 Hunter Web Services, Inc.
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/shared/js/utilities.coffee:
--------------------------------------------------------------------------------
1 | Utilities =
2 | # Return a decomposition of all successive subdomains in an array
3 | # In some rare case, this function might remove the main domain name and let only the
4 | # TLD. To mitigate these cases, we check if we can find emails by adding back one level
5 | # in the function "findRelevantDomain".
6 | #
7 | decomposeSubDomains: (domain) ->
8 | domains = [domain]
9 |
10 | loop
11 | newDomain = domain.substring(domain.indexOf('.') + 1)
12 | subdomainsCount = (newDomain.match(/\./g) or []).length
13 |
14 | if (subdomainsCount == 0 || newDomain.length <= 7) then break
15 | domain = newDomain
16 | domains.push newDomain
17 |
18 | return domains
19 |
20 | # This function decides if it's relevant to remove the highest found subdomain or not.
21 | # If we find data by keeping the subdomain, this is the domain we will use.
22 | #
23 | findRelevantDomain: (domain, fn) ->
24 | # In any case, we won't try emails on domain starting with "www."
25 | domain = domain.replace(/^www\./i, "")
26 |
27 | domains = @decomposeSubDomains(domain)
28 |
29 | return fn(domain) if domains.length == 1
30 |
31 | withSubdomain = domains[domains.length - 2]
32 | withoutSubdomain = domains.pop()
33 |
34 | @dataFoundForDomain withSubdomain, (results) ->
35 | if results
36 | return fn(withSubdomain)
37 | else
38 | return fn(withoutSubdomain)
39 |
40 | # Check if we have data for the given domain name, by using a dedicated API endpoint.
41 | #
42 | dataFoundForDomain: (domain, fn) ->
43 | fetch(Api.dataForDomain(domain)).then((response) ->
44 | response.json()
45 | ).then((response) ->
46 | if response == 1
47 | fn(true)
48 | else
49 | fn(false)
50 | ).catch (error) ->
51 | console.warn error
52 | fn(false)
53 |
54 | # Localize a given HTML string.
55 | #
56 | localizeHTML: (html) ->
57 | html = $("#{html}
")
58 |
59 | html.find("[data-locale]").each () ->
60 | $(this).text(chrome.i18n.getMessage($(this).data("locale")))
61 |
62 | html.find("[data-locale-title]").each () ->
63 | $(this).prop("title", chrome.i18n.getMessage($(this).data("localeTitle")))
64 |
65 | return html.html()
66 |
67 | # Add commas separating thousands
68 | #
69 | numberWithCommas: (x) ->
70 | x.toString().replace /\B(?=(\d{3})+(?!\d))/g, ','
71 |
72 | # Display dates easy to read
73 | #
74 | dateInWords: (input) ->
75 | date = undefined
76 | monthNames = undefined
77 | splitted_date = undefined
78 | splitted_date = input.split('-')
79 | date = new Date(splitted_date[0], splitted_date[1] - 1, splitted_date[2])
80 | if $(window).width() > 768
81 | monthNames = [
82 | chrome.i18n.getMessage("january")
83 | chrome.i18n.getMessage("february")
84 | chrome.i18n.getMessage("march")
85 | chrome.i18n.getMessage("april")
86 | chrome.i18n.getMessage("may")
87 | chrome.i18n.getMessage("june")
88 | chrome.i18n.getMessage("july")
89 | chrome.i18n.getMessage("august")
90 | chrome.i18n.getMessage("september")
91 | chrome.i18n.getMessage("october")
92 | chrome.i18n.getMessage("november")
93 | chrome.i18n.getMessage("december")
94 | ]
95 | else
96 | monthNames = [
97 | chrome.i18n.getMessage("jan")
98 | chrome.i18n.getMessage("feb")
99 | chrome.i18n.getMessage("mar")
100 | chrome.i18n.getMessage("apr")
101 | chrome.i18n.getMessage("may")
102 | chrome.i18n.getMessage("jun")
103 | chrome.i18n.getMessage("jul")
104 | chrome.i18n.getMessage("aug")
105 | chrome.i18n.getMessage("sep")
106 | chrome.i18n.getMessage("oct")
107 | chrome.i18n.getMessage("nov")
108 | chrome.i18n.getMessage("dec")
109 | ]
110 | monthNames[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear()
111 |
112 | capitalizeFirstLetter: (string) ->
113 | return string.charAt(0).toUpperCase() + string.slice(1)
114 |
115 | # Open in a new tab
116 | #
117 | openInNewTab: (url) ->
118 | win = window.open(url, '_blank')
119 | win.focus()
120 | return
121 |
122 | executeCopy : (text) ->
123 | input = document.createElement('textarea')
124 | $('#copy-area').prepend input
125 | input.value = text
126 | input.focus()
127 | input.select()
128 | document.execCommand 'Copy'
129 | input.remove()
130 | return
131 |
132 | # This method attaches a tooltip to the passed DOM element and safely
133 | # destroys it. If the passed element already has a visible tooltip
134 | # attached, we do nothing.
135 | #
136 | showDismissableTooltip: (selector, title, duration) ->
137 | # prevents building and finally destroying a new tooltip
138 | # whereas one is already attached to the selector
139 | return if selector.next("div.tooltip:visible").length
140 |
141 | selector.tooltip(title: title).tooltip("show")
142 |
143 | setTimeout (->
144 | selector.tooltip("destroy")
145 | ), duration
146 |
147 |
148 | # Capitalizes the first letter of each word in the string and lower cases
149 | # the other letters
150 | #
151 | toTitleCase: (string) ->
152 | (string.split(" ").map (word) -> word[0].toUpperCase() + word[1..-1].toLowerCase()).join " "
153 |
154 |
155 | # Copy the email in an .email tag
156 | #
157 | copyEmailListener: ->
158 | $(".copy-email").on "click", ->
159 | email = $(this).data("email")
160 | Utilities.executeCopy(email)
161 | $(this).next().find(".tooltip-inner").text("Copied!")
162 |
163 | # Sort an object by values
164 | #
165 | sortObject: (array) ->
166 | sortable = []
167 | for el of array
168 | sortable.push [
169 | el
170 | array[el]
171 | ]
172 | sortable.sort (a, b) ->
173 | b[1] - a[1]
174 |
175 | return sortable
176 |
177 | # Converts to MD5
178 | #
179 | MD5: (s) ->
180 | C = Array()
181 | P = undefined
182 | h = undefined
183 | E = undefined
184 | v = undefined
185 | g = undefined
186 | Y = undefined
187 | X = undefined
188 | W = undefined
189 | V = undefined
190 | S = 7
191 | Q = 12
192 | N = 17
193 | M = 22
194 | A = 5
195 | z = 9
196 | y = 14
197 | w = 20
198 | o = 4
199 | m = 11
200 | l = 16
201 | j = 23
202 | U = 6
203 | T = 10
204 | R = 15
205 | O = 21
206 |
207 | L = (k, d) ->
208 | k << d | k >>> 32 - d
209 |
210 | K = (G, k) ->
211 | I = undefined
212 | d = undefined
213 | F = undefined
214 | H = undefined
215 | x = undefined
216 | F = G & 2147483648
217 | H = k & 2147483648
218 | I = G & 1073741824
219 | d = k & 1073741824
220 | x = (G & 1073741823) + (k & 1073741823)
221 | if I & d
222 | return x ^ 2147483648 ^ F ^ H
223 | if I | d
224 | if x & 1073741824
225 | x ^ 3221225472 ^ F ^ H
226 | else
227 | x ^ 1073741824 ^ F ^ H
228 | else
229 | x ^ F ^ H
230 |
231 | r = (d, F, k) ->
232 | d & F | ~d & k
233 |
234 | q = (d, F, k) ->
235 | d & k | F & ~k
236 |
237 | p = (d, F, k) ->
238 | d ^ F ^ k
239 |
240 | n = (d, F, k) ->
241 | F ^ (d | ~k)
242 |
243 | u = (G, F, aa, Z, k, H, I) ->
244 | G = K(G, K(K(r(F, aa, Z), k), I))
245 | K L(G, H), F
246 |
247 | f = (G, F, aa, Z, k, H, I) ->
248 | G = K(G, K(K(q(F, aa, Z), k), I))
249 | K L(G, H), F
250 |
251 | D = (G, F, aa, Z, k, H, I) ->
252 | G = K(G, K(K(p(F, aa, Z), k), I))
253 | K L(G, H), F
254 |
255 | t = (G, F, aa, Z, k, H, I) ->
256 | G = K(G, K(K(n(F, aa, Z), k), I))
257 | K L(G, H), F
258 |
259 | e = (G) ->
260 | Z = undefined
261 | F = G.length
262 | x = F + 8
263 | k = (x - (x % 64)) / 64
264 | I = (k + 1) * 16
265 | aa = Array(I - 1)
266 | d = 0
267 | H = 0
268 | while H < F
269 | Z = (H - (H % 4)) / 4
270 | d = H % 4 * 8
271 | aa[Z] = aa[Z] | G.charCodeAt(H) << d
272 | H++
273 | Z = (H - (H % 4)) / 4
274 | d = H % 4 * 8
275 | aa[Z] = aa[Z] | 128 << d
276 | aa[I - 2] = F << 3
277 | aa[I - 1] = F >>> 29
278 | aa
279 |
280 | B = (x) ->
281 | k = ''
282 | F = ''
283 | G = undefined
284 | d = undefined
285 | d = 0
286 | while d <= 3
287 | G = x >>> d * 8 & 255
288 | F = '0' + G.toString(16)
289 | k = k + F.substr(F.length - 2, 2)
290 | d++
291 | k
292 |
293 | J = (k) ->
294 | k = k.replace(/rn/g, 'n')
295 | d = ''
296 | F = 0
297 | while F < k.length
298 | x = k.charCodeAt(F)
299 | if x < 128
300 | d += String.fromCharCode(x)
301 | else
302 | if x > 127 and x < 2048
303 | d += String.fromCharCode(x >> 6 | 192)
304 | d += String.fromCharCode(x & 63 | 128)
305 | else
306 | d += String.fromCharCode(x >> 12 | 224)
307 | d += String.fromCharCode(x >> 6 & 63 | 128)
308 | d += String.fromCharCode(x & 63 | 128)
309 | F++
310 | d
311 |
312 | s = J(s)
313 | C = e(s)
314 | Y = 1732584193
315 | X = 4023233417
316 | W = 2562383102
317 | V = 271733878
318 | P = 0
319 | while P < C.length
320 | h = Y
321 | E = X
322 | v = W
323 | g = V
324 | Y = u(Y, X, W, V, C[P + 0], S, 3614090360)
325 | V = u(V, Y, X, W, C[P + 1], Q, 3905402710)
326 | W = u(W, V, Y, X, C[P + 2], N, 606105819)
327 | X = u(X, W, V, Y, C[P + 3], M, 3250441966)
328 | Y = u(Y, X, W, V, C[P + 4], S, 4118548399)
329 | V = u(V, Y, X, W, C[P + 5], Q, 1200080426)
330 | W = u(W, V, Y, X, C[P + 6], N, 2821735955)
331 | X = u(X, W, V, Y, C[P + 7], M, 4249261313)
332 | Y = u(Y, X, W, V, C[P + 8], S, 1770035416)
333 | V = u(V, Y, X, W, C[P + 9], Q, 2336552879)
334 | W = u(W, V, Y, X, C[P + 10], N, 4294925233)
335 | X = u(X, W, V, Y, C[P + 11], M, 2304563134)
336 | Y = u(Y, X, W, V, C[P + 12], S, 1804603682)
337 | V = u(V, Y, X, W, C[P + 13], Q, 4254626195)
338 | W = u(W, V, Y, X, C[P + 14], N, 2792965006)
339 | X = u(X, W, V, Y, C[P + 15], M, 1236535329)
340 | Y = f(Y, X, W, V, C[P + 1], A, 4129170786)
341 | V = f(V, Y, X, W, C[P + 6], z, 3225465664)
342 | W = f(W, V, Y, X, C[P + 11], y, 643717713)
343 | X = f(X, W, V, Y, C[P + 0], w, 3921069994)
344 | Y = f(Y, X, W, V, C[P + 5], A, 3593408605)
345 | V = f(V, Y, X, W, C[P + 10], z, 38016083)
346 | W = f(W, V, Y, X, C[P + 15], y, 3634488961)
347 | X = f(X, W, V, Y, C[P + 4], w, 3889429448)
348 | Y = f(Y, X, W, V, C[P + 9], A, 568446438)
349 | V = f(V, Y, X, W, C[P + 14], z, 3275163606)
350 | W = f(W, V, Y, X, C[P + 3], y, 4107603335)
351 | X = f(X, W, V, Y, C[P + 8], w, 1163531501)
352 | Y = f(Y, X, W, V, C[P + 13], A, 2850285829)
353 | V = f(V, Y, X, W, C[P + 2], z, 4243563512)
354 | W = f(W, V, Y, X, C[P + 7], y, 1735328473)
355 | X = f(X, W, V, Y, C[P + 12], w, 2368359562)
356 | Y = D(Y, X, W, V, C[P + 5], o, 4294588738)
357 | V = D(V, Y, X, W, C[P + 8], m, 2272392833)
358 | W = D(W, V, Y, X, C[P + 11], l, 1839030562)
359 | X = D(X, W, V, Y, C[P + 14], j, 4259657740)
360 | Y = D(Y, X, W, V, C[P + 1], o, 2763975236)
361 | V = D(V, Y, X, W, C[P + 4], m, 1272893353)
362 | W = D(W, V, Y, X, C[P + 7], l, 4139469664)
363 | X = D(X, W, V, Y, C[P + 10], j, 3200236656)
364 | Y = D(Y, X, W, V, C[P + 13], o, 681279174)
365 | V = D(V, Y, X, W, C[P + 0], m, 3936430074)
366 | W = D(W, V, Y, X, C[P + 3], l, 3572445317)
367 | X = D(X, W, V, Y, C[P + 6], j, 76029189)
368 | Y = D(Y, X, W, V, C[P + 9], o, 3654602809)
369 | V = D(V, Y, X, W, C[P + 12], m, 3873151461)
370 | W = D(W, V, Y, X, C[P + 15], l, 530742520)
371 | X = D(X, W, V, Y, C[P + 2], j, 3299628645)
372 | Y = t(Y, X, W, V, C[P + 0], U, 4096336452)
373 | V = t(V, Y, X, W, C[P + 7], T, 1126891415)
374 | W = t(W, V, Y, X, C[P + 14], R, 2878612391)
375 | X = t(X, W, V, Y, C[P + 5], O, 4237533241)
376 | Y = t(Y, X, W, V, C[P + 12], U, 1700485571)
377 | V = t(V, Y, X, W, C[P + 3], T, 2399980690)
378 | W = t(W, V, Y, X, C[P + 10], R, 4293915773)
379 | X = t(X, W, V, Y, C[P + 1], O, 2240044497)
380 | Y = t(Y, X, W, V, C[P + 8], U, 1873313359)
381 | V = t(V, Y, X, W, C[P + 15], T, 4264355552)
382 | W = t(W, V, Y, X, C[P + 6], R, 2734768916)
383 | X = t(X, W, V, Y, C[P + 13], O, 1309151649)
384 | Y = t(Y, X, W, V, C[P + 4], U, 4149444226)
385 | V = t(V, Y, X, W, C[P + 11], T, 3174756917)
386 | W = t(W, V, Y, X, C[P + 2], R, 718787259)
387 | X = t(X, W, V, Y, C[P + 9], O, 3951481745)
388 | Y = K(Y, h)
389 | X = K(X, E)
390 | W = K(W, v)
391 | V = K(V, g)
392 | P += 16
393 | i = B(Y) + B(X) + B(W) + B(V)
394 | i.toLowerCase()
395 |
396 | # Generate a hash from a string
397 | #
398 | String::hashCode = ->
399 | hash = 0
400 | i = undefined
401 | chr = undefined
402 | len = undefined
403 | if @length == 0
404 | return hash
405 | i = 0
406 | len = @length
407 | while i < len
408 | chr = @charCodeAt(i)
409 | hash = (hash << 5) - hash + chr
410 | hash |= 0
411 | i++
412 | hash
413 |
--------------------------------------------------------------------------------
/src/browser_action/css/_base.scss:
--------------------------------------------------------------------------------
1 | // Popup
2 | //
3 | ::selection {
4 | background: var(--colors-grey-800);
5 | color: #fff;
6 | }
7 |
8 | html {
9 | width: 620px;
10 | min-height: 520px;
11 | position: relative;
12 | }
13 |
14 | body {
15 | width: 620px;
16 | font-display: swap;
17 | font-family: var(--fonts-family-body) !important;
18 | font-feature-settings: var(--fonts-family-feature-settings);
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | font-size: 1.3rem;
22 | color: var(--colors-grey-800);
23 | margin-top: 6rem; // header height
24 |
25 | &::-webkit-scrollbar {
26 | display: none;
27 | }
28 | }
29 |
30 | #domain-search {
31 | display: none;
32 | }
33 |
34 | a:focus, a:active {
35 | outline: none;
36 | }
37 |
38 | strong {
39 | font-weight: 600;
40 | }
41 |
42 | .fa-spin {
43 | -webkit-animation-duration: 400ms !important;
44 | animation-duration: 400ms !important;
45 | }
46 |
47 | abbr {
48 | text-decoration: none;
49 | }
50 |
51 | // Confidence score
52 | //
53 | .score {
54 | display: inline-block;
55 | width: 8px;
56 | height: 8px;
57 | border-radius: 50%;
58 | border: 2px solid transparent;
59 |
60 | &.low-score {
61 | border-color: var(--colors-danger-600);
62 | }
63 |
64 | &.average-score {
65 | background: linear-gradient(0deg, var(--colors-warning-500), var(--colors-warning-500) 50%, transparent 0, transparent);
66 | border-color: var(--colors-warning-500);
67 | }
68 |
69 | &.high-score {
70 | background-color: var(--colors-success-600);
71 | }
72 | }
73 |
74 |
75 | // Buttons
76 | // prefixed with "h-" to avoid Bootstrap collison
77 | //
78 | .h-button {
79 | // Initial
80 | --h-button-padding: var(--spacing-03) var(--spacing-04);
81 | --h-button-gap: var(--spacing-02);
82 | --h-button-border-color: var(--colors-grey-300);
83 | --h-button-border-radius: 4px;
84 | --h-button-box-shadow: 0px 1px 1px rgba(29, 38, 46, 0.05);
85 | --h-button-font-size: 1.3rem;
86 | --h-button-line-height: calc(16/13);
87 | --h-button-text-shadow: none;
88 | --h-button-color: var(--colors-grey-800);
89 | --h-button-background: #fff;
90 | --h-button-height: 4rem;
91 | --h-button-width: max-content;
92 |
93 | // Hover
94 | --h-button-color-hover: var(--colors-grey-800);
95 | --h-button-background-hover: var(--colors-grey-100);
96 | --h-button-border-color-hover: var(--colors-grey-300);
97 | --h-button-box-shadow-hover: 0px 1px 4px 2px rgba(29, 38, 46, 0.04), 0px 1px 1px rgba(29, 38, 46, 0.05);
98 |
99 | // Focus-visible
100 | --h-button-box-shadow-focus: 0px 0px 0px 2px var(--colors-secondary-600), 0px 1px 1px rgba(29, 38, 46, 0.05);
101 |
102 | // Active
103 | --h-button-color-active: var(--colors-grey-800);
104 | --h-button-background-active: var(--colors-grey-100);
105 | --h-button-box-shadow-active: none;
106 |
107 | display: inline-flex;
108 | align-items: center;
109 | gap: var(--h-button-gap);
110 | padding: var(--h-button-padding);
111 | height: var(--h-button-height);
112 | width: var(--h-button-width);
113 | border: 1px solid var(--h-button-border-color);
114 | border-radius: var(--h-button-border-radius);
115 | box-shadow: var(--h-button-box-shadow);
116 | font-size: var(--h-button-font-size);
117 | font-weight: 500;
118 | line-height: var(--h-button-line-height);
119 | text-decoration: none;
120 | text-shadow: var(--h-button-text-shadow);
121 | background: var(--h-button-background);
122 | color: var(--h-button-color);
123 | transition: background-color 250ms ease, color 250ms ease-in, border-color 250ms ease-in;
124 |
125 | &:hover,
126 | &:focus {
127 | color: var(--h-button-color-hover);
128 | background-color: var(--h-button-background-hover);
129 | border-color: var(--h-button-border-color-hover);
130 | box-shadow: var(--h-button-box-shadow-hover);
131 | text-decoration: none;
132 | outline: none;
133 | }
134 |
135 | &:focus-visible {
136 | box-shadow: var(--h-button-box-shadow-focus);
137 | }
138 |
139 | &:active,
140 | &.active,
141 | &[aria-expanded="true"] {
142 | color: var(--h-button-color-active);
143 | background-color: var(--h-button-background-active);
144 | box-shadow: var(--h-button-box-shadow-active);
145 | }
146 |
147 | &:active {
148 | transform: translateY(0.5px);
149 | }
150 |
151 | &:disabled,
152 | &.disabled {
153 | opacity: .5;
154 | user-select: none;
155 | pointer-events: none;
156 | }
157 | }
158 |
159 | .h-button--primary {
160 | // Default
161 | --h-button-border-color: transparent;
162 | --h-button-box-shadow: none;
163 | --h-button-text-shadow: 0px 1px 0px rgba(212, 69, 25, 0.5);
164 | --h-button-color: #fff;
165 | --h-button-background: var(--colors-primary-600);
166 |
167 | // Hover
168 | --h-button-color-hover: #fff;
169 | --h-button-background-hover: var(--colors-primary-700);
170 | --h-button-border-color-hover: var(--colors-primary-700);
171 | --h-button-box-shadow-hover: none;
172 |
173 | // Focus-visible
174 | --h-button-box-shadow-focus: 0px 0px 0px 1px #FFFFFF, 0px 0px 0px 3px var(--colors-primary-700);
175 |
176 | // Active
177 | --h-button-color-active: #fff;
178 | --h-button-background-active: var(--colors-primary-700);
179 | --h-button-box-shadow-active: none;
180 |
181 | // Specifities
182 | border-bottom-color: var(--colors-primary-700);
183 | }
184 |
185 | .h-button--ghost {
186 | // Default
187 | --h-button-border-color: transparent;
188 | --h-button-box-shadow: none;
189 | --h-button-background: transparent;
190 |
191 | // Hover
192 | --h-button-background-hover: var(--colors-grey-100);
193 | --h-button-border-color-hover: transparent;
194 | --h-button-box-shadow-hover: none;
195 |
196 | // Focus-visible
197 | --h-button-box-shadow-focus: 0px 0px 0px 1px var(--colors-grey-100), 0px 0px 0px 3px var(--colors-grey-300);
198 |
199 | // Active
200 | --h-button-background-active: var(--colors-grey-100);
201 | --h-button-box-shadow-active: none;
202 | }
203 |
204 | // Sizes modifier
205 | //
206 | .h-button--sm {
207 | --h-button-padding: var(--spacing-02);
208 | --h-button-font-size: 1.2rem;
209 | --h-button-line-height: calc(16/12);
210 | --h-button-height: 3.2rem;
211 | }
212 |
213 | .h-button--xs {
214 | --h-button-padding: var(--spacing-01) var(--spacing-02);
215 | --h-button-gap: var(--spacing-01);
216 | --h-button-font-size: 1.1rem;
217 | --h-button-line-height: calc(16/11);
218 | --h-button-height: 2.4rem;
219 | }
220 |
221 |
222 | // Tags
223 | //
224 | .tag {
225 | --c-tag-background: var(--colors-grey-200);
226 | --c-tag-background-hover: var(--colors-grey-300);
227 | --c-tag-color: var(--colors-grey-700);
228 | --c-tag-icon-color: var(--colors-grey-600);
229 | --c-tag-padding: var(--spacing-01) var(--spacing-02);
230 |
231 | display: inline-flex;
232 | align-items: center;
233 | padding: var(--c-tag-padding);
234 | overflow: hidden;
235 | border: 0;
236 | border-radius: 2px;
237 | background-color: var(--c-tag-background);
238 | color: var(--c-tag-color);
239 | font-size: 1.2rem;
240 | font-weight: 600;
241 | line-height: 1.33333; // 16px
242 |
243 | .score {
244 | margin-right: var(--spacing-01);
245 | }
246 | }
247 |
248 | .tag__icon {
249 | color: var(--c-tag-icon-color);
250 | margin-right: var(--spacing-01);
251 | }
252 |
253 | .tag__label {
254 | display: flex;
255 | align-items: center;
256 | white-space: nowrap;
257 | overflow: hidden;
258 | color: var(--c-tag-color);
259 | text-decoration: none;
260 | text-overflow: ellipsis;
261 | }
262 |
263 | /* Size */
264 | .tag--sm {
265 | --c-tag-padding: var(--spacing-005) var(--spacing-02);
266 |
267 | font-size: 1.1rem;
268 | font-weight: 500;
269 | line-height: calc(16/11);
270 | }
271 |
272 | /* Colors */
273 | .tag--success {
274 | --c-tag-background: var(--colors-success-200);
275 | --c-tag-background-hover: var(--colors-success-300);
276 | --c-tag-color: var(--colors-success-700);
277 | --c-tag-icon-color: var(--colors-success-600);
278 | }
279 |
280 | .tag--warning {
281 | --c-tag-background: var(--colors-warning-200);
282 | --c-tag-background-hover: var(--colors-warning-300);
283 | --c-tag-color: var(--colors-warning-800);
284 | --c-tag-icon-color: var(--colors-warning-600);
285 | }
286 |
287 | .tag--danger {
288 | --c-tag-background: var(--colors-danger-200);
289 | --c-tag-background-hover: var(--colors-danger-300);
290 | --c-tag-color: var(--colors-danger-700);
291 | --c-tag-icon-color: var(--colors-danger-600);
292 | }
293 |
294 | .tag--info {
295 | --c-tag-background: var(--colors-secondary-200);
296 | --c-tag-background-hover: var(--colors-secondary-300);
297 | --c-tag-color: var(--colors-secondary-700);
298 | --c-tag-icon-color: var(--colors-secondary-600);
299 | }
300 |
301 |
302 | // Select
303 | //
304 | .h-select {
305 | // Initial
306 | --h-select-padding: calc(var(--spacing-02) - 1px) var(--spacing-05) calc(var(--spacing-02) - 1px) var(--spacing-03);
307 | --h-select-border-color: var(--colors-grey-300);
308 | --h-select-border-radius: 4px;
309 | --h-select-font-size: 1.3rem;
310 | --h-select-line-height: calc(16/13);
311 | --h-select-color: var(--colors-grey-900);
312 | --h-select-background: #fff;
313 | --h-select-height: 4rem;
314 |
315 | // Hover
316 | --h-select-border-color-hover: var(--colors-grey-400);
317 |
318 | // Focus
319 | --h-select-border-color-focus: var(--colors-secondary-600);
320 | --h-select-box-shadow-focus: 0px 0px 0px 2px var(--colors-secondary-300);
321 |
322 | // Error
323 | --h-select-border-color-error: var(--colors-danger-700);
324 |
325 | appearance: none;
326 | display: inline-block;
327 | padding: var(--h-select-padding);
328 | height: var(--h-select-height);
329 | border: 1px solid var(--h-select-border-color);
330 | border-radius: var(--h-select-border-radius);
331 | font-size: var(--h-select-font-size);
332 | font-weight: normal;
333 | line-height: var(--h-select-line-height);
334 | text-decoration: none;
335 | background-color: var(--h-select-background);
336 | background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSI1IiB2aWV3Qm94PSIwIDAgOCA1IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNNy44MzAwNyAwLjk0NTEyM0w0LjM3NTI4IDQuMTk3NjJDNC4yNzA3MyA0LjMxNTgxIDQuMTM0MzYgNC4zNjU4MSA0LjAwMDI2IDQuMzY1ODFDMy44NjYxNSA0LjM2NTgxIDMuNzMwMjQgNC4zMTYwOCAzLjYyNTIzIDQuMjE2NjdMMC4xNzA0NDMgMC45NDUxMjNDLTAuMDQ4NDM1NCAwLjczNjAxNyAtMC4wNTc1MjY5IDAuMzkwNTM5IDAuMTQ5MDc4IDAuMTcyMzQyQzAuMzU2NTkzIC0wLjA0ODEyODEgMC43MDMyMDggLTAuMDU0OTQ2NyAwLjkyMDQ5NiAwLjE1MTg4Nkw0LjAwMDI2IDMuMDY4TDcuMDgwMDEgMC4xNDk2MTNDNy4yOTczNSAtMC4wNTcwODMzIDcuNjQyNTUgLTAuMDQ5MjY0NiA3Ljg1MTQzIDAuMTcwOTIxQzguMDU3MzYgMC4zOTA1MzkgOC4wNDgyNiAwLjczNjAxNyA3LjgzMDA3IDAuOTQ1MTIzWiIgZmlsbD0iIzcwNzg4MCIvPgo8L3N2Zz4K");
337 | background-repeat: no-repeat;
338 | background-position: right 10px top calc(50% + 1px);
339 | color: var(--h-select-color);
340 | transition: background-color 250ms ease, color 250ms ease-in, border-color 250ms ease-in;
341 |
342 | &:hover {
343 | border-color: var(--h-select-border-color-hover) !important;
344 | }
345 |
346 | &:focus {
347 | outline: none;
348 | border-color: var(--h-select-border-color-focus) !important;
349 | box-shadow: var(--h-select-box-shadow-focus) !important;
350 | }
351 |
352 | &.error {
353 | border-color: var(--h-select-border-color-error) !important;
354 | }
355 |
356 | &:disabled,
357 | &.disabled {
358 | opacity: .5;
359 | user-select: none;
360 | pointer-events: none;
361 | }
362 | }
363 |
364 | // Hide default browser arrow on IE
365 | .h-select::-ms-expand {
366 | display: none;
367 | }
368 |
369 | // Sizes modifier
370 | //
371 | .h-select--sm {
372 | --h-select-padding: calc(var(--spacing-02) - 1px) var(--spacing-05) calc(var(--spacing-02) - 1px) var(--spacing-02);
373 | --h-select-font-size: 1.2rem;
374 | --h-select-line-height: calc(16/12);
375 | --h-select-height: 3.2rem;
376 | }
377 |
378 |
379 | // Alerts
380 | //
381 | .h-alert {
382 | --h-alert-border: var(--colors-grey-400);
383 | --h-alert-background: var(--colors-grey-100);
384 | --h-alert-title-color: var(--colors-grey-800);
385 | --h-alert-icon-color: var(--colors-grey-600);
386 |
387 | position: relative;
388 | display: flex;
389 | align-items: top;
390 | justify-content: space-between;
391 | width: 100%;
392 | padding: var(--spacing-04);
393 | background-color: var(--h-alert-background);
394 | border: 1px solid var(--h-alert-border);
395 | border-radius: 4px;
396 | font-size: 1.4rem;
397 | }
398 |
399 | .h-alert__icon-wrapper {
400 | padding-right: var(--spacing-03);
401 | }
402 |
403 | .h-alert__icon {
404 | font-size: 1.6rem;
405 | line-height: 1.25;
406 | color: var(--h-alert-icon-color);
407 | }
408 |
409 | .h-alert__content {
410 | flex: 1;
411 | }
412 |
413 | .h-alert__description {
414 | margin-top: var(--spacing-005);
415 | font-size: 1.3rem;
416 | line-height: 1.5385;
417 | color: var(--colors-grey-800);
418 |
419 | a {
420 | color: currentColor;
421 | text-decoration-line: underline;
422 | text-decoration-color: var(--colors-grey-500);
423 | text-underline-offset: var(--spacing-01);
424 | cursor: pointer;
425 |
426 | &:hover,
427 | &:focus {
428 | text-decoration-color: currentColor;
429 | }
430 | }
431 |
432 | ul,
433 | ol,
434 | p {
435 | &:last-child {
436 | margin-bottom: 0;
437 | }
438 | }
439 |
440 | ul,
441 | ol {
442 | padding-left: 1em;
443 | }
444 | }
445 |
446 | .h-alert--warning {
447 | --h-alert-border: var(--colors-warning-400);
448 | --h-alert-background: var(--colors-warning-100);
449 | --h-alert-icon-color: var(--colors-warning-600);
450 | --h-alert-title-color: var(--colors-warning-800);
451 | }
452 |
453 | // Empty States
454 | //
455 | .empty-state {
456 | padding: var(--spacing-14) var(--spacing-10);
457 | display: flex;
458 | align-items: center;
459 | justify-content: center;
460 | flex-direction: column;
461 | gap: var(--spacing-08);
462 |
463 | .empty-state__title {
464 | font-weight: 500;
465 | font-size: 1.6rem;
466 | line-height: 1.25;
467 | color: var(--colors-grey-900);
468 | margin-top: 0 !important;
469 | margin-bottom: var(--spacing-02) !important;
470 | }
471 | }
472 |
473 | .empty-state__img {
474 | max-width: 20rem;
475 | height: auto;
476 | }
477 |
478 | .empty-state__icon {
479 | font-size: 12rem;
480 | line-height: 1;
481 | color: var(--colors-grey-300);
482 | }
483 |
484 | .empty-state__content {
485 | font-size: 1.3rem;
486 | text-align: center;
487 | color: var(--colors-grey-700);
488 | line-height: 1.5;
489 | }
490 |
491 | .empty-state__actions {
492 | display: flex;
493 | align-items: center;
494 | justify-content: center;
495 | gap: var(--spacing-02);
496 | margin-top: var(--spacing-04);
497 |
498 | a:not([class*=btn]):not([class*=h-button]) {
499 | font-size: 1.2rem;
500 | color: var(--colors-grey-700);
501 | text-decoration-line: underline;
502 | text-decoration-color: var(--colors-grey-300);
503 | text-underline-offset: var(--spacing-01);
504 |
505 | &:hover,
506 | &:focus {
507 | text-decoration-color: var(--colors-grey-400);
508 | }
509 | }
510 | }
511 |
512 | .empty-state--horizontal {
513 | flex-direction: row;
514 |
515 | .empty-state__content {
516 | text-align: left;
517 |
518 | p,
519 | ul,
520 | ol {
521 | max-width: 36rem;
522 | }
523 | }
524 |
525 | .empty-state__actions {
526 | justify-content: flex-start;
527 | }
528 | }
529 |
530 | // Avatar
531 | //
532 | .h-avatar {
533 | --h-avatar-size: 2.4rem;
534 | --h-avatar-background: var(--colors-grey-200);
535 | --h-avatar-color: var(--colors-grey-600);
536 | --h-avatar-radius: 50%;
537 |
538 | // Hover
539 | --h-avatar-background-hover: var(--colors-secondary-600);
540 | --h-avatar-color-hover: #fff;
541 |
542 | display: inline-flex;
543 | align-items: center;
544 | justify-content: center;
545 | width: var(--h-avatar-size);
546 | aspect-ratio: 1/1;
547 | border-radius: var(--h-avatar-radius);
548 | background-color: var(--h-avatar-background);
549 | color: var(--h-avatar-color);
550 | font-size: .8rem;
551 | font-weight: 500;
552 | overflow: hidden;
553 | transition: background-color 150ms ease-in, color 150ms ease-in;
554 |
555 | img {
556 | object-fit: cover;
557 | transition: opacity 150ms ease-in;
558 | }
559 | }
560 |
561 | // Sizes
562 | .h-avatar--auto {
563 | --h-avatar-size: auto;
564 | }
565 |
566 | .h-avatar--xs {
567 | --h-avatar-size: 1.6rem;
568 | }
569 |
570 | .h-avatar--sm {
571 | --h-avatar-size: 2rem;
572 | }
573 |
--------------------------------------------------------------------------------
/src/browser_action/css/_search.scss:
--------------------------------------------------------------------------------
1 | // Popup header
2 | //
3 | .header {
4 | position: fixed;
5 | top: 0;
6 | left: 0;
7 | right: 0;
8 | display: flex;
9 | align-items: center;
10 | padding: var(--spacing-02) var(--spacing-06);
11 | background-color: var(--colors-grey-900);
12 | color: #fff;
13 | min-height: 5.2rem;
14 | z-index: 1001;
15 | }
16 |
17 | .header__logo {
18 | fill: #fff;
19 | }
20 |
21 | .header__metas {
22 | margin-left: auto;
23 | display: flex;
24 | align-items: center;
25 | gap: var(--spacing-02);
26 | font-size: 1.2rem;
27 | font-weight: 500;
28 | line-height: calc(16/12);
29 | }
30 |
31 | .header__all-results {
32 | display: none;
33 | transition: color 200ms ease-in;
34 |
35 | span {
36 | text-decoration: underline;
37 | color: #fff;
38 | }
39 |
40 | &:hover,
41 | &:focus {
42 | text-decoration: none;
43 |
44 | span {
45 | text-decoration: none;
46 | color: var(--colors-grey-200);
47 | }
48 | }
49 |
50 | &::after {
51 | content: "•";
52 | display: inline-block;
53 | color: var(--colors-grey-400);
54 | margin-left: calc(var(--spacing-02) - .25em);
55 | font-weight: 500;
56 | text-decoration: none;
57 | }
58 | }
59 |
60 | // Account
61 | .account-not-logged,
62 | .account-logged,
63 | .account-upgrade-cta {
64 | display: none;
65 | }
66 |
67 | .account-avatar {
68 | margin-left: var(--spacing-01);
69 | text-decoration: none;
70 | }
71 |
72 | .account-avatar__img {
73 | width: 2rem;
74 | height: 2rem;
75 | }
76 |
77 | .account-not-logged {
78 | .h-button:not(:first-child) {
79 | margin-left: var(--spacing-01);
80 | }
81 | }
82 |
83 | .account-upgrade-cta {
84 | margin: 0 var(--spacing-01) 0 var(--spacing-02);
85 | }
86 |
87 |
88 | // Loading placeholder
89 | //
90 | @keyframes placeHolderShimmer{
91 | 0%{
92 | background-position: -700px 0
93 | }
94 | 100%{
95 | background-position: 700px 0
96 | }
97 | }
98 |
99 | #loading-placeholder {
100 | padding: 1px 24px 24px;
101 |
102 | .background-masker {
103 | animation-duration: 2s;
104 | animation-fill-mode: forwards;
105 | animation-iteration-count: infinite;
106 | animation-name: placeHolderShimmer;
107 | animation-timing-function: linear;
108 | background: #f6f6f6;
109 | background: linear-gradient(to right, #f3f3f3 8%, #eee 18%, #f3f3f3 33%);
110 | background-size: 700px 104px;
111 | position: relative;
112 | }
113 |
114 | .people-search-block {
115 | margin-top: 32px;
116 | height: 56px;
117 | }
118 |
119 | .result-name-block {
120 | margin-top: 24px;
121 | height: 16px;
122 | width: 200px;
123 | }
124 |
125 | .result-email-block {
126 | margin-top: 8px;
127 | height: 16px;
128 | width: 300px;
129 | }
130 | }
131 |
132 | // No results
133 | //
134 | .ds-no-results {
135 | .leads-manager,
136 | .results-header,
137 | .ds-results,
138 | .filters {
139 | display: none;
140 | }
141 | }
142 |
143 |
144 | // Leads list (footer)
145 | //
146 | .leads-manager {
147 | display: flex;
148 | align-items: center;
149 | gap: var(--spacing-04);
150 | justify-content: space-between;
151 | position: fixed;
152 | bottom: 0;
153 | left: 0;
154 | width: 100%;
155 | padding: var(--spacing-04) var(--spacing-06);
156 | background: #FFFFFF;
157 | border-top: 1px solid var(--colors-grey-300);
158 | box-shadow: 0px -6px 12px rgba(0, 0, 0, 0.04);
159 | z-index: 1001;
160 | }
161 |
162 | .list-select-container {
163 | display: flex;
164 | align-items: center;
165 | gap: var(--spacing-02);
166 |
167 | label {
168 | font-weight: 500;
169 | font-size: 1.3rem;
170 | color: var(--colors-grey-800);
171 | margin: 0;
172 | }
173 | }
174 |
175 | .leads-manager__list {
176 | width: 24rem;
177 | }
178 |
179 | .leads-manager__link {
180 | font-size: 1.2rem;
181 | text-align: right;
182 | text-decoration-line: underline;
183 | color: var(--colors-grey-700);
184 | text-decoration-color: var(--colors-grey-400);
185 | transition: text-decoration-color 100ms ease-in;
186 |
187 | &:hover,
188 | &:focus {
189 | color: var(--colors-grey-800);
190 | text-decoration-color: transparent;
191 | }
192 | }
193 |
194 | // Error notifications or empty states
195 | //
196 | .error {
197 | display: none;
198 | margin: 0px 20px 90px 20px;
199 | }
200 |
201 | #linkedin-notification,
202 | #empty-notification,
203 | #blocked-notification {
204 | display: none;
205 | padding: var(--spacing-16) var(--spacing-10);
206 | }
207 |
208 | // Results header
209 | //
210 | .results-header {
211 | display: flex;
212 | align-items: center;
213 | justify-content: space-between;
214 | padding: var(--spacing-04) var(--spacing-06);
215 | }
216 |
217 | .results-header__count {
218 | margin: 0;
219 | font-size: 1.4rem;
220 | font-weight: 400;
221 | line-height: calc(24/14);
222 | color: var(--colors-grey-900);
223 | }
224 |
225 | .results-header__pattern {
226 | display: none;
227 | font-size: 1.3rem;
228 | line-height: calc(24/13);
229 | color: var(--colors-grey-700);
230 | }
231 |
232 | // Filters
233 | //
234 | .filters {
235 | display: flex;
236 | align-items: center;
237 | gap: var(--spacing-02);
238 | padding: var(--spacing-04) var(--spacing-06);
239 | background: var(--colors-grey-100);
240 | border-top: 1px solid var(--colors-grey-300);
241 | border-bottom: 1px solid var(--colors-grey-300);
242 |
243 | [data-selected-filters] {
244 | .fa-angle-down {
245 | order: 10;
246 | }
247 |
248 | &::after {
249 | content: attr(data-selected-filters);
250 | width: var(--spacing-04);
251 | height: var(--spacing-04);
252 | background-color: var(--colors-grey-700);
253 | color: #fff;
254 | border-radius: 50%;
255 | font-size: 1rem;
256 | line-height: calc(16/10);
257 | }
258 | }
259 | }
260 |
261 | .filters-dropdown {
262 | padding: 0;
263 | background: #fff;
264 | box-shadow: 0px 4px 12px rgba(29, 38, 46, 0.12), 0px 12px 24px rgba(29, 38, 46, 0.08);
265 | border: 1px solid var(--colors-grey-300);
266 | border-radius: 4px;
267 | overflow: hidden;
268 | min-width: 19rem;
269 |
270 | &:lang(fr) {
271 | min-width: 21rem;
272 | }
273 | }
274 |
275 | .filters-dropdown__submit {
276 | display: block;
277 | width: 100%;
278 | padding: var(--spacing-02);
279 | border: 0;
280 | border-top: 1px solid var(--colors-grey-300);
281 | background: #fff;
282 | text-align: center;
283 | font-size: 1.3rem;
284 | font-weight: 600;
285 | line-height: calc(20/13);
286 | color: var(--colors-secondary-700);
287 | transition: background-color 150ms ease-in;
288 |
289 | &:hover,
290 | &:focus {
291 | background-color: var(--colors-grey-100);
292 | }
293 | }
294 |
295 | .filters-choice {
296 | list-style-type: none;
297 | padding: var(--spacing-04);
298 | margin: 0;
299 | max-height: 24rem;
300 | overflow: auto;
301 | }
302 |
303 | .filters-choice__item {
304 | &:not(:first-child) {
305 | margin-top: var(--spacing-02);
306 | }
307 | }
308 |
309 | .filters-choice__checkbox,
310 | .filters-choice__radio {
311 | display: flex;
312 | align-items: center;
313 | gap: var(--spacing-02);
314 |
315 | input {
316 | margin: 0;
317 | }
318 |
319 | label {
320 | margin: 0;
321 | font-size: 1.2rem;
322 | line-height: calc(16/12);
323 | font-weight: 500;
324 | color: var(--colors-grey-800);
325 | width: max-content;
326 | }
327 | }
328 |
329 | .filters__by-name {
330 | margin-left: auto;
331 |
332 | &[aria-expanded="true"] {
333 | .fa-angle-down {
334 | transform: rotate(180deg);
335 | }
336 | }
337 | }
338 |
339 | .filters__clear {
340 | display: none;
341 | }
342 |
343 |
344 | // Email Finder
345 | //
346 | // Form
347 | .find-by-name {
348 | display: none;
349 | }
350 |
351 | .ds-finder {
352 | display: flex;
353 | align-items: center;
354 | justify-content: space-between;
355 | gap: var(--spacing-02);
356 | padding: var(--spacing-04);
357 | background-color: var(--colors-grey-100);
358 | border-bottom: 1px solid var(--colors-grey-300);
359 | }
360 |
361 | .ds-finder-form {
362 | flex: 1;
363 | display: flex;
364 | min-width: 0;
365 | border: 1px solid var(--colors-grey-300);
366 | border-radius: 4px;
367 | background-color: #fff;
368 | transition: border-color 150ms ease-in, box-shadow 150ms ease-in;
369 |
370 | &:focus-within {
371 | border-color: var(--colors-secondary-600);
372 | box-shadow: 0px 0px 0px 2px var(--colors-secondary-300);
373 | }
374 | }
375 |
376 | .ds-finder-form-name {
377 | flex: 1;
378 | display: flex;
379 | min-width: 0;
380 | align-items: center;
381 | gap: var(--spacing-02);
382 | padding: var(--spacing-01) var(--spacing-03);
383 |
384 | .ds-finder-form-name__field {
385 | padding: var(--spacing-01) 0;
386 | border: 0;
387 | height: auto;
388 | min-width: 0;
389 | border: 0;
390 | background-color: #fff;
391 |
392 | &:hover,
393 | &:focus {
394 | border: 0 !important;
395 | outline: 0;
396 | }
397 | }
398 | }
399 |
400 | .ds-finder-form-name__icon {
401 | color: var(--colors-grey-500);
402 | }
403 |
404 | .ds-finder-form__at {
405 | padding: var(--spacing-03);
406 | border-left: 1px solid var(--colors-grey-300);
407 | border-right: 1px solid var(--colors-grey-300);
408 | color: var(--colors-grey-500);
409 | font-size: 1.4rem;
410 | line-height: calc(16/14);
411 | }
412 |
413 | .ds-finder-form-company {
414 | flex: 1;
415 | display: flex;
416 | align-items: center;
417 | gap: var(--spacing-02);
418 | padding: var(--spacing-01) var(--spacing-03);
419 | max-width: 28rem;
420 | }
421 |
422 | .ds-finder-form-company__logo {
423 | height: 1.6rem;
424 | width: auto;
425 | }
426 |
427 | .ds-finder-form-company__name {
428 | flex: 1;
429 | font-size: 1.3rem;
430 | line-height: calc(20/13);
431 | white-space: nowrap;
432 | max-width: 50%;
433 | overflow: hidden;
434 | text-overflow: ellipsis;
435 | }
436 |
437 | .ds-finder-form__submit {
438 | padding: var(--spacing-02) var(--spacing-04);
439 | border-width: 0 0 0 1px;
440 | border-color: var(--colors-grey-300);
441 | border-radius: 0 4px 4px 0;
442 | background-color: #fff;
443 | font-size: 1.6rem;
444 | line-height: calc(24/16);
445 | color: var(--colors-grey-700);
446 | transition: background-color 150ms ease-in, color 150ms ease-in;
447 |
448 | &:hover,
449 | &:focus {
450 | background-color: var(--colors-grey-100);
451 | color: var(--colors-grey-900);
452 | }
453 | }
454 |
455 | .ds-finder__close {
456 | font-size: 1.6rem;
457 | color: var(--colors-grey-600);
458 | }
459 |
460 | // Result
461 | .ds-result--single {
462 | padding: var(--spacing-06);
463 |
464 | .ds-result__fullname {
465 | font-size: 1.4rem;
466 | }
467 |
468 | .ds-result__sources {
469 | padding-top: var(--spacing-04);
470 | margin-top: var(--spacing-06);
471 | border-top: 1px solid var(--colors-grey-300);
472 | }
473 |
474 | .ds-sources-list {
475 | margin-top: var(--spacing-06);
476 | }
477 | }
478 |
479 | // Results
480 | //
481 | .ds-results {
482 | padding-bottom: var(--spacing-16);
483 | }
484 |
485 | .ds-result {
486 | padding: var(--spacing-04) var(--spacing-06);
487 | border-bottom: 1px solid var(--colors-grey-300);
488 | }
489 |
490 | .ds-result__avatar {
491 | display: flex;
492 | width: 7.2rem;
493 | height: 7.2rem;
494 | margin-right: var(--spacing-02);
495 | border-radius: 50%;
496 | overflow: hidden;
497 | background: #fff;
498 |
499 | img {
500 | max-width: 100%;
501 | height: auto;
502 | object-fit: contain;
503 | }
504 | }
505 |
506 | .ds-result__data {
507 | display: flex;
508 | gap: var(--spacing-02);
509 | }
510 |
511 | .ds-result__primary {
512 | flex: 0 0 35%;
513 | max-width: 35%;
514 | }
515 |
516 | .ds-result__secondary {
517 | flex: 1;
518 | padding-left: var(--spacing-02);
519 | }
520 |
521 | .ds-result__fullname {
522 | font-weight: 600;
523 | font-size: 1.3rem;
524 | line-height: calc(20/13);
525 | color: var(--colors-grey-1000);
526 | }
527 |
528 | .ds-result__email {
529 | display: inline-block;
530 | max-width: 100%;
531 | font-weight: 500;
532 | font-size: 1.3rem;
533 | line-height: calc(20/13);
534 | color: var(--colors-grey-800);
535 | overflow: hidden;
536 | text-overflow: ellipsis;
537 | white-space: nowrap;
538 | user-select: none;
539 |
540 | &.copy-email {
541 | user-select: all;
542 | cursor: pointer;
543 | }
544 |
545 | span {
546 | filter: blur(4px);
547 | }
548 | }
549 |
550 | .ds-result__verification {
551 | margin-top: var(--spacing-01);
552 | display: flex;
553 | gap: var(--spacing-02);
554 | align-items: center;
555 | line-height: calc(16/12);
556 | }
557 |
558 | .ds-result__attribute {
559 | display: flex;
560 | align-items: center;
561 | gap: var(--spacing-01);
562 | font-size: 1.2rem;
563 | line-height: calc(16/12);
564 | color: var(--colors-grey-800);
565 |
566 | .far {
567 | color: var(--colors-grey-500);
568 | }
569 |
570 | &:not(:first-child) {
571 | margin-top: var(--spacing-01);
572 | }
573 | }
574 |
575 | .ds-result__department {
576 | text-transform: capitalize;
577 | }
578 |
579 | .ds-result__save {
580 | align-self: center;
581 | text-align: right;
582 | }
583 |
584 | .ds-result__source {
585 | align-self: center;
586 | padding: var(--spacing-005);
587 | min-width: 9rem;
588 | font-size: 1.2rem;
589 | text-align: right;
590 | color: var(--colors-grey-700);
591 | background-color: transparent;
592 | border: 0;
593 | padding: 0;
594 |
595 | &:hover,
596 | &:focus {
597 | color: var(--colors-grey-900);
598 | }
599 | }
600 |
601 | .ds-result__social {
602 | &[href*="twitter.com"] {
603 | color: var(--colors-brand-twitter-blue);
604 | }
605 |
606 | &[href*="linkedin.com"] {
607 | color: var(--colors-brand-linkedin-blue);
608 | }
609 |
610 | &:hover,
611 | &:focus {
612 | text-decoration: none;
613 | opacity: .8;
614 | }
615 | }
616 |
617 | .ds-result__sources {
618 | display: none;
619 | color: var(--colors-grey-700);
620 | font-size: 1.3rem;
621 | line-height: calc(16/13);
622 | }
623 |
624 | .ds-sources-list {
625 | margin: var(--spacing-06) 0 0 0;
626 | padding: 0;
627 | list-style-type: none;
628 | }
629 |
630 | .ds-sources-list__item {
631 | display: flex;
632 | align-items: center;
633 | gap: var(--spacing-02);
634 |
635 | &:not(:first-child) {
636 | margin-top: var(--spacing-02);
637 | }
638 | }
639 |
640 | .ds-sources-list__link {
641 | flex: 1;
642 | max-width: max-content;
643 | overflow: hidden;
644 | text-overflow: ellipsis;
645 | font-size: 1.3rem;
646 | line-height: calc(20/13);
647 | white-space: nowrap;
648 | color: var(--colors-secondary-700);
649 |
650 | &:hover,
651 | &:focus {
652 | color: var(--colors-secondary-800);
653 | text-decoration: underline;
654 | }
655 | }
656 |
657 | .ds-sources-list__date {
658 | margin-left: auto;
659 | color: var(--colors-grey-600);
660 | font-size: 1.2rem;
661 | line-height: calc(20/12);
662 | }
663 |
664 | .ds-sources-list--outdated,
665 | .ds-sources-list__item--outdated {
666 | .ds-sources-list__link {
667 | pointer-events: none;
668 | color: var(--colors-grey-600);
669 | text-decoration: line-through;
670 | }
671 | }
672 |
673 | .ds-sources__toggle {
674 | display: flex;
675 | align-items: center;
676 | gap: var(--spacing-01);
677 | padding: 0;
678 | background-color: transparent;
679 | border: 0;
680 | font-size: 1.2rem;
681 | line-height: calc(16/12);
682 | color: var(--colors-grey-700)
683 | }
684 |
685 |
686 | // Other popup rendering
687 | //
688 | .connect-container,
689 | .connect-again-container,
690 | .no-result-container,
691 | .no-result-with-filters-container,
692 | .webmail-container {
693 | display: none;
694 | height: auto;
695 | padding: var(--spacing-16) var(--spacing-10);
696 | }
697 |
698 | .no-finder-result {
699 | display: none;
700 | border-bottom: 1px solid var(--colors-grey-300);
701 | }
702 |
703 | .connect-message,
704 | .connect-again-container {
705 | margin-bottom: 15px;
706 | }
707 |
708 | #error-message-container {
709 | display: none;
710 | margin: var(--spacing-02);
711 | }
712 |
713 | .see-more {
714 | margin: var(--spacing-06);
715 | }
716 |
717 | // Feedback
718 | //
719 | .feedback-notification,
720 | .rate-notification,
721 | .contact-notification {
722 | display: none;
723 | }
724 |
725 | .feedback-block {
726 | display: flex;
727 | align-items: center;
728 | gap: var(--spacing-02);
729 | padding: var(--spacing-04) var(--spacing-06);
730 | border-radius: .4rem;
731 | margin: var(--spacing-02) var(--spacing-02) var(--spacing-04);
732 | background: var(--colors-secondary-100);
733 | min-height: 9.2rem;
734 | line-height: calc(20/14);
735 |
736 | .h-button {
737 | min-width: 8rem;
738 | justify-content: center;
739 | white-space: nowrap;
740 | }
741 |
742 | p {
743 | margin: 0;
744 | }
745 | }
746 |
747 | .feedback-block__title {
748 | font-size: 1.4rem;
749 | font-weight: 500;
750 | }
751 |
752 | .feedback-block__actions {
753 | margin-left: auto;
754 | display: flex;
755 | gap: var(--spacing-02);
756 | justify-content: flex-end;
757 | }
758 |
759 | .tooltip-inner {
760 | padding: 7px 10px;
761 | font-size: 13px;
762 | }
763 |
764 |
765 | // Hidden area to copy emails
766 | //
767 | #copy-area {
768 | position: fixed;
769 | top: 0px;
770 | left: 0px;
771 | }
772 |
--------------------------------------------------------------------------------
/src/shared/js/lib/purify.min.js:
--------------------------------------------------------------------------------
1 | /*! @license DOMPurify 2.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.1/LICENSE */
2 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()}(this,(function(){"use strict";var e=Object.hasOwnProperty,t=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,o=Object.getOwnPropertyDescriptor,i=Object.freeze,a=Object.seal,l=Object.create,c="undefined"!=typeof Reflect&&Reflect,s=c.apply,u=c.construct;s||(s=function(e,t,n){return e.apply(t,n)}),i||(i=function(e){return e}),a||(a=function(e){return e}),u||(u=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1?n-1:0),o=1;o/gm),U=a(/^data-[\-\w.\u00B7-\uFFFF]/),j=a(/^aria-[\-\w]+$/),B=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),P=a(/^(?:\w+script|data):/i),W=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),G="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function q(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:K(),n=function(t){return e(t)};if(n.version="2.3.1",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var r=t.document,o=t.document,a=t.DocumentFragment,l=t.HTMLTemplateElement,c=t.Node,s=t.Element,u=t.NodeFilter,f=t.NamedNodeMap,x=void 0===f?t.NamedNodeMap||t.MozNamedAttrMap:f,Y=t.Text,X=t.Comment,$=t.DOMParser,Z=t.trustedTypes,J=s.prototype,Q=N(J,"cloneNode"),ee=N(J,"nextSibling"),te=N(J,"childNodes"),ne=N(J,"parentNode");if("function"==typeof l){var re=o.createElement("template");re.content&&re.content.ownerDocument&&(o=re.content.ownerDocument)}var oe=V(Z,r),ie=oe&&ze?oe.createHTML(""):"",ae=o,le=ae.implementation,ce=ae.createNodeIterator,se=ae.createDocumentFragment,ue=ae.getElementsByTagName,fe=r.importNode,me={};try{me=w(o).documentMode?o.documentMode:{}}catch(e){}var de={};n.isSupported="function"==typeof ne&&le&&void 0!==le.createHTMLDocument&&9!==me;var pe=z,ge=H,he=U,ye=j,ve=P,be=W,Te=B,Ae=null,xe=S({},[].concat(q(k),q(E),q(D),q(R),q(M))),Se=null,we=S({},[].concat(q(L),q(F),q(I),q(C))),Ne=null,ke=null,Ee=!0,De=!0,Oe=!1,Re=!1,_e=!1,Me=!1,Le=!1,Fe=!1,Ie=!1,Ce=!0,ze=!1,He=!0,Ue=!0,je=!1,Be={},Pe=null,We=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Ge=null,qe=S({},["audio","video","img","source","image","track"]),Ke=null,Ve=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ye="http://www.w3.org/1998/Math/MathML",Xe="http://www.w3.org/2000/svg",$e="http://www.w3.org/1999/xhtml",Ze=$e,Je=!1,Qe=null,et=o.createElement("form"),tt=function(e){Qe&&Qe===e||(e&&"object"===(void 0===e?"undefined":G(e))||(e={}),e=w(e),Ae="ALLOWED_TAGS"in e?S({},e.ALLOWED_TAGS):xe,Se="ALLOWED_ATTR"in e?S({},e.ALLOWED_ATTR):we,Ke="ADD_URI_SAFE_ATTR"in e?S(w(Ve),e.ADD_URI_SAFE_ATTR):Ve,Ge="ADD_DATA_URI_TAGS"in e?S(w(qe),e.ADD_DATA_URI_TAGS):qe,Pe="FORBID_CONTENTS"in e?S({},e.FORBID_CONTENTS):We,Ne="FORBID_TAGS"in e?S({},e.FORBID_TAGS):{},ke="FORBID_ATTR"in e?S({},e.FORBID_ATTR):{},Be="USE_PROFILES"in e&&e.USE_PROFILES,Ee=!1!==e.ALLOW_ARIA_ATTR,De=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Re=e.SAFE_FOR_TEMPLATES||!1,_e=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,Ie=e.RETURN_DOM_FRAGMENT||!1,Ce=!1!==e.RETURN_DOM_IMPORT,ze=e.RETURN_TRUSTED_TYPE||!1,Le=e.FORCE_BODY||!1,He=!1!==e.SANITIZE_DOM,Ue=!1!==e.KEEP_CONTENT,je=e.IN_PLACE||!1,Te=e.ALLOWED_URI_REGEXP||Te,Ze=e.NAMESPACE||$e,Re&&(De=!1),Ie&&(Fe=!0),Be&&(Ae=S({},[].concat(q(M))),Se=[],!0===Be.html&&(S(Ae,k),S(Se,L)),!0===Be.svg&&(S(Ae,E),S(Se,F),S(Se,C)),!0===Be.svgFilters&&(S(Ae,D),S(Se,F),S(Se,C)),!0===Be.mathMl&&(S(Ae,R),S(Se,I),S(Se,C))),e.ADD_TAGS&&(Ae===xe&&(Ae=w(Ae)),S(Ae,e.ADD_TAGS)),e.ADD_ATTR&&(Se===we&&(Se=w(Se)),S(Se,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&S(Ke,e.ADD_URI_SAFE_ATTR),e.FORBID_CONTENTS&&(Pe===We&&(Pe=w(Pe)),S(Pe,e.FORBID_CONTENTS)),Ue&&(Ae["#text"]=!0),_e&&S(Ae,["html","head","body"]),Ae.table&&(S(Ae,["tbody"]),delete Ne.tbody),i&&i(e),Qe=e)},nt=S({},["mi","mo","mn","ms","mtext"]),rt=S({},["foreignobject","desc","title","annotation-xml"]),ot=S({},E);S(ot,D),S(ot,O);var it=S({},R);S(it,_);var at=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:$e,tagName:"template"});var n=g(e.tagName),r=g(t.tagName);if(e.namespaceURI===Xe)return t.namespaceURI===$e?"svg"===n:t.namespaceURI===Ye?"svg"===n&&("annotation-xml"===r||nt[r]):Boolean(ot[n]);if(e.namespaceURI===Ye)return t.namespaceURI===$e?"math"===n:t.namespaceURI===Xe?"math"===n&&rt[r]:Boolean(it[n]);if(e.namespaceURI===$e){if(t.namespaceURI===Xe&&!rt[r])return!1;if(t.namespaceURI===Ye&&!nt[r])return!1;var o=S({},["title","style","font","a","script"]);return!it[n]&&(o[n]||!ot[n])}return!1},lt=function(e){p(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ie}catch(t){e.remove()}}},ct=function(e,t){try{p(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Se[e])if(Fe||Ie)try{lt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},st=function(e){var t=void 0,n=void 0;if(Le)e=""+e;else{var r=h(e,/^[\r\n\t ]+/);n=r&&r[0]}var i=oe?oe.createHTML(e):e;if(Ze===$e)try{t=(new $).parseFromString(i,"text/html")}catch(e){}if(!t||!t.documentElement){t=le.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?"":i}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(o.createTextNode(n),a.childNodes[0]||null),Ze===$e?ue.call(t,_e?"html":"body")[0]:_e?t.documentElement:a},ut=function(e){return ce.call(e.ownerDocument||e,e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT,null,!1)},ft=function(e){return!(e instanceof Y||e instanceof X)&&!("string"==typeof e.nodeName&&"string"==typeof e.textContent&&"function"==typeof e.removeChild&&e.attributes instanceof x&&"function"==typeof e.removeAttribute&&"function"==typeof e.setAttribute&&"string"==typeof e.namespaceURI&&"function"==typeof e.insertBefore)},mt=function(e){return"object"===(void 0===c?"undefined":G(c))?e instanceof c:e&&"object"===(void 0===e?"undefined":G(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},dt=function(e,t,r){de[e]&&m(de[e],(function(e){e.call(n,t,r,Qe)}))},pt=function(e){var t=void 0;if(dt("beforeSanitizeElements",e,null),ft(e))return lt(e),!0;if(h(e.nodeName,/[\u0080-\uFFFF]/))return lt(e),!0;var r=g(e.nodeName);if(dt("uponSanitizeElement",e,{tagName:r,allowedTags:Ae}),!mt(e.firstElementChild)&&(!mt(e.content)||!mt(e.content.firstElementChild))&&T(/<[/\w]/g,e.innerHTML)&&T(/<[/\w]/g,e.textContent))return lt(e),!0;if("select"===r&&T(/=0;--a)o.insertBefore(Q(i[a],!0),ee(e))}return lt(e),!0}return e instanceof s&&!at(e)?(lt(e),!0):"noscript"!==r&&"noembed"!==r||!T(/<\/no(script|embed)/i,e.innerHTML)?(Re&&3===e.nodeType&&(t=e.textContent,t=y(t,pe," "),t=y(t,ge," "),e.textContent!==t&&(p(n.removed,{element:e.cloneNode()}),e.textContent=t)),dt("afterSanitizeElements",e,null),!1):(lt(e),!0)},gt=function(e,t,n){if(He&&("id"===t||"name"===t)&&(n in o||n in et))return!1;if(De&&!ke[t]&&T(he,t));else if(Ee&&T(ye,t));else{if(!Se[t]||ke[t])return!1;if(Ke[t]);else if(T(Te,y(n,be,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==v(n,"data:")||!Ge[e]){if(Oe&&!T(ve,y(n,be,"")));else if(n)return!1}else;}return!0},ht=function(e){var t=void 0,r=void 0,o=void 0,i=void 0;dt("beforeSanitizeAttributes",e,null);var a=e.attributes;if(a){var l={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Se};for(i=a.length;i--;){var c=t=a[i],s=c.name,u=c.namespaceURI;if(r=b(t.value),o=g(s),l.attrName=o,l.attrValue=r,l.keepAttr=!0,l.forceKeepAttr=void 0,dt("uponSanitizeAttribute",e,l),r=l.attrValue,!l.forceKeepAttr&&(ct(s,e),l.keepAttr))if(T(/\/>/i,r))ct(s,e);else{Re&&(r=y(r,pe," "),r=y(r,ge," "));var f=e.nodeName.toLowerCase();if(gt(f,o,r))try{u?e.setAttributeNS(u,s,r):e.setAttribute(s,r),d(n.removed)}catch(e){}}}dt("afterSanitizeAttributes",e,null)}},yt=function e(t){var n=void 0,r=ut(t);for(dt("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)dt("uponSanitizeShadowNode",n,null),pt(n)||(n.content instanceof a&&e(n.content),ht(n));dt("afterSanitizeShadowDOM",t,null)};return n.sanitize=function(e,o){var i=void 0,l=void 0,s=void 0,u=void 0,f=void 0;if((Je=!e)&&(e="\x3c!--\x3e"),"string"!=typeof e&&!mt(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!n.isSupported){if("object"===G(t.toStaticHTML)||"function"==typeof t.toStaticHTML){if("string"==typeof e)return t.toStaticHTML(e);if(mt(e))return t.toStaticHTML(e.outerHTML)}return e}if(Me||tt(o),n.removed=[],"string"==typeof e&&(je=!1),je);else if(e instanceof c)1===(l=(i=st("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===l.nodeName||"HTML"===l.nodeName?i=l:i.appendChild(l);else{if(!Fe&&!Re&&!_e&&-1===e.indexOf("<"))return oe&&ze?oe.createHTML(e):e;if(!(i=st(e)))return Fe?null:ie}i&&Le&<(i.firstChild);for(var m=ut(je?e:i);s=m.nextNode();)3===s.nodeType&&s===u||pt(s)||(s.content instanceof a&&yt(s.content),ht(s),u=s);if(u=null,je)return e;if(Fe){if(Ie)for(f=se.call(i.ownerDocument);i.firstChild;)f.appendChild(i.firstChild);else f=i;return Ce&&(f=fe.call(r,f,!0)),f}var d=_e?i.outerHTML:i.innerHTML;return Re&&(d=y(d,pe," "),d=y(d,ge," ")),oe&&ze?oe.createHTML(d):d},n.setConfig=function(e){tt(e),Me=!0},n.clearConfig=function(){Qe=null,Me=!1},n.isValidAttribute=function(e,t,n){Qe||tt({});var r=g(e),o=g(t);return gt(r,o,n)},n.addHook=function(e,t){"function"==typeof t&&(de[e]=de[e]||[],p(de[e],t))},n.removeHook=function(e){de[e]&&d(de[e])},n.removeHooks=function(e){de[e]&&(de[e]=[])},n.removeAllHooks=function(){de={}},n}()}));
3 | //# sourceMappingURL=purify.min.js.map
4 |
--------------------------------------------------------------------------------
/src/browser_action/js/domain_search.coffee:
--------------------------------------------------------------------------------
1 | DomainSearch = ->
2 | {
3 | # Used to display full department names
4 | department_names: {
5 | executive: chrome.i18n.getMessage("department_executive"),
6 | it: chrome.i18n.getMessage("department_it"),
7 | finance: chrome.i18n.getMessage("department_finance"),
8 | management: chrome.i18n.getMessage("department_finance"),
9 | sales: chrome.i18n.getMessage("department_sales"),
10 | legal: chrome.i18n.getMessage("department_legal"),
11 | support: chrome.i18n.getMessage("department_support"),
12 | hr: chrome.i18n.getMessage("department_hr"),
13 | marketing: chrome.i18n.getMessage("department_marketing"),
14 | communication: chrome.i18n.getMessage("department_communication"),
15 | education: chrome.i18n.getMessage("department_education"),
16 | design:chrome.i18n.getMessage("department_design"),
17 | health: chrome.i18n.getMessage("department_health"),
18 | operations: chrome.i18n.getMessage("department_operations")
19 | }
20 |
21 | launch: ->
22 | @domain = window.domain
23 | @trial = (typeof window.api_key == "undefined" || window.api_key == "")
24 | @fetch()
25 |
26 | fetch: () ->
27 | _this = @
28 | _this.cleanSearchResults()
29 |
30 | _this.department = _this.departmentFilter()
31 | _this.type = _this.typeFilter()
32 |
33 | if _this.department or _this.type
34 | $('.filters__clear').show()
35 | else
36 | $('.filters__clear').hide()
37 |
38 | $.ajax
39 | url: Api.domainSearch(_this.domain, _this.department, _this.type, window.api_key)
40 | headers: "Email-Hunter-Origin": "chrome_extension"
41 | type: "GET"
42 | data: format: "json"
43 | dataType: "json"
44 | jsonp: false
45 | error: (xhr) ->
46 | $("#loading-placeholder").hide()
47 | $("#domain-search").show()
48 | $(".filters").css("visibility", "hidden")
49 |
50 | if xhr.status == 400
51 | displayError chrome.i18n.getMessage("something_went_wrong_with_the_query")
52 | else if xhr.status == 401
53 | $(".connect-again-container").show()
54 | else if xhr.status == 403
55 | $("#blocked-notification").show()
56 | else if xhr.status == 429
57 | unless _this.trial
58 | Account.returnRequestsError (e) ->
59 | displayError e
60 | else
61 | $(".connect-container").show()
62 | else
63 | displayError DOMPurify.sanitize(xhr.responseJSON["errors"][0]["details"])
64 |
65 | success: (result) ->
66 | _this.webmail = result.data.webmail
67 | _this.pattern = result.data.pattern
68 | _this.accept_all = result.data.accept_all
69 | _this.verification = result.data.verification
70 | _this.organization = result.data.organization
71 | _this.results = result.data.emails
72 | _this.results_count = result.meta.results
73 | _this.offset = result.meta.offset
74 | _this.type = result.meta.params.type
75 |
76 | $("#loading-placeholder").hide()
77 | $("#domain-search").show()
78 | $(".filters").removeAttr("style")
79 |
80 | # Not logged in: we hide the Email Finder
81 | if _this.trial
82 | $(".filters__by-name").hide()
83 | $(".find-by-name").hide()
84 |
85 | _this.manageFilters()
86 | _this.clearFilters()
87 | _this.render()
88 |
89 | render: ->
90 | # Is webmail -> STOP
91 | if @webmail == true
92 | $("#domain-search").addClass("ds-no-results")
93 | $(".webmail-container .domain").text @domain
94 | $(".webmail-container").show()
95 | return
96 |
97 | # No results -> STOP
98 | if @results_count == 0
99 | if @type or @department
100 | $(".no-result-with-filters-container").show()
101 | else
102 | $("#domain-search").addClass("ds-no-results")
103 | $(".no-result-container .domain").text @domain
104 | $(".no-result-container").show()
105 |
106 | return
107 |
108 | # Display: the current domain
109 | $("#current-domain").text @domain
110 |
111 | # Remove "no-results" class
112 | $("#domain-search").removeClass("ds-no-results")
113 |
114 | # Activate dropdown
115 | $("[data-toggle='dropdown']").dropdown()
116 |
117 | # Display: complete search link or Sign up CTA
118 | unless @trial
119 | $(".header-search-link").attr "href", "https://hunter.io/search/" + @domain + "?utm_source=chrome_extension&utm_medium=chrome_extension&utm_campaign=extension&utm_content=browser_popup"
120 | $(".header-search-link").show()
121 |
122 | # Display: the number of results
123 | if @results_count == 1 then s = "" else s = "s"
124 | $("#domain-search .results-header__count").html chrome.i18n.getMessage("results_for_domain", [DOMPurify.sanitize(Utilities.numberWithCommas(@results_count)), s, DOMPurify.sanitize(@domain)])
125 |
126 | # Display: the email pattern if any
127 | if @pattern != null
128 | $(".results-header__pattern").show()
129 | $(".results-header__pattern strong").html(@addPatternTitle(@pattern) + "@" + @domain)
130 | $("[data-toggle='tooltip']").tooltip()
131 |
132 | # Email Finder
133 | $(".ds-finder-form-company__name").text @organization
134 | $(".ds-finder-form-company__logo").attr "src", "https://logo.clearbit.com/" + @domain
135 | emailFinder = new EmailFinder
136 | emailFinder.validateForm()
137 | @openFindbyName()
138 |
139 | # Display: the updated number of requests
140 | loadAccountInformation()
141 |
142 | # We count call to measure use
143 | countCall()
144 | @feedbackNotification()
145 |
146 | # Display: the results
147 | @showResults()
148 |
149 | # Render: set again an auto height on html
150 | $("html").css
151 | height: "auto"
152 |
153 | # Display: link to see more
154 | if @results_count > 10
155 | remaining_results = @results_count - 10
156 | $(".search-results").append "" + chrome.i18n.getMessage("see_all_the_results", DOMPurify.sanitize(Utilities.numberWithCommas(remaining_results))) + ""
157 |
158 |
159 | showResults: ->
160 | _this = @
161 | @results.slice(0,10).forEach (result) ->
162 |
163 | # Sources
164 | if result.sources.length == 1
165 | result.sources_link = "1 source"
166 | else if result.sources.length >= 20
167 | result.sources_link = "20+ sources"
168 | else
169 | result.sources_link = result.sources.length + " sources"
170 |
171 | # Confidence score color
172 | if result.confidence < 30
173 | result.confidence_score_class = "low-score"
174 | else if result.confidence >= 70
175 | result.confidence_score_class = "high-score"
176 | else
177 | result.confidence_score_class = "average-score"
178 |
179 | # Save leads button
180 | unless _this.trial
181 | result.lead_button = ""
182 | else
183 | result.lead_button = ""
184 |
185 | # Full name
186 | if result.first_name != null && result.last_name != null
187 | result.full_name = DOMPurify.sanitize(result.first_name) + " " + DOMPurify.sanitize(result.last_name)
188 |
189 | if result.department
190 | result.department = DOMPurify.sanitize(_this.department_names[result.department])
191 |
192 |
193 | Handlebars.registerHelper "userDate", (options) ->
194 | new Handlebars.SafeString(Utilities.dateInWords(options.fn(this)))
195 |
196 | Handlebars.registerHelper "ifIsVerified", (verification_status, options) ->
197 | if verification_status == "valid"
198 | return options.fn(this)
199 | options.inverse this
200 |
201 | Handlebars.registerHelper "ifIsAcceptAll", (options) ->
202 | if _this.accept_all
203 | return options.fn(this)
204 | options.inverse this
205 |
206 | # Integrate the result
207 | template = JST["src/browser_action/templates/search_results.hbs"]
208 | result_tag = $(Utilities.localizeHTML(template(result)))
209 | $(".search-results").append(result_tag)
210 |
211 | # Add the lead's data
212 | save_lead_button = result_tag.find(".save-lead-button")
213 | save_lead_button.data
214 | email: result.value
215 | lead = new LeadButton
216 | lead.saveButtonListener(save_lead_button)
217 | lead.disableSaveLeadButtonIfLeadExists(save_lead_button)
218 |
219 | # Hide beautifully if the user is not logged
220 | if _this.trial
221 | result_tag.find(".ds-result__email").removeClass("copy-email")
222 | result_tag.find(".ds-result__email").attr("title", chrome.i18n.getMessage("sign_up_to_uncover_more_emails"))
223 | result_tag.find(".ds-result__email").html result_tag.find(".ds-result__email").text().replace("**", "aaa")
224 |
225 | @openSources()
226 | $(".search-results").show()
227 | $("[data-toggle='tooltip']").tooltip()
228 |
229 | # For people not logged in, the copy and verification functions are not displayed
230 | if _this.trial
231 | $(".ds-result__verification").remove()
232 | else
233 | @searchVerificationListener()
234 | Utilities.copyEmailListener()
235 |
236 |
237 | searchVerificationListener: ->
238 | _this = @
239 | $(".verification-link").unbind("click").click ->
240 |
241 | verification_link_tag = $(this)
242 | verification_result_tag = $(this).parent().find(".verification-result")
243 |
244 | email = verification_link_tag.data("email")
245 |
246 | return if !email
247 |
248 | verification_link_tag.html(" " + chrome.i18n.getMessage("verifying") + "…")
249 | verification_link_tag.attr("disabled", "true")
250 |
251 | # Launch the API call
252 | $.ajax
253 | url: Api.emailVerifier(email, window.api_key)
254 | headers: "Email-Hunter-Origin": "chrome_extension"
255 | type: "GET"
256 | data: format: "json"
257 | dataType: "json"
258 | jsonp: false
259 | error: (xhr, statusText, err) ->
260 | verification_link_tag.removeAttr("disabled")
261 | verification_link_tag.show()
262 |
263 | if xhr.status == 400
264 | displayError chrome.i18n.getMessage("something_went_wrong_with_the_query")
265 | else if xhr.status == 401
266 | $(".connect-again-container").show()
267 | else if xhr.status == 403
268 | $("#domain-search").hide()
269 | $("#blocked-notification").show()
270 | else if xhr.status == 429
271 | unless _this.trial
272 | Account.returnRequestsError (e) ->
273 | displayError e
274 | else
275 | $(".connect-container").show()
276 | else
277 | displayError DOMPurify.sanitize(xhr.responseJSON["errors"][0]["details"])
278 |
279 |
280 | success: (result, statusText, xhr) ->
281 | if xhr.status == 202
282 | verification_link_tag.removeAttr("disabled")
283 | verification_link_tag.html(" " + chrome.i18n.getMessage("retry"))
284 | displayError chrome.i18n.getMessage("email_verification_takes_longer")
285 |
286 | else if xhr.status == 222
287 | verification_link_tag.removeAttr("disabled")
288 | verification_link_tag.html(" " + chrome.i18n.getMessage("retry"))
289 | displayError DOMPurify.sanitize(result.errors[0].details)
290 | return
291 |
292 | else
293 | verification_link_tag.remove()
294 |
295 | if result.data.status == "valid"
296 | verification_result_tag.html("
297 |
298 |
299 |
300 | " + result.data.score + "%
301 |
302 | ")
303 | else if result.data.status == "invalid"
304 | verification_result_tag.html("
305 |
306 |
307 |
308 | " + result.data.score + "%
309 |
310 | ")
311 | else if result.data.status == "accept_all"
312 | verification_result_tag.html("
313 |
314 |
315 |
316 | " + result.data.score + "%
317 |
318 | ")
319 | else
320 | verification_result_tag.html("
321 |
322 |
323 |
324 | " + result.data.score + "%
325 |
326 | ")
327 |
328 | # We update the number of requests
329 | loadAccountInformation()
330 |
331 |
332 | openSources: ->
333 | $(".sources-link").unbind("click").click (e) ->
334 | if $(this).parents(".ds-result").find(".ds-result__sources").is(":visible")
335 | $(this).parents(".ds-result").find(".ds-result__sources").slideUp 300
336 | $(this).find(".fa-angle-up").removeClass("fa-angle-up").addClass("fa-angle-down")
337 | else
338 | $(this).parents(".ds-result").find(".ds-result__sources").slideDown 300
339 | $(this).find(".fa-angle-down").removeClass("fa-angle-down").addClass("fa-angle-up")
340 |
341 | openFindbyName: ->
342 | $(".filters__by-name").unbind("click").click (e) ->
343 | if $("#find-by-name").is(":visible")
344 | $(this).attr "aria-expanded", "false"
345 | $("#find-by-name").hide()
346 | else
347 | $(this).attr "aria-expanded", "true"
348 | $("#find-by-name").show()
349 | $("#full-name-field").focus()
350 |
351 |
352 | manageFilters: ->
353 | _this = @
354 | $(document).on 'click', '.dropdown .dropdown-menu', (e) ->
355 | e.stopPropagation()
356 |
357 | $('.apply-filters').unbind().on "click", ->
358 | checked = $(this).parent().find('[type="checkbox"]:checked, [type="radio"]:checked')
359 | checkedCount = checked.length
360 | dropdownContainer = $(this).parents(".dropdown")
361 |
362 | dropdownContainer.removeClass("open")
363 | dropdownContainer.find('.h-button[data-toggle]').attr("aria-expanded", "false")
364 |
365 | if checkedCount > 0
366 | dropdownContainer.find('.h-button[data-toggle]').attr("data-selected-filters", checkedCount)
367 | else
368 | dropdownContainer.find('.h-button[data-toggle]').removeAttr("data-selected-filters")
369 |
370 | _this.fetch()
371 |
372 | typeFilter: ->
373 | value = $("#type-filters [type='radio']:checked").val()
374 |
375 | departmentFilter: ->
376 | department = ''
377 | $("#departments-filters [type='checkbox']").each ->
378 | if $(this).is(":checked")
379 | department = $(this).val()
380 | return false
381 |
382 | department
383 |
384 | clearFilters: ->
385 | _this = @
386 | $(".clear-filters").unbind().on "click", ->
387 | $('.filters').find('[type="checkbox"]:checked, [type="radio"]:checked').each ->
388 | $(this).prop("checked", false)
389 |
390 | $('.filters [data-selected-filters]').removeAttr("data-selected-filters")
391 | _this.fetch()
392 |
393 |
394 | addPatternTitle: (pattern) ->
395 | pattern = pattern
396 | .replace("{first}", "{first}")
397 | .replace("{last}", "{last}")
398 | .replace("{f}", "{f}")
399 | .replace("{l}", "{l}")
400 | pattern
401 |
402 |
403 | cleanSearchResults: ->
404 | $("#loading-placeholder").show()
405 | $(".search-results").html ""
406 | $(".no-result-with-filters-container").hide()
407 |
408 |
409 | feedbackNotification: ->
410 | chrome.storage.sync.get "calls_count", (value) ->
411 | if value["calls_count"] >= 10
412 | chrome.storage.sync.get "has_given_feedback", (value) ->
413 | if typeof value["has_given_feedback"] == "undefined"
414 | $(".feedback-notification").slideDown 300
415 |
416 | # Ask to note the extension
417 | $("#open-rate-notification").click ->
418 | $(".feedback-notification").slideUp 300
419 | $(".rate-notification").slideDown 300
420 |
421 | # Ask to give use feedback
422 | $("#open-contact-notification").click ->
423 | $(".feedback-notification").slideUp 300
424 | $(".contact-notification").slideDown 300
425 |
426 | $(".feedback-link").click ->
427 | chrome.storage.sync.set "has_given_feedback": true
428 | }
429 |
--------------------------------------------------------------------------------