├── .eslintignore
├── .codebeatignore
├── versions
├── .htaccess
└── updates.json
├── .gitignore
├── .codebeatsettings
├── experiment
├── modules
│ ├── log.sys.js
│ ├── extension.sys.js
│ ├── setup.sys.js
│ ├── credentials.sys.js
│ ├── emitters.sys.js
│ ├── wait.sys.js
│ ├── prompter
│ │ ├── asyncPrompter.sys.js
│ │ ├── loginManagerAuthPrompter.sys.js
│ │ ├── prompter.sys.js
│ │ └── utils.sys.js
│ ├── wrappers
│ │ ├── oauth2.sys.js
│ │ ├── cal.sys.js
│ │ ├── LDAPListenerBase.sys.js
│ │ └── oauth2Module.sys.js
│ ├── gui
│ │ ├── windowListener.sys.js
│ │ ├── commonDialog.sys.js
│ │ └── utils.sys.js
│ ├── onlineOfflineControl.sys.js
│ └── dialogStrings.sys.js
├── schema.json
└── implementation.js
├── modules
├── utils.js
├── log.js
├── externalPrivileges.js
├── nativeMessaging.js
├── getCredentials.js
├── externalRequests.js
├── selected.js
├── storeCredentials.js
├── modal.js
└── keepass.js
├── modal
├── message
│ ├── message.css
│ ├── index.html
│ └── message.js
├── confirm
│ ├── confirm.css
│ ├── index.html
│ └── confirm.js
├── savingPassword
│ ├── savingPassword.css
│ ├── index.html
│ └── savingPassword.js
├── choice
│ ├── choice.css
│ ├── index.html
│ └── choice.js
├── modal.css
└── modalUtils.js
├── crowdin.yml
├── .vscode
├── tasks.json
└── settings.json
├── options
├── options.css
├── options.html
└── options.js
├── package.json
├── manifest.json
├── from-keepassxc-browser
└── nacl-util.min.js
├── .github
└── ISSUE_TEMPLATE.md
├── global.js
├── main.js
├── .eslintrc.json
├── .tools
├── translate.js
└── build.js
├── icons
└── icon.svg
├── README.md
├── releaseNotes.txt
└── _locales
├── zh_CN
└── messages.json
├── ja
└── messages.json
├── ar
└── messages.json
└── bg
└── messages.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | from-keepassxc-browser/*
--------------------------------------------------------------------------------
/.codebeatignore:
--------------------------------------------------------------------------------
1 | from-keepassxc-browser/**
--------------------------------------------------------------------------------
/versions/.htaccess:
--------------------------------------------------------------------------------
1 | Options +Indexes
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | mail-ext-artifacts/
2 | versions/*.xpi
3 | node_modules/
4 |
--------------------------------------------------------------------------------
/.codebeatsettings:
--------------------------------------------------------------------------------
1 | {
2 | "JAVASCRIPT": {
3 | "LOC": [80, 90, 100, 120],
4 | "BLOCK_NESTING": [4, 5, 6, 7]
5 | }
6 | }
--------------------------------------------------------------------------------
/experiment/modules/log.sys.js:
--------------------------------------------------------------------------------
1 | import { extension } from "./extension.sys.js";
2 | export const log = console.log.bind(console, `KeePassXC-Mail (${extension.addonData.version}):`);
--------------------------------------------------------------------------------
/modules/utils.js:
--------------------------------------------------------------------------------
1 | export async function wait(ms, returnValue){
2 | return new Promise(function(resolve){
3 | window.setTimeout(function(){
4 | resolve(returnValue);
5 | }, ms);
6 | });
7 | }
--------------------------------------------------------------------------------
/experiment/modules/extension.sys.js:
--------------------------------------------------------------------------------
1 | import { ExtensionParent } from "resource://gre/modules/ExtensionParent.sys.mjs";
2 | export const extension = ExtensionParent.GlobalManager.getExtension("keepassxc-mail@kkapsner.de");
--------------------------------------------------------------------------------
/modal/message/message.css:
--------------------------------------------------------------------------------
1 | .text {
2 | padding: 1em;
3 | min-width: 300px;
4 | display: inline-block;
5 | }
6 |
7 | .buttons {
8 | text-align: right;
9 | padding: 0 1em;
10 | }
11 | .buttons button {
12 | margin: 0 0.3em;
13 | }
--------------------------------------------------------------------------------
/modal/confirm/confirm.css:
--------------------------------------------------------------------------------
1 | .question {
2 | padding: 1em;
3 | min-width: 300px;
4 | display: inline-block;
5 | }
6 |
7 | .buttons {
8 | text-align: right;
9 | padding: 0 1em;
10 | }
11 | .buttons button {
12 | margin: 0 0.3em;
13 | }
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | files:
2 | - source: /_locales/en/*.json
3 | translation: /_locales/%two_letters_code%/%original_file_name%
4 | languages_mapping:
5 | two_letters_code:
6 | pt-BR: pt_BR
7 | zh-CN: zh_CN
8 | zh-TW: zh_TW
--------------------------------------------------------------------------------
/modal/savingPassword/savingPassword.css:
--------------------------------------------------------------------------------
1 | .question {
2 | padding: 1em;
3 | min-width: 300px;
4 | display: inline-block;
5 | }
6 |
7 | #entries {
8 | margin: 0 1em 1em 1em ;
9 | }
10 |
11 | .buttons {
12 | text-align: right;
13 | grid-column: 2;
14 | }
15 | .buttons button {
16 | margin: 0.3em;
17 | }
--------------------------------------------------------------------------------
/modal/choice/choice.css:
--------------------------------------------------------------------------------
1 | .text {
2 | padding: 1em;
3 | min-width: 300px;
4 | display: inline-block;
5 | }
6 |
7 | .entries {
8 | margin: 0 1em;
9 | }
10 |
11 | .doNotAskAgain {
12 | padding: 0.3em 1em;
13 | }
14 |
15 | .buttons {
16 | text-align: right;
17 | padding: 0 1em;
18 | }
19 | .buttons button {
20 | margin: 0 0.3em;
21 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "build",
9 | "group": "build",
10 | "problemMatcher": []
11 | },
12 | {
13 | "type": "npm",
14 | "script": "eslint",
15 | "problemMatcher": [
16 | "$eslint-compact"
17 | ]
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/modal/modal.css:
--------------------------------------------------------------------------------
1 | html {
2 | margin: 0;
3 | padding: 0;
4 | }
5 | body {
6 | font-size: 11pt;
7 | margin: 0;
8 | padding: 10px;
9 | }
10 |
11 | .content {
12 | display: grid;
13 | grid-template-columns: max-content auto;
14 | }
15 |
16 | .icon {
17 | grid-column: 1 / span 1;
18 | grid-row: 1 / span 2;
19 | align-self: center;
20 | }
21 | .right {
22 | grid-column: 2;
23 | }
24 |
25 | .hidden {
26 | display: none;
27 | }
28 |
29 | select {
30 | font-weight: normal;
31 | padding: 5px;
32 | box-sizing: border-box;
33 | max-width: 100%;
34 | }
--------------------------------------------------------------------------------
/experiment/modules/setup.sys.js:
--------------------------------------------------------------------------------
1 | const setupFunctions = [];
2 | export function addSetup({setup, shutdown}){
3 | if (!setup){
4 | return;
5 | }
6 | if (setupFunctions.setup){
7 | setup();
8 | }
9 | setupFunctions.push({setup, shutdown});
10 | }
11 |
12 | export function setup(){
13 | setupFunctions.forEach(function(setupFunction){
14 | setupFunction.setup();
15 | });
16 | setupFunctions.setup = true;
17 | }
18 |
19 | export function shutdown(){
20 | setupFunctions.forEach(function(setupFunction){
21 | setupFunction.shutdown();
22 | });
23 | setupFunctions.setup = false;
24 | }
--------------------------------------------------------------------------------
/modules/log.js:
--------------------------------------------------------------------------------
1 |
2 | export const log = function(){
3 | function f(d, n){
4 | const s = d.toString();
5 | return "0".repeat(n - s.length) + s;
6 | }
7 | function getCurrentTimestamp(){
8 | const now = new Date();
9 | return `${f(now.getFullYear(), 4)}-${f(now.getMonth() + 1, 2)}-${f(now.getDate(), 2)} `+
10 | `${f(now.getHours(), 2)}:${f(now.getMinutes(), 2)}:` +
11 | `${f(now.getSeconds(), 2)}.${f(now.getMilliseconds(), 3)}`;
12 | }
13 | class Prefix{
14 | toString(){
15 | return `KeePassXC-Mail (${getCurrentTimestamp()}):`;
16 | }
17 | }
18 |
19 | return console.log.bind(console, "%s", new Prefix());
20 | }();
--------------------------------------------------------------------------------
/experiment/modules/credentials.sys.js:
--------------------------------------------------------------------------------
1 | import { passwordEmitter, passwordRequestEmitter } from "./emitters.sys.js";
2 |
3 | export async function storeCredentials(data){
4 | return passwordEmitter.emit("password", data);
5 | }
6 |
7 | export async function requestCredentials(credentialInfo){
8 | const eventData = await passwordRequestEmitter.emit(
9 | "password-requested", credentialInfo
10 | );
11 | return eventData.reduce(function(details, currentDetails){
12 | if (!currentDetails){
13 | return details;
14 | }
15 | details.autoSubmit &= currentDetails.autoSubmit;
16 | if (currentDetails.credentials && currentDetails.credentials.length){
17 | details.credentials = details.credentials.concat(currentDetails.credentials);
18 | }
19 | return details;
20 | }, {autoSubmit: true, credentials: []});
21 | }
--------------------------------------------------------------------------------
/modal/message/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Save password for {host}?
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |

16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/options/options.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | body {
7 | font-size: 10pt;
8 | margin: 0.25em;
9 | }
10 |
11 | h1 {
12 | font-size: 1.5em;
13 | font-weight: bold;
14 | margin-top: 0;
15 | }
16 |
17 | #connectionTable {
18 | table-layout: fixed;
19 | width: 100%;
20 | border-collapse: collapse;
21 | margin: 1em 0em;
22 | }
23 |
24 | #connectionTable td, #connectionTable th {
25 | border: 1px solid darkgray;
26 | overflow: hidden;
27 | padding: 2px;
28 | }
29 |
30 | #connectionTable thead {
31 | background-color: lightgray;
32 | }
33 |
34 | #privilegesTable {
35 | table-layout: fixed;
36 | width: 100%;
37 | border-collapse: collapse;
38 | margin: 1em 0em;
39 | }
40 |
41 | #privilegesTable td, #privilegesTable th {
42 | border: 1px solid darkgray;
43 | overflow: hidden;
44 | padding: 2px;
45 | }
46 |
47 | #privilegesTable thead {
48 | background-color: lightgray;
49 | }
--------------------------------------------------------------------------------
/experiment/modules/emitters.sys.js:
--------------------------------------------------------------------------------
1 | import { log } from "./log.sys.js";
2 | import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
3 | import { setup, shutdown } from "./setup.sys.js";
4 |
5 | log("initializing emitters");
6 |
7 | export const passwordEmitter = new ExtensionCommon.EventEmitter();
8 |
9 | export const passwordRequestEmitter = new class extends ExtensionCommon.EventEmitter {
10 | constructor(){
11 | super();
12 | this.callbackCount = 0;
13 | }
14 |
15 | add(callback){
16 | this.on("password-requested", callback);
17 | this.callbackCount++;
18 |
19 | if (this.callbackCount === 1){
20 | setup();
21 | }
22 | }
23 |
24 | remove(callback){
25 | this.off("password-requested", callback);
26 | this.callbackCount--;
27 |
28 | if (this.callbackCount === 0){
29 | log("Last password request emitter removed -> shutdown experiment");
30 | shutdown();
31 | }
32 | }
33 | };
--------------------------------------------------------------------------------
/modal/confirm/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {title}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |

16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "keepassxc-mail",
3 | "version": "0.1.0",
4 | "description": "MailExtension to talk to keepassxc",
5 | "main": "main.js",
6 | "scripts": {
7 | "build": "node .tools/build.js",
8 | "eslint": "eslint ./ --ext .js,.html,.php"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/kkapsner/keepassxc-mail.git"
13 | },
14 | "keywords": [
15 | "keepassxc",
16 | "MailExtension",
17 | "Thunderbird",
18 | "password manager"
19 | ],
20 | "author": "Korbinian Kapsner",
21 | "license": "GPL-3.0",
22 | "bugs": {
23 | "url": "https://github.com/kkapsner/keepassxc-mail/issues"
24 | },
25 | "homepage": "https://github.com/kkapsner/keepassxc-mail#readme",
26 | "devDependencies": {
27 | "eslint": "^8.18.0",
28 | "eslint-plugin-eslint-comments": "^3.2.0",
29 | "eslint-plugin-html": "^6.2.0",
30 | "eslint-plugin-promise": "^6.0.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/modules/externalPrivileges.js:
--------------------------------------------------------------------------------
1 | import { log } from "./log.js";
2 | let blocker = null;
3 |
4 | export async function getPrivileges(extensionId){
5 | await blocker;
6 | const storage = await browser.storage.local.get({"privileges": {}});
7 | const p = storage.privileges[extensionId] || {request: undefined, store: undefined};
8 | return p;
9 | }
10 |
11 | async function nonBlockingSetPrivileges(extensionId, type, value){
12 | log("Setting privilege", type, "for", extensionId, "to", value);
13 |
14 | const storage = await browser.storage.local.get({"privileges": {}});
15 | const p = storage.privileges[extensionId] || {request: undefined, store: undefined};
16 | p[type] = value;
17 | storage.privileges[extensionId] = p;
18 | await browser.storage.local.set(storage);
19 | }
20 |
21 | export async function setPrivileges(extensionId, type, value){
22 | await blocker;
23 | const ownBlocker = nonBlockingSetPrivileges(extensionId, type, value);
24 | blocker = ownBlocker;
25 | return ownBlocker;
26 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.enabled": true,
3 | "cSpell.words": [
4 | "calauth",
5 | "cardbook",
6 | "codebeat",
7 | "dialogaccept",
8 | "gdata",
9 | "Gdata",
10 | "hbox",
11 | "HKCU",
12 | "IIFE",
13 | "keebird",
14 | "keepass",
15 | "keepassxc",
16 | "kkapsner",
17 | "ldaps",
18 | "menulist",
19 | "menupopup",
20 | "messengercompose",
21 | "mozilla",
22 | "mozldap",
23 | "Msgs",
24 | "nacl",
25 | "nntp",
26 | "oauth",
27 | "openpgp",
28 | "Passwd",
29 | "pipnss",
30 | "startupcache",
31 | "stringbundle",
32 | "tooltiptext",
33 | "tweetnacl",
34 | "wcap",
35 | "XPCOM"
36 | ],
37 | "cSpell.language": "en,de,en-GB",
38 | "cSpell.ignorePaths": [
39 | "**/package-lock.json",
40 | "**/node_modules/**",
41 | "**/from-keepassxc-browser/**",
42 | "**/vscode-extension/**",
43 | "**/.git/objects/**",
44 | "experiment/modules/wrappers/LDAPListenerBase.sys.js",
45 | ".vscode",
46 | ".eslintrc.json"
47 | ],
48 | "eslint.lintTask.enable": true
49 | }
--------------------------------------------------------------------------------
/modal/message/message.js:
--------------------------------------------------------------------------------
1 | /* globals getMessage, resizeToContent, initModal*/
2 | "use strict";
3 |
4 | function fillText(message){
5 | document.querySelector("title").textContent = message.title;
6 | const textNode = document.querySelector(".text");
7 | let first = true;
8 | message.text.split(/\n/g).forEach(function(line){
9 | if (!first){
10 | textNode.appendChild(document.createElement("br"));
11 | }
12 | first = false;
13 | textNode.appendChild(document.createTextNode(line));
14 | });
15 | document.getElementById("ok").textContent = browser.i18n.getMessage("modal.message.ok");
16 | }
17 |
18 | initModal({messageCallback: function(message){
19 | return new Promise(function(resolve){
20 | fillText(message);
21 | document.querySelectorAll("button").forEach(function(button){
22 | button.disabled = false;
23 | button.addEventListener("click", function(){
24 | if (button.id === "ok"){
25 | resolve(true);
26 | window.close();
27 | }
28 | });
29 | });
30 | resizeToContent();
31 | });
32 | }});
--------------------------------------------------------------------------------
/experiment/modules/wait.sys.js:
--------------------------------------------------------------------------------
1 | /* globals Services */
2 | import { storeCredentials, requestCredentials } from "./credentials.sys.js";
3 |
4 |
5 | export function waitForPromise(promise, defaultValue){
6 | let finished = false;
7 | let returnValue = defaultValue;
8 | promise.then(function(value){
9 | finished = true;
10 | returnValue = value;
11 | return returnValue;
12 | }).catch(function(){
13 | finished = true;
14 | });
15 |
16 | if (Services.tm.spinEventLoopUntilOrShutdown){
17 | Services.tm.spinEventLoopUntilOrShutdown(() => finished);
18 | }
19 | else if (Services.tm.spinEventLoopUntilOrQuit){
20 | Services.tm.spinEventLoopUntilOrQuit("keepassxc-mail:waitForPromise", () => finished);
21 | }
22 | else {
23 | console.error("Unable to wait for promise!");
24 | }
25 | return returnValue;
26 | }
27 |
28 | export function waitForCredentials(data){
29 | data.openChoiceDialog = true;
30 | return waitForPromise(requestCredentials(data), false);
31 | }
32 |
33 | export function waitForPasswordStore(data){
34 | return waitForPromise(storeCredentials(data), []).reduce(function(alreadyStored, stored){
35 | return alreadyStored || stored;
36 | }, false);
37 | }
--------------------------------------------------------------------------------
/modal/confirm/confirm.js:
--------------------------------------------------------------------------------
1 | /* globals resizeToContent, initModal*/
2 | "use strict";
3 |
4 | function fillText(message){
5 | document.querySelector("title").textContent = message.title;
6 | const textNode = document.querySelector(".question");
7 | let first = true;
8 | message.question.split(/\n/g).forEach(function(line){
9 | if (!first){
10 | textNode.appendChild(document.createElement("br"));
11 | }
12 | first = false;
13 | textNode.appendChild(document.createTextNode(line));
14 | });
15 | document.getElementById("yes").textContent = browser.i18n.getMessage("modal.confirm.yes");
16 | document.getElementById("no").textContent = browser.i18n.getMessage("modal.confirm.no");
17 | }
18 |
19 | initModal({messageCallback: function(message){
20 | return new Promise(function(resolve){
21 | fillText(message);
22 | document.querySelectorAll("button").forEach(function(button){
23 | button.disabled = false;
24 | button.addEventListener("click", function(){
25 | if (button.id === "yes"){
26 | resolve(true);
27 | window.close();
28 | }
29 | else if (button.id === "no"){
30 | resolve(false);
31 | window.close();
32 | }
33 | });
34 | });
35 | resizeToContent();
36 | });
37 | }});
--------------------------------------------------------------------------------
/modal/choice/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Save password for {host}?
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |

16 |
Multiple entries were found for {login} on {host}. Please select the correct one.
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "keepassxc-mail",
3 | "description": "MailExtension to talk to keepassxc",
4 | "version": "1.15",
5 | "icons": {
6 | "48": "icons/icon.svg",
7 | "96": "icons/icon.svg"
8 | },
9 | "background": {
10 | "scripts": [
11 | "from-keepassxc-browser/nacl.min.js",
12 | "from-keepassxc-browser/nacl-util.min.js",
13 | "global.js",
14 | "from-keepassxc-browser/client.js",
15 | "from-keepassxc-browser/keepass.js",
16 | "main.js"
17 | ]
18 | },
19 | "options_ui": {
20 | "browser_style": true,
21 | "page": "options/options.html"
22 | },
23 | "author": "Korbinian Kapsner",
24 | "permissions": [
25 | "nativeMessaging",
26 | "management",
27 | "storage",
28 | "https://api.github.com/"
29 | ],
30 | "experiment_apis": {
31 | "credentials": {
32 | "schema": "experiment/schema.json",
33 | "parent": {
34 | "scopes": [
35 | "addon_parent"
36 | ],
37 | "paths": [
38 | [
39 | "credentials"
40 | ]
41 | ],
42 | "script": "experiment/implementation.js"
43 | }
44 | }
45 | },
46 | "browser_specific_settings": {
47 | "gecko": {
48 | "id": "keepassxc-mail@kkapsner.de",
49 | "strict_min_version": "128.0",
50 | "strict_max_version": "148.*"
51 | }
52 | },
53 | "default_locale": "en",
54 | "manifest_version": 2
55 | }
--------------------------------------------------------------------------------
/from-keepassxc-browser/nacl-util.min.js:
--------------------------------------------------------------------------------
1 | !function(e,n){"use strict";"undefined"!=typeof module&&module.exports?module.exports=n():e.nacl?e.nacl.util=n():(e.nacl={},e.nacl.util=n())}(this,function(){"use strict";function e(e){if(!/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test(e))throw new TypeError("invalid encoding")}var n={};return n.decodeUTF8=function(e){if("string"!=typeof e)throw new TypeError("expected string");var n,r=unescape(encodeURIComponent(e)),t=new Uint8Array(r.length);for(n=0;n
2 |
3 |
4 | Save password for {host}?
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |

16 |
Do you want to save the entered password for {login} on {host}?
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/modules/nativeMessaging.js:
--------------------------------------------------------------------------------
1 | /* globals keepassClient, keepass, onDisconnected */
2 | import { log } from "./log.js";
3 |
4 | keepassClient.nativeHostName = "de.kkapsner.keepassxc_mail";
5 | export async function connect(forceOptionSearch){
6 | const savedNativeHostName = forceOptionSearch?
7 | false:
8 | (await browser.storage.local.get({nativeHostName: false})).nativeHostName;
9 | if (savedNativeHostName){
10 | keepassClient.nativeHostName = savedNativeHostName;
11 | log("Use saved native application", keepassClient.nativeHostName);
12 | if (await keepass.reconnect(null, 10000)){ // 10 second timeout for the first connect
13 | return true;
14 | }
15 | }
16 | else {
17 | const options = [
18 | "de.kkapsner.keepassxc_mail",
19 | "org.keepassxc.keepassxc_mail",
20 | "org.keepassxc.keepassxc_browser",
21 | ];
22 | for (let index = 0; index < options.length; index += 1){
23 | keepassClient.nativeHostName = options[index];
24 | log("Try native application", keepassClient.nativeHostName);
25 | if (await keepass.reconnect(null, 10000)){ // 10 second timeout for the first connect
26 | browser.storage.local.set({nativeHostName: keepassClient.nativeHostName});
27 | return true;
28 | }
29 | }
30 | }
31 | throw "Unable to connect to native messaging";
32 | }
33 |
34 | export async function disconnect(){
35 | if (keepassClient.nativePort){
36 | await keepassClient.nativePort.disconnect();
37 | onDisconnected();
38 | }
39 | }
--------------------------------------------------------------------------------
/modal/modalUtils.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const windowLoad = new Promise(function(resolve){
4 | window.addEventListener("load", resolve);
5 | });
6 |
7 | async function resizeToContent(){
8 | await windowLoad;
9 | const sizingNode = document.querySelector("body");
10 | await browser.windows.update(browser.windows.WINDOW_ID_CURRENT, {
11 | width: sizingNode.clientWidth + 10 + window.outerWidth - window.innerWidth,
12 | height: sizingNode.clientHeight + 10 + window.outerHeight - window.innerHeight
13 | });
14 | }
15 |
16 | function getMessage(name, replacements){
17 | const message = browser.i18n.getMessage(name) || name;
18 | if (!replacements){
19 | return message;
20 | }
21 | return message.replace(/\{\s*([^}]*?)\s*\}/g, function(m, key){
22 | const keysToTry = key.split(/\s*\|\s*/g);
23 | for (const key of keysToTry){
24 | if (key.match(/^["'].*["']$/)){
25 | return key.replace(/^['"]|['"]$/g, "");
26 | }
27 | if (replacements[key]){
28 | return replacements[key];
29 | }
30 | }
31 | return m;
32 | });
33 | }
34 |
35 | function initModal({messageCallback}){
36 | const port = browser.runtime.connect();
37 | port.onMessage.addListener(async function(message){
38 | if (message.type === "start"){
39 | const value = await messageCallback(message.message);
40 | port.postMessage({
41 | type: "response",
42 | value
43 | });
44 | }
45 | });
46 | window.addEventListener("keyup", function(event){
47 | if (event.key === "Escape"){
48 | window.close();
49 | }
50 | });
51 | }
--------------------------------------------------------------------------------
/experiment/modules/prompter/asyncPrompter.sys.js:
--------------------------------------------------------------------------------
1 | import { initPromptFunctions, registerPromptFunctions } from "./utils.sys.js";
2 | import { MsgAuthPrompt } from "resource:///modules/MsgAsyncPrompter.sys.mjs";
3 |
4 | const promptFunctions = [
5 | {
6 | name: "prompt",
7 | promptType: "promptPassword",
8 | titleIndex: 0,
9 | textIndex: 1,
10 | realmIndex: 2,
11 | passwordObjectIndex: 5,
12 | },
13 | {
14 | name: "promptUsernameAndPassword",
15 | promptType: "promptUserAndPass",
16 | titleIndex: 0,
17 | textIndex: 1,
18 | realmIndex: 2,
19 | passwordObjectIndex: 5,
20 | },
21 | {
22 | name: "promptPassword",
23 | promptType: "promptPassword",
24 | titleIndex: 0,
25 | textIndex: 1,
26 | realmIndex: 2,
27 | passwordObjectIndex: 4,
28 | },
29 | {
30 | name: "promptPassword2",
31 | promptType: "promptPassword",
32 | titleIndex: 0,
33 | textIndex: 1,
34 | passwordObjectIndex: 2,
35 | savePasswordIndex: 4,
36 | },
37 | {
38 | name: "promptAuth",
39 | promptType: "promptUserAndPass",
40 | dataFunction: function(args){
41 | return {
42 | host: `${args[0].URI.scheme}://${args[0].URI.host}`,
43 | login: args[2].username,
44 | };
45 | },
46 | // channelIndex: 0,
47 | // authInfoIndex: 2,
48 | setCredentials: function(args, username, password){
49 | args[2].username = username;
50 | args[2].password = password;
51 | },
52 | // passwordObjectIndex: 2,
53 | savePasswordIndex: 4,
54 | },
55 | ];
56 | initPromptFunctions(promptFunctions, MsgAuthPrompt.prototype, "MsgAuthPrompt");
57 | registerPromptFunctions(promptFunctions);
--------------------------------------------------------------------------------
/experiment/modules/wrappers/oauth2.sys.js:
--------------------------------------------------------------------------------
1 | /* globals Services */
2 | import { OAuth2 } from "resource:///modules/OAuth2.sys.mjs";
3 | import { getRefreshToken } from "./oauth2Module.sys.js";
4 | import { addSetup } from "../setup.sys.js";
5 |
6 | if (OAuth2.prototype.hasOwnProperty("connect")){
7 | const getUsername = function getUsername(oAuth){
8 | if (!oAuth){
9 | return false;
10 | }
11 | if (oAuth.username){
12 | return oAuth.username;
13 | }
14 | if (!Array.isArray(oAuth.extraAuthParams)){
15 | return false;
16 | }
17 | for (let i = 0; i + 1 < oAuth.extraAuthParams.length; i += 2){
18 | if (oAuth.extraAuthParams[i] === "login_hint"){
19 | return oAuth.extraAuthParams[i + 1];
20 | }
21 | }
22 | return false;
23 | };
24 | const updateRefreshToken = function updateRefreshToken(oAuth){
25 | const username = getUsername(oAuth);
26 | if (!username){
27 | return false;
28 | }
29 | const authorizationEndpointURL = Services.io.newURI(oAuth.authorizationEndpoint);
30 | const refreshToken = getRefreshToken({
31 | _username: username,
32 | _loginOrigin: "oauth://" + authorizationEndpointURL.host
33 | });
34 | if (refreshToken !== null){
35 | oAuth.refreshToken = refreshToken;
36 | return true;
37 | }
38 | return false;
39 | };
40 |
41 | const originalConnect = OAuth2.prototype.connect;
42 | const alteredConnect = async function(...args){
43 | if (!this.refreshToken){
44 | updateRefreshToken(this);
45 | }
46 | return originalConnect.call(this, ...args);
47 | };
48 | addSetup({
49 | setup: function(){
50 | OAuth2.prototype.connect = alteredConnect;
51 | },
52 | shutdown: function(){
53 | OAuth2.prototype.connect = originalConnect;
54 | }
55 | });
56 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Description
4 |
5 |
6 |
7 | ## Expected Behaviour
8 |
9 |
10 |
11 | ## Current Behaviour
12 |
13 |
14 |
15 | ## Possible Solution
16 |
17 |
18 |
19 | ## Steps to Reproduce (for bugs)
20 |
21 |
22 |
23 |
24 | 1. create a fresh Thunderbird profile
25 | 2.
26 | 3.
27 | 4.
28 |
29 | ## Context
30 |
31 |
32 |
33 |
34 | ## Your Environment
35 |
36 | * KeePassXC-mail version used:
37 | * KeePassXC version:
38 | * Thunderbird version:
39 | * Operating system and version:
40 |
--------------------------------------------------------------------------------
/global.js:
--------------------------------------------------------------------------------
1 | /* globals page */
2 | "use strict";
3 |
4 | const EXTENSION_NAME = "KeePassXC-Mail";
5 |
6 | const AssociatedAction = {
7 | NOT_ASSOCIATED: 0,
8 | ASSOCIATED: 1,
9 | NEW_ASSOCIATION: 2,
10 | CANCELED: 3
11 | };
12 |
13 | function tr(key, params) {
14 | return browser.i18n.getMessage(key, params);
15 | }
16 |
17 | // Returns file name and line number from error stack
18 | const getFileAndLine = function() {
19 | const err = new Error().stack.split("\n");
20 | const line = err[4] ?? err[err.length - 1];
21 | const result = line.substring(line.lastIndexOf("/") + 1, line.lastIndexOf(":"));
22 |
23 | return result;
24 | };
25 |
26 | const debugLogMessage = function(message, extra) {
27 | console.log(`[Debug ${getFileAndLine()}] ${EXTENSION_NAME} - ${message}`);
28 |
29 | if (extra) {
30 | console.log(extra);
31 | }
32 | };
33 |
34 | const logDebug = function(message, extra) {
35 | if (page.settings.debugLogging) {
36 | debugLogMessage(message, extra);
37 | }
38 | };
39 |
40 | const logError = function(message) {
41 | console.log(`[Error ${getFileAndLine()}] ${EXTENSION_NAME} - ${message}`);
42 | };
43 |
44 | const compareVersion = function(minimum, current, canBeEqual = true) {
45 | if (!minimum || !current || minimum?.indexOf(".") === -1 || current?.indexOf(".") === -1) {
46 | return false;
47 | }
48 |
49 | // Handle beta/snapshot builds as stable version
50 | const snapshot = "-snapshot";
51 | const beta = "-beta";
52 | if (current.endsWith(snapshot)) {
53 | current = current.slice(0, -snapshot.length);
54 | }
55 |
56 | if (current.endsWith(beta)) {
57 | current = current.slice(0, -beta.length);
58 | }
59 |
60 | const min = minimum.split(".", 3).map(s => s.padStart(4, "0")).join(".");
61 | const cur = current.split(".", 3).map(s => s.padStart(4, "0")).join(".");
62 | return (canBeEqual ? (min <= cur) : (min < cur));
63 | };
--------------------------------------------------------------------------------
/modal/choice/choice.js:
--------------------------------------------------------------------------------
1 | /* globals getMessage, resizeToContent, initModal*/
2 | "use strict";
3 |
4 | function fillText(message){
5 | document.querySelector("title").textContent = getMessage("modal.choice.title", message);
6 | document.querySelector(".text").textContent = getMessage(
7 | message.login && message.login !== true?
8 | "modal.choice.textWithLogin":
9 | "modal.choice.textWithoutLogin",
10 | message
11 | );
12 | document.querySelector(".doNotAskAgainText").textContent = browser.i18n.getMessage("modal.choice.doNotAskAgain");
13 | document.getElementById("ok").textContent = browser.i18n.getMessage("modal.choice.ok");
14 | document.getElementById("cancel").textContent = browser.i18n.getMessage("modal.choice.cancel");
15 | }
16 |
17 | function fillSelect(message, sendAnswer){
18 | const select = document.getElementById("entries");
19 | message.entries.forEach(function(entry){
20 | const option = new Option(getMessage("entryLabel", entry), entry.uuid);
21 | option.entry = entry;
22 | select.appendChild(
23 | option
24 | );
25 | });
26 | select.addEventListener("change", function(){
27 | const selectedOption = select.options[select.selectedIndex];
28 | if (selectedOption?.entry?.autoSubmit){
29 | sendAnswer();
30 | }
31 | });
32 | }
33 |
34 | initModal({messageCallback: function(message){
35 | return new Promise(function(resolve){
36 | function sendAnswer(){
37 | resolve({
38 | selectedUuid: document.getElementById("entries").value,
39 | doNotAskAgain: document.getElementById("doNotAskAgain").checked
40 | });
41 | window.close();
42 | }
43 | fillText(message);
44 | fillSelect(message, sendAnswer);
45 | document.querySelectorAll("button").forEach(function(button){
46 | button.disabled = false;
47 | button.addEventListener("click", function(){
48 | if (button.id === "ok"){
49 | sendAnswer();
50 | }
51 | else {
52 | resolve({selectedUuid: false, doNotAskAgain: document.getElementById("doNotAskAgain").checked});
53 | window.close();
54 | }
55 | });
56 | });
57 | resizeToContent();
58 | });
59 | }});
--------------------------------------------------------------------------------
/experiment/modules/wrappers/cal.sys.js:
--------------------------------------------------------------------------------
1 |
2 | import { cal } from "resource:///modules/calendar/calUtils.sys.mjs";
3 | import { waitForCredentials } from "../wait.sys.js";
4 | import { storeCredentials } from "../credentials.sys.js";
5 | import { addSetup } from "../setup.sys.js";
6 |
7 | const originalPasswordManagerGet = cal.auth.passwordManagerGet;
8 | const originalPasswordManagerSave = cal.auth.passwordManagerSave;
9 |
10 | const getAuthCredentialInfo = function getAuthCredentialInfo(login, host){
11 | return {
12 | login: login,
13 | host: host.replace(/^oauth:([^/]{2})/, "oauth://$1")
14 | };
15 | };
16 | const changePasswordManager = function changePasswordManager(){
17 | cal.auth.passwordManagerGet = function(login, passwordObject, host, realm){
18 | if (host.startsWith("oauth:")){
19 | const credentialDetails = waitForCredentials(getAuthCredentialInfo(login, host));
20 | if (
21 | credentialDetails &&
22 | credentialDetails.credentials.length &&
23 | (typeof credentialDetails.credentials[0].password) === "string"
24 | ){
25 | passwordObject.value = credentialDetails.credentials[0].password;
26 | return true;
27 | }
28 | }
29 | return originalPasswordManagerGet.call(this, login, passwordObject, host, realm);
30 | };
31 | cal.auth.passwordManagerSave = function(login, password, host, realm){
32 | if (host.startsWith("oauth:")){
33 | const credentialInfo = getAuthCredentialInfo(login, host);
34 | credentialInfo.password = password;
35 | credentialInfo.callback = (stored) => {
36 | if (!stored){
37 | originalPasswordManagerSave.call(this, login, password, host, realm);
38 | }
39 | };
40 | storeCredentials(credentialInfo);
41 | return false;
42 | }
43 |
44 | return originalPasswordManagerSave.call(this, login, password, host, realm);
45 | };
46 | };
47 | const restorePasswordManager = function restorePasswordManager(){
48 | cal.auth.passwordManagerGet = originalPasswordManagerGet;
49 | cal.auth.passwordManagerSave = originalPasswordManagerSave;
50 | };
51 | addSetup({
52 | setup: changePasswordManager,
53 | shutdown: restorePasswordManager
54 | });
--------------------------------------------------------------------------------
/modal/savingPassword/savingPassword.js:
--------------------------------------------------------------------------------
1 | /* globals getMessage, resizeToContent, initModal */
2 | "use strict";
3 |
4 | function fillText(message){
5 | document.querySelector("title").textContent = getMessage("modal.savingPassword.title", message);
6 | document.querySelector(".question").textContent = getMessage(
7 | message.login && message.login !== true?
8 | "modal.savingPassword.questionWithLogin":
9 | "modal.savingPassword.questionWithoutLogin",
10 | message
11 | );
12 | document.querySelector(".doNotAskAgainText").textContent = browser.i18n.getMessage("modal.choice.doNotAskAgain");
13 | document.getElementById("createNewEntry").textContent = browser.i18n.getMessage("modal.savingPassword.newEntry");
14 | document.getElementById("yes").textContent = browser.i18n.getMessage("modal.savingPassword.yes");
15 | document.getElementById("no").textContent = browser.i18n.getMessage("modal.savingPassword.no");
16 | resizeToContent();
17 | }
18 | function fillSelect(message){
19 | if (!message.entries?.length){
20 | return;
21 | }
22 | const select = document.getElementById("entries");
23 | select.classList.remove("hidden");
24 | message.entries.forEach(function(entry){
25 | const option = new Option(getMessage("entryLabel", entry), entry.uuid);
26 | select.appendChild(option);
27 | option.selected = entry.preselected;
28 | });
29 | }
30 |
31 | initModal({messageCallback: function(message){
32 | fillText(message);
33 | fillSelect(message);
34 | return new Promise(function(resolve){
35 | document.querySelectorAll("button").forEach(function(button){
36 | button.disabled = false;
37 | button.addEventListener("click", function(){
38 | if (button.id === "yes"){
39 | resolve({
40 | save: true,
41 | uuid: document.getElementById("entries").value || null,
42 | doNotAskAgain: document.getElementById("doNotAskAgain").checked
43 | });
44 | }
45 | else {
46 | resolve({
47 | save: false,
48 | uuid: null,
49 | doNotAskAgain: document.getElementById("doNotAskAgain").checked
50 | });
51 | }
52 | window.close();
53 | });
54 | });
55 | });
56 | }});
--------------------------------------------------------------------------------
/experiment/modules/gui/windowListener.sys.js:
--------------------------------------------------------------------------------
1 |
2 | import { ExtensionSupport } from "resource:///modules/ExtensionSupport.sys.mjs";
3 | import { addSetup } from "../setup.sys.js";
4 | import { passwordEmitter } from "../emitters.sys.js";
5 | import { buildDialogGui, updateGUI } from "./utils.sys.js";
6 | import { requestCredentials } from "../credentials.sys.js";
7 |
8 | const windowListeners = [];
9 |
10 | export function addWindowListener(data){
11 | if (!data){
12 | return;
13 | }
14 | windowListeners.push(data);
15 | }
16 |
17 |
18 | function registerWindowListener(){
19 | async function handleEvent(guiOperations, credentialInfo){
20 | if (guiOperations.doHandle && !(await guiOperations.doHandle())){
21 | return;
22 | }
23 | buildDialogGui(guiOperations, credentialInfo);
24 | const credentialDetails = await requestCredentials(credentialInfo);
25 | updateGUI(guiOperations, credentialInfo, credentialDetails);
26 | if (guiOperations.registerOnSubmit){
27 | guiOperations.registerOnSubmit(function(login, password){
28 | if (
29 | credentialInfo.login &&
30 | !credentialInfo.loginChangeable
31 | ){
32 | login = credentialInfo.login;
33 | }
34 | if (!credentialDetails.credentials.some(function(credentials){
35 | return login === credentials.login && password === credentials.password;
36 | })){
37 | passwordEmitter.emit("password", {
38 | login,
39 | password,
40 | host: credentialInfo.host
41 | });
42 | }
43 | });
44 | }
45 | }
46 |
47 | windowListeners.forEach(function(listener){
48 | ExtensionSupport.registerWindowListener(listener.name, {
49 | chromeURLs: listener.chromeURLs,
50 | onLoadWindow: function(window){
51 | const credentialInfo = listener.getCredentialInfo(window);
52 | if (credentialInfo){
53 | handleEvent(listener.getGuiOperations(window), credentialInfo);
54 | }
55 | },
56 | });
57 | });
58 | }
59 | function unregisterWindowListener(){
60 | windowListeners.forEach(function(listener){
61 | ExtensionSupport.unregisterWindowListener(listener.name);
62 | });
63 | }
64 | addSetup({
65 | setup: registerWindowListener,
66 | shutdown: unregisterWindowListener
67 | });
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | /* globals keepass, keepassClient, onDisconnected */
2 | "use strict";
3 |
4 | const page = {
5 | tabs: [],
6 | clearCredentials: () => {},
7 | clearAllLogins: () => {},
8 | settings: {
9 | autoReconnect: true,
10 | checkUpdateKeePassXC: 0
11 | }
12 | };
13 |
14 | const browserAction = {
15 | show: () => {},
16 | showDefault: () => {}
17 | };
18 |
19 | // enable access to keepass object in option page
20 | window.keepass = keepass;
21 | const isKeepassReady = function(){
22 | const keepassModule = import("./modules/keepass.js");
23 | return async function(){
24 | const { isReady } = await keepassModule;
25 | return isReady();
26 | };
27 | }();
28 | window.isKeepassReady = isKeepassReady;
29 |
30 | import("./modules/externalRequests.js");
31 |
32 | window.selectedModule = import("./modules/selected.js");
33 |
34 | const getCredentialsModule = import("./modules/getCredentials.js");
35 | browser.credentials.onCredentialRequested.addListener(async function(credentialInfo){
36 | const { getCredentials } = await getCredentialsModule;
37 | return await getCredentials(credentialInfo);
38 | });
39 |
40 |
41 | const storeCredentialsModule = import("./modules/storeCredentials.js");
42 | browser.credentials.onNewCredential.addListener(async function(credentialInfo){
43 | const { storeCredentials } = await storeCredentialsModule;
44 | return await storeCredentials(credentialInfo);
45 | });
46 |
47 | browser.credentials.getThunderbirdSavedLoginsStatus().then(async function(status){
48 | if (status.count === 0){
49 | return false;
50 | }
51 | const currentSeenPasswordChange = Math.max(status.latestTimeCreated, status.latestTimePasswordChanged);
52 | const { lastSeenPasswordChange } = await browser.storage.local.get({lastSeenPasswordChange: -1});
53 | browser.storage.local.set({lastSeenPasswordChange: currentSeenPasswordChange});
54 | if (lastSeenPasswordChange >= currentSeenPasswordChange){
55 | return false;
56 | }
57 | (await import("./modules/modal.js")).messageModal(
58 | browser.i18n.getMessage("passwordsStoredInThunderbird.title"),
59 | lastSeenPasswordChange === -1?
60 | browser.i18n.getMessage("passwordsStoredInThunderbird.message"):
61 | browser.i18n.getMessage("passwordsStoredInThunderbird.newStored")
62 | );
63 | return true;
64 | }).catch(error => {});
65 |
--------------------------------------------------------------------------------
/experiment/modules/prompter/loginManagerAuthPrompter.sys.js:
--------------------------------------------------------------------------------
1 | /* globals Services, Ci */
2 | import { initPromptFunctions, registerPromptFunctions } from "./utils.sys.js";
3 | import { LoginManagerAuthPrompter } from "resource://gre/modules/LoginManagerAuthPrompter.sys.mjs";
4 | import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
5 |
6 | const promptFunctions = [
7 | {
8 | name: "asyncPromptPassword",
9 | isAsync: true,
10 | promptType: "promptPassword",
11 | titleIndex: 0,
12 | textIndex: 1,
13 | realmIndex: 2,
14 | savePasswordIndex: 3,
15 | savePasswordValue: Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER,
16 | passwordObjectIndex: 4,
17 | createReturnValue: Services.vc.compare(AppConstants.MOZ_APP_VERSION, "148.0a1") >= 0 && function(credentials){
18 | return {
19 | ok: !!credentials,
20 | password: credentials? credentials.password: ""
21 | };
22 | },
23 | },
24 | {
25 | name: "asyncPromptUsernameAndPassword",
26 | isAsync: true,
27 | promptType: "promptUserAndPass",
28 | titleIndex: 0,
29 | textIndex: 1,
30 | realmIndex: 2,
31 | savePasswordIndex: 3,
32 | savePasswordValue: Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER,
33 | usernameObjectIndex: 4,
34 | passwordObjectIndex: 5,
35 | createReturnValue: Services.vc.compare(AppConstants.MOZ_APP_VERSION, "148.0a1") >= 0 && function(credentials){
36 | return {
37 | ok: !!credentials,
38 | username: credentials? credentials.login: "",
39 | password: credentials? credentials.password: ""
40 | };
41 | },
42 | },
43 | {
44 | name: "prompt",
45 | promptType: "promptPassword",
46 | titleIndex: 0,
47 | textIndex: 1,
48 | realmIndex: 2,
49 | savePasswordIndex: 3,
50 | savePasswordValue: Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER,
51 | passwordObjectIndex: 5,
52 | },
53 | {
54 | name: "promptPassword",
55 | promptType: "promptPassword",
56 | titleIndex: 0,
57 | textIndex: 1,
58 | realmIndex: 2,
59 | passwordObjectIndex: 4,
60 | },
61 | {
62 | name: "promptUsernameAndPassword",
63 | promptType: "promptUserAndPass",
64 | titleIndex: 0,
65 | textIndex: 1,
66 | realmIndex: 2,
67 | passwordObjectIndex: 5,
68 | },
69 | ];
70 | initPromptFunctions(promptFunctions, LoginManagerAuthPrompter.prototype, "LoginManagerAuthPrompter");
71 | registerPromptFunctions(promptFunctions);
--------------------------------------------------------------------------------
/experiment/schema.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "namespace": "credentials",
4 | "functions": [
5 | {
6 | "name": "getThunderbirdSavedLoginsStatus",
7 | "description": "Returns some status information about credentials that are stored in the Thunderbird password vault",
8 | "type": "function",
9 | "async": true,
10 | "parameters": []
11 | }
12 | ],
13 | "events": [
14 | {
15 | "name": "onCredentialRequested",
16 | "description": "Fires when credentials are requested",
17 | "type": "function",
18 | "parameters": [{
19 | "name": "credentialInformation",
20 | "description": "Information about the requested credentials",
21 | "type": "object",
22 | "properties": {
23 | "host": {
24 | "description": "The host including protocol for which credentials are requested",
25 | "type": "string"
26 | },
27 | "login": {
28 | "description": "The login for which credentials are requested",
29 | "optional": true,
30 | "type": "string"
31 | },
32 | "loginChangeable": {
33 | "description": "If the login is changeable in the password dialog",
34 | "optional": true,
35 | "type": "boolean"
36 | },
37 | "openChoiceDialog": {
38 | "description": "If the choice dialog should be displayed if more than one entry is found or auto submit is disabled",
39 | "optional": true,
40 | "type": "boolean"
41 | }
42 | }
43 | }]
44 | },
45 | {
46 | "name": "onNewCredential",
47 | "description": "Fires when new credentials are entered",
48 | "type": "function",
49 | "parameters": [{
50 | "name": "credentialInformation",
51 | "description": "Information about the entered credentials",
52 | "type": "object",
53 | "properties": {
54 | "host": {
55 | "description": "The host including protocol for which credentials were entered.",
56 | "type": "string"
57 | },
58 | "login": {
59 | "description": "The login for which credentials were entered.",
60 | "optional": true,
61 | "type": "string"
62 | },
63 | "password": {
64 | "description": "The password that was entered.",
65 | "optional": true,
66 | "type": "string"
67 | }
68 | }
69 | }]
70 | }
71 | ]
72 | }
73 | ]
--------------------------------------------------------------------------------
/experiment/modules/gui/commonDialog.sys.js:
--------------------------------------------------------------------------------
1 | import { getCredentialInfoFromStrings } from "../dialogStrings.sys.js";
2 | import { addWindowListener } from "./windowListener.sys.js";
3 |
4 | function getCredentialInfo(window){
5 | const promptType = window.args.promptType;
6 | if (["promptPassword", "promptUserAndPass"].indexOf(promptType) === -1){
7 | return false;
8 | }
9 |
10 | const promptData = getCredentialInfoFromStrings(window.args.title, window.args.text);
11 | if (promptData){
12 | const host = promptData.host;
13 | let login = promptData.login;
14 | let loginChangeable = false;
15 | if (!login && promptType === "promptUserAndPass"){
16 | const loginInput = window.document.getElementById("loginTextbox");
17 | if (loginInput && loginInput.value){
18 | login = loginInput.value;
19 | }
20 | loginChangeable = true;
21 | }
22 | return {host, login, loginChangeable};
23 | }
24 | return false;
25 | }
26 |
27 | const getGuiOperations = function(){
28 | return function(window){
29 | const document = window.document;
30 | const commonDialog = document.getElementById("commonDialog");
31 | return {
32 | window,
33 | guiParent: commonDialog,
34 | submit: function(){
35 | commonDialog._buttons.accept.click();
36 | },
37 | registerOnSubmit: function(callback){
38 | let submitted = false;
39 | function submit(){
40 | if (!submitted){
41 | submitted = true;
42 | callback(
43 | document.getElementById("loginTextbox").value,
44 | document.getElementById("password1Textbox").value
45 | );
46 | }
47 | }
48 | commonDialog._buttons.accept.addEventListener("command", submit);
49 | commonDialog.addEventListener("dialogaccept", submit);
50 | },
51 | fillCredentials: function(credentialInfo, credentials){
52 | if (
53 | !credentialInfo.login ||
54 | credentialInfo.loginChangeable
55 | ){
56 | document.getElementById("loginTextbox").value = credentials.login;
57 | }
58 | document.getElementById("password1Textbox").value = credentials.password;
59 | }
60 | };
61 | };
62 | }();
63 | addWindowListener({
64 | name: "passwordDialogListener",
65 | chromeURLs: [
66 | "chrome://global/content/commonDialog.xul",
67 | "chrome://global/content/commonDialog.xhtml",
68 | ],
69 | getCredentialInfo,
70 | getGuiOperations
71 | });
--------------------------------------------------------------------------------
/experiment/modules/prompter/prompter.sys.js:
--------------------------------------------------------------------------------
1 | import { initPromptFunctions, registerPromptFunctions } from "./utils.sys.js";
2 | import { Prompter } from "resource://gre/modules/Prompter.sys.mjs";
3 | const promptFunctions = [
4 | {
5 | name: "promptUsernameAndPassword",
6 | promptType: "promptUserAndPass",
7 | titleIndex: 1,
8 | textIndex: 2,
9 | passwordObjectIndex: 4,
10 | },
11 | {
12 | name: "promptUsernameAndPasswordBC",
13 | promptType: "promptUserAndPass",
14 | titleIndex: 2,
15 | textIndex: 3,
16 | passwordObjectIndex: 5,
17 | },
18 | {
19 | name: "asyncPromptUsernameAndPassword",
20 | isAsync: true,
21 | promptType: "promptUserAndPass",
22 | titleIndex: 2,
23 | textIndex: 3,
24 | passwordObjectIndex: 5,
25 | createReturnValue: function(credentials){
26 | return {
27 | ok: !!credentials,
28 | user: credentials? credentials.login: "",
29 | pass: credentials? credentials.password: ""
30 | };
31 | },
32 | },
33 | {
34 | name: "promptPassword",
35 | promptType: "promptPassword",
36 | titleIndex: 1,
37 | textIndex: 2,
38 | passwordObjectIndex: 3,
39 | },
40 | {
41 | name: "promptPasswordBC",
42 | promptType: "promptPassword",
43 | titleIndex: 2,
44 | textIndex: 3,
45 | passwordObjectIndex: 4,
46 | },
47 | {
48 | name: "asyncPromptPassword",
49 | isAsync: true,
50 | promptType: "promptPassword",
51 | titleIndex: 2,
52 | textIndex: 3,
53 | passwordObjectIndex: 4,
54 | createReturnValue: function(credentials){
55 | return {
56 | ok: !!credentials,
57 | password: credentials? credentials.password: ""
58 | };
59 | },
60 | },
61 | {
62 | name: "promptAuth",
63 | promptType: "promptUserAndPass",
64 | channelIndex: 1,
65 | authInfoIndex: 3,
66 | passwordObjectIndex: 3,
67 | },
68 | {
69 | name: "promptAuthBC",
70 | promptType: "promptUserAndPass",
71 | channelIndex: 2,
72 | authInfoIndex: 4,
73 | passwordObjectIndex: 4,
74 | },
75 | {
76 | name: "asyncPromptAuth",
77 | promptType: "promptUserAndPass",
78 | channelIndex: 1,
79 | authInfoIndex: 5,
80 | passwordObjectIndex: 5,
81 | },
82 | {
83 | name: "asyncPromptAuthBC",
84 | promptType: "promptUserAndPass",
85 | channelIndex: 2,
86 | authInfoIndex: 6,
87 | passwordObjectIndex: 6,
88 | },
89 | ];
90 | initPromptFunctions(promptFunctions, Prompter.prototype, "Prompter");
91 | registerPromptFunctions(promptFunctions);
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "webextensions": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 2022,
9 | "ecmaFeatures": {
10 | "jsx": true
11 | },
12 | "sourceType": "script"
13 | },
14 | "plugins": [
15 | "promise",
16 | "eslint-comments",
17 | "html"
18 | ],
19 | "extends": [
20 | "eslint:recommended",
21 | "plugin:promise/recommended",
22 | "plugin:eslint-comments/recommended"
23 | ],
24 | "globals": {
25 | "exportFunction": false
26 | },
27 | "rules": {
28 | "brace-style": ["error", "stroustrup", {"allowSingleLine": true}],
29 | "comma-spacing": ["error", { "before": false, "after": true }],
30 | "complexity": ["warn", 20],
31 | "consistent-return": "error",
32 | "constructor-super": "warn",
33 | "eqeqeq": "error",
34 | "eslint-comments/no-use": ["error", {"allow": ["globals"]}],
35 | "indent": ["error", "tab", {"SwitchCase": 1}],
36 | "max-depth": ["warn", 4],
37 | "max-len": ["warn", {"code": 120, "tabWidth": 4}],
38 | "max-lines-per-function": ["warn", {"max": 80,"skipBlankLines": true, "skipComments": true}],
39 | "max-lines": ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}],
40 | "max-params": ["warn", 4],
41 | "no-console": "off",
42 | "no-const-assign": "error",
43 | "no-inner-declarations": "warn",
44 | "no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
45 | "no-prototype-builtins": "off",
46 | "no-this-before-super": "warn",
47 | "no-trailing-spaces": ["error", {"skipBlankLines": true}],
48 | "no-undef": "error",
49 | "no-unreachable": "warn",
50 | "no-unused-vars": "off",
51 | "no-use-before-define": ["error", {"functions": false}],
52 | "no-useless-rename": "warn",
53 | "no-useless-return": "warn",
54 | "no-var": "error",
55 | "quotes": ["error", "double"],
56 | "require-atomic-updates": "off",
57 | "semi": ["error", "always"],
58 | "space-in-parens": ["error", "never"],
59 | "strict": ["error", "global"],
60 | "valid-typeof": "warn"
61 | },
62 | "overrides": [
63 | {
64 | "files": ["test/*"],
65 | "rules": {
66 | "no-console": "off"
67 | }
68 | },
69 | {
70 | "files": ["**/modules/**/*.js"],
71 | "parserOptions": {
72 | "sourceType": "module"
73 | }
74 | },
75 | {
76 | "files": [".tools/*.js"],
77 | "env": {
78 | "node": true
79 | },
80 | "rules": {
81 | "no-console": "off"
82 | }
83 | }
84 | ]
85 | }
--------------------------------------------------------------------------------
/modules/getCredentials.js:
--------------------------------------------------------------------------------
1 | import { log } from "./log.js";
2 | import { isReady as isKeepassReady, keepass, handleLockedDatabase } from "./keepass.js";
3 | import { choiceModal } from "./modal.js";
4 |
5 | const lastRequest = {};
6 | export async function getCredentials(credentialInfo){
7 | log("got credential request:", credentialInfo);
8 | await isKeepassReady();
9 | const presentIds = new Map();
10 | let credentialsForHost = (await keepass.retrieveCredentials(false, [credentialInfo.host, credentialInfo.host]))
11 | .filter(function(credentials){
12 | const alreadyPresent = presentIds.has(credentials.uuid);
13 | if (alreadyPresent){
14 | return false;
15 | }
16 | presentIds.set(credentials.uuid, true);
17 | return true;
18 | })
19 | .filter(function(credential){
20 | return (credentialInfo.login || !credentialInfo.loginChangeable)?
21 | (
22 | credentialInfo.login === true ||
23 | credential.login.toLowerCase?.() === credentialInfo.login.toLowerCase?.()
24 | ):
25 | credential.login;
26 | }).map(function(credential){
27 | credential.skipAutoSubmit = credential.skipAutoSubmit === "true";
28 | return credential;
29 | });
30 | if (keepass.isDatabaseClosed && await handleLockedDatabase()){
31 | return getCredentials(credentialInfo);
32 | }
33 | log("keepassXC provided", credentialsForHost.length, "logins");
34 | let autoSubmit = (await browser.storage.local.get({autoSubmit: false})).autoSubmit;
35 | if (autoSubmit){
36 | const requestId = credentialInfo.login + "|" + credentialInfo.host;
37 | const now = Date.now();
38 | if (now - lastRequest[requestId] < 1000){
39 | autoSubmit = false;
40 | }
41 | lastRequest[requestId] = now;
42 | }
43 |
44 | if (
45 | credentialInfo.openChoiceDialog &&
46 | credentialsForHost.length
47 | ){
48 | const selectedUuid = await choiceModal(
49 | credentialInfo.host,
50 | credentialInfo.login,
51 | credentialsForHost.map(function (data){
52 | return {
53 | name: data.name,
54 | login: data.login,
55 | uuid: data.uuid,
56 | autoSubmit: autoSubmit && !data.skipAutoSubmit
57 | };
58 | })
59 | );
60 | const filteredCredentialsForHost = credentialsForHost.filter(e => e.uuid === selectedUuid);
61 | if (!selectedUuid || filteredCredentialsForHost.length){
62 | credentialsForHost = filteredCredentialsForHost;
63 | }
64 | }
65 |
66 | return {
67 | autoSubmit,
68 | credentials: credentialsForHost
69 | };
70 | }
--------------------------------------------------------------------------------
/.tools/translate.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const util = require("util");
4 | const en = require("../_locales/en/messages.json");
5 | const enKeys = Object.keys(en);
6 |
7 | const language = process.argv[2];
8 |
9 |
10 | function getTranslationPath(language){
11 | "use strict";
12 |
13 | return path.join(__dirname, "../_locales/" + language + "/messages.json");
14 | }
15 | async function loadTranslation(language){
16 | "use strict";
17 |
18 | const path = getTranslationPath(language);
19 | const exists = await util.promisify(fs.exists)(path);
20 | if (exists){
21 | console.log("language exists -> load data");
22 | const data = await util.promisify(fs.readFile)(path, {encoding: "UTF-8"});
23 | return JSON.parse(data);
24 | }
25 | else {
26 | console.log("language does not exist -> create it");
27 | return {};
28 | }
29 | }
30 |
31 | async function saveTranslation(language, data){
32 | "use strict";
33 |
34 | const path = getTranslationPath(language);
35 | return await util.promisify(fs.writeFile)(path, JSON.stringify(data, null, "\t"));
36 | }
37 |
38 | async function getInput(prompt){
39 | "use strict";
40 |
41 | return new Promise(function(resolve){
42 | process.stdout.write(prompt);
43 | process.stdin.setEncoding("utf8");
44 | process.stdin.resume();
45 | process.stdin.on("data", function onData(data){
46 | process.stdin.removeListener("data", onData);
47 | process.stdin.pause();
48 | resolve(data.replace(/[\n\r]+$/, ""));
49 | });
50 | });
51 | }
52 |
53 | async function askForTranslation(key){
54 | "use strict";
55 |
56 | const enData = en[key];
57 | console.log("English translation for", key, ":", enData.message);
58 | if (enData.description){
59 | console.log("\nDescription:", enData.description);
60 | }
61 | return getInput("Please enter translation: ");
62 | }
63 |
64 | async function translate(language){
65 | "use strict";
66 |
67 | const originalData = await loadTranslation(language);
68 | const data = {};
69 | for (let i = 0; i < enKeys.length; i += 1){
70 | const key = enKeys[i];
71 | const oldData = originalData[key];
72 | const enData = en[key];
73 | if (oldData && oldData.message && oldData.message.trim()){
74 | data[key] = oldData;
75 | }
76 | else {
77 | data[key] = {
78 | message: enData.message.trim() === ""? "": await askForTranslation(key),
79 | description: (oldData && oldData.description) || enData.description || enData.message
80 | };
81 | }
82 | }
83 | return data;
84 | }
85 |
86 | (async function(){
87 | "use strict";
88 |
89 | const data = await translate(language);
90 |
91 | saveTranslation(language, data);
92 | }());
--------------------------------------------------------------------------------
/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | KeePassXC-Mail settings
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | KeePassXC-Mail settings
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | External privileges
21 |
22 |
23 |
24 | | extension |
25 | request |
26 | store |
27 | |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Database connections
35 |
36 |
37 |
38 |
39 |
40 | | ID |
41 | DB hash |
42 | key |
43 | last used |
44 | created |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/modules/externalRequests.js:
--------------------------------------------------------------------------------
1 | import { log } from "./log.js";
2 | import { confirmModal } from "./modal.js";
3 | import { getCredentials } from "./getCredentials.js";
4 | import { storeCredentials } from "./storeCredentials.js";
5 | import { getPrivileges, setPrivileges } from "./externalPrivileges.js";
6 |
7 | const messageTypes = {
8 | test: {
9 | neededPrivileges: [],
10 | needsData: false,
11 | callback: data => "Yes, KPM is installed and responding",
12 | },
13 | "request-credentials": {
14 | neededPrivileges: ["request"],
15 | needsData: true,
16 | callback: async data => {
17 | log("requesting credentials", data);
18 | const credentialData = await getCredentials({
19 | host: data.host,
20 | login: data.login,
21 | loginChangeable: data.loginChangeable
22 | });
23 | return credentialData.credentials.map(credentials => {
24 | return {
25 | login: credentialData.login,
26 | password: credentials.password,
27 | };
28 | });
29 | },
30 | },
31 | "store-credentials": {
32 | neededPrivileges: ["store"],
33 | needsData: true,
34 | callback: async data => {
35 | log("storing credentials", data);
36 | return storeCredentials({host: data.host, login: data.login, password: data.password});
37 | },
38 | },
39 | };
40 |
41 | async function extensionHasPrivilege(extensionData, privilege){
42 | log("Checking if", extensionData.id, "has the privilege", privilege);
43 | const privileges = await getPrivileges(extensionData.id);
44 | if ("boolean" === typeof privileges[privilege]){
45 | return privileges[privilege];
46 | }
47 |
48 | const extension = await browser.management.get(extensionData.id);
49 | const decision = await confirmModal(
50 | browser.i18n.getMessage("privilegeRequest.title"),
51 | browser.i18n.getMessage("privilegeRequest.message")
52 | .replace("{typeMessage}", browser.i18n.getMessage(`privilegeRequest.message.${privilege}`))
53 | .replace("{extensionName}", extension.name)
54 | );
55 | setPrivileges(extensionData.id, privilege, decision);
56 | return decision;
57 | }
58 |
59 | async function extensionHasAllPrivileges(extension, privileges){
60 | return (await Promise.all(privileges.map(privilege => extensionHasPrivilege(extension, privilege)))).every(a => a);
61 | }
62 |
63 | browser.runtime.onMessageExternal.addListener(async function(message, extension){
64 | if (!extension || !extension.id){
65 | log("External extension not verifiable:", extension);
66 | }
67 | const messageType = messageTypes[message.type];
68 | if (!messageType){
69 | log("Invalid external message", message, "from", extension.id);
70 | return null;
71 | }
72 | if (messageType.needsData && !message.data){
73 | log("Needed data for", message.type, "from", extension.id, "not provided:", message);
74 | return null;
75 | }
76 | if (!(await extensionHasAllPrivileges(extension, messageType.neededPrivileges))){
77 | log(extension.id, "does not have the privileges to perform", message.type);
78 | return null;
79 | }
80 | return messageType.callback(message.data);
81 | });
--------------------------------------------------------------------------------
/modules/selected.js:
--------------------------------------------------------------------------------
1 | import { log } from "./log.js";
2 |
3 | export const selectedEntries = new Map();
4 | const storeAtEntries = new Map();
5 | export async function clearSelectedEntries(){
6 | selectedEntries.clear();
7 | storeAtEntries.clear();
8 | await browser.storage.local.set({selectedEntries: [], storeAtEntries: []});
9 | }
10 | browser.storage.local.get({selectedEntries: [], storeAtEntries: []}).then(function({
11 | selectedEntries: selectedEntriesStorage,
12 | storeAtEntries: storeAtEntriesStorage
13 | }){
14 | selectedEntriesStorage.forEach(function(selectedEntry){
15 | selectedEntries.set(selectedEntry.host, {doNotAskAgain: true, uuid: selectedEntry.uuid});
16 | });
17 | storeAtEntriesStorage.forEach(function(storeAtEntry){
18 | storeAtEntries.set(storeAtEntry.host, {doNotAskAgain: true, save: storeAtEntry.save, uuid: storeAtEntry.uuid});
19 | });
20 | return undefined;
21 | }).catch(()=>{});
22 |
23 | export function getSelectedEntryUuid(id, entries){
24 | if (selectedEntries.has(id)){
25 | const cached = selectedEntries.get(id);
26 | if (
27 | (
28 | cached.doNotAskAgain ||
29 | Date.now() - cached.timestamp <= 60000
30 | ) &&
31 | (
32 | cached.uuid === false ||
33 | entries.some(e => e.uuid === cached.uuid)
34 | )
35 | ){
36 | log(`Use last selected entry for ${id}: ${cached.uuid}`);
37 | return cached.uuid;
38 | }
39 | }
40 | return undefined;
41 | }
42 |
43 | export async function setSelectedEntry(id, uuid, doNotAskAgain){
44 | selectedEntries.set(id, {uuid: uuid, doNotAskAgain, timestamp: Date.now()});
45 | if (doNotAskAgain){
46 | await browser.storage.local.get({selectedEntries: []}).then(async function({selectedEntries}){
47 | let found = false;
48 | for (let i = 0; i < selectedEntries.length; i += 1){
49 | if (selectedEntries[i].host === id){
50 | selectedEntries[i].uuid = uuid;
51 | found = true;
52 | }
53 | }
54 | if (!found){
55 | selectedEntries.push({host: id, uuid: uuid});
56 | }
57 | await browser.storage.local.set({selectedEntries});
58 | return undefined;
59 | }).catch(error => console.error(error));
60 | }
61 | }
62 |
63 | export function getStoreAtEntry(id, entries){
64 | if (storeAtEntries.has(id)){
65 | const stored = storeAtEntries.get(id);
66 | if (
67 | !stored.save ||
68 | entries.some(e => e.uuid === stored.uuid)
69 | ){
70 | log("Use last store at entry for", id);
71 | return stored;
72 | }
73 | }
74 | return undefined;
75 | }
76 |
77 | export async function setStoreAtEntry(id, uuid, save, doNotAskAgain){
78 | if (doNotAskAgain){
79 | storeAtEntries.set(id, {save, uuid, doNotAskAgain});
80 |
81 | await browser.storage.local.get({storeAtEntries: []}).then(async function({storeAtEntries}){
82 | let found = false;
83 | for (let i = 0; i < storeAtEntries.length; i += 1){
84 | if (storeAtEntries[i].host === id){
85 | storeAtEntries[i].save = save;
86 | storeAtEntries[i].uuid = uuid;
87 | found = true;
88 | }
89 | }
90 | if (!found){
91 | storeAtEntries.push({host: id, uuid, save});
92 | }
93 | await browser.storage.local.set({storeAtEntries});
94 | return undefined;
95 | }).catch(error => console.error(error));
96 | }
97 | }
--------------------------------------------------------------------------------
/experiment/modules/wrappers/LDAPListenerBase.sys.js:
--------------------------------------------------------------------------------
1 | /* globals Services, Ci */
2 | import { LDAPListenerBase } from "resource:///modules/LDAPListenerBase.sys.mjs";
3 | import { LoginManagerAuthPrompter } from "resource://gre/modules/LoginManagerAuthPrompter.sys.mjs";
4 | import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
5 | import { addSetup } from "../setup.sys.js";
6 |
7 |
8 | const originalOnLDAPInit = LDAPListenerBase.prototype.onLDAPInit;
9 | const fixedOnLDAPInit = Services.vc.compare(AppConstants.MOZ_APP_VERSION, "148.0a1") >= 0?
10 | async function onLDAPInit() {
11 | let password = null;
12 | const outPassword = {value: null};
13 | if (this._directory.authDn && this._directory.saslMechanism !== "GSSAPI") {
14 | // If authDn is set, we're expected to use it to get a password.
15 | const bundle = Services.strings.createBundle(
16 | "chrome://mozldap/locale/ldap.properties"
17 | );
18 |
19 | const authPrompt = Services.ww.getNewAuthPrompter(
20 | Services.wm.getMostRecentWindow(null)
21 | );
22 | const result = await authPrompt.asyncPromptPassword(
23 | bundle.GetStringFromName("authPromptTitle"),
24 | bundle.formatStringFromName("authPromptText", [
25 | this._directory.lDAPURL.host,
26 | ]),
27 | this._directory.lDAPURL.spec,
28 | Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
29 | {}
30 | );
31 | if (result.ok){
32 | password = result.password;
33 | }
34 | }
35 | this._operation.init(this._connection, this, null);
36 |
37 | if (this._directory.saslMechanism !== "GSSAPI") {
38 | this._operation.simpleBind(password);
39 | return;
40 | }
41 |
42 | // Handle GSSAPI now.
43 | this._operation.saslBind(
44 | `ldap@${this._directory.lDAPURL.host}`,
45 | "GSSAPI",
46 | "sasl-gssapi"
47 | );
48 | }:
49 | async function onLDAPInit() {
50 | const outPassword = {value: null};
51 | if (this._directory.authDn && this._directory.saslMechanism !== "GSSAPI") {
52 | // If authDn is set, we're expected to use it to get a password.
53 | const bundle = Services.strings.createBundle(
54 | "chrome://mozldap/locale/ldap.properties"
55 | );
56 |
57 | // const authPrompt = Services.ww.getNewAuthPrompter(
58 | // Services.wm.getMostRecentWindow(null)
59 | // );
60 | const authPrompt = LoginManagerAuthPrompter.prototype;
61 | await authPrompt.asyncPromptPassword(
62 | bundle.GetStringFromName("authPromptTitle"),
63 | bundle.formatStringFromName("authPromptText", [
64 | this._directory.lDAPURL.host,
65 | ]),
66 | this._directory.lDAPURL.spec,
67 | Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
68 | outPassword
69 | );
70 | }
71 | this._operation.init(this._connection, this, null);
72 |
73 | if (this._directory.saslMechanism !== "GSSAPI") {
74 | this._operation.simpleBind(outPassword.value);
75 | return;
76 | }
77 |
78 | // Handle GSSAPI now.
79 | this._operation.saslBind(
80 | `ldap@${this._directory.lDAPURL.host}`,
81 | "GSSAPI",
82 | "sasl-gssapi"
83 | );
84 | };
85 |
86 | function changeLDAPListenerBase(){
87 | LDAPListenerBase.prototype.onLDAPInit = fixedOnLDAPInit;
88 | }
89 | function restoreLDAPListenerBase(){
90 | LDAPListenerBase.prototype.onLDAPInit = originalOnLDAPInit;
91 | }
92 |
93 | addSetup({
94 | setup: changeLDAPListenerBase,
95 | shutdown: restoreLDAPListenerBase
96 | });
97 |
--------------------------------------------------------------------------------
/experiment/modules/onlineOfflineControl.sys.js:
--------------------------------------------------------------------------------
1 | /* globals ChromeUtils, Services */
2 | import { log } from "./log.sys.js";
3 | import { OfflineStartup } from "resource:///modules/OfflineStartup.sys.mjs";
4 | import { addSetup } from "./setup.sys.js";
5 |
6 | // online/offline control
7 | const ALWAYS_OFFLINE = 3;
8 | const topics = [
9 | "profile-change-net-teardown",
10 | "quit-application-granted", "quit-application",
11 | ];
12 | class ShutdownObserver{
13 | QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);
14 |
15 | startObserving(){
16 | topics.forEach((topic) => {
17 | log("start observing", topic);
18 | try {
19 | Services.obs.addObserver(this, topic);
20 | }
21 | catch (error){
22 | log("unable to observer", topic, error, error.stack);
23 | }
24 | });
25 | }
26 | stopObserving(){
27 | topics.forEach((topic) => {
28 | Services.obs.removeObserver(this, topic);
29 | });
30 | }
31 |
32 | save(){
33 | if (Services.prefs.prefHasUserValue("keepassxc-mail.offline.startup_state")){
34 | log("Startup online state already saved - do not overwrite");
35 | return;
36 | }
37 | const valueToSave = Services.prefs.getIntPref("offline.startup_state");
38 | log("Saving startup online state:", valueToSave);
39 | Services.prefs.setIntPref(
40 | "keepassxc-mail.offline.startup_state",
41 | valueToSave
42 | );
43 | }
44 | setOfflineStartup(){
45 | log("Set to offline startup");
46 | Services.prefs.setIntPref(
47 | "offline.startup_state",
48 | ALWAYS_OFFLINE
49 | );
50 | }
51 | restore(){
52 | log("Restoring startup online state");
53 | this.restore = () => {};
54 | if (Services.prefs.prefHasUserValue("keepassxc-mail.offline.startup_state")){
55 | const storedValue = Services.prefs.getIntPref("keepassxc-mail.offline.startup_state");
56 | log("keepassxc-mail.offline.startup_state:", storedValue);
57 | log("offline.startup_state:", Services.prefs.getIntPref("offline.startup_state"));
58 | Services.prefs.setIntPref(
59 | "offline.startup_state",
60 | storedValue
61 | );
62 | Services.prefs.clearUserPref("keepassxc-mail.offline.startup_state");
63 |
64 | if (storedValue === ALWAYS_OFFLINE){
65 | log("Offline startup -> do nothing");
66 | return;
67 | }
68 |
69 | log("Calling OfflineStartup.prototype.onProfileStartup");
70 | OfflineStartup.prototype.onProfileStartup();
71 |
72 | if (Services.prefs.getBoolPref("offline.autoDetect")){
73 | log("auto detect: need to test for online state");
74 | Services.io.offline = false;
75 | Services.io.manageOfflineStatus = Services.prefs.getBoolPref(
76 | "offline.autoDetect"
77 | );
78 | }
79 | }
80 | }
81 |
82 | observe(_subject, topic, _data){
83 | log("observed", _subject, topic, _data);
84 | if (
85 | Services.prefs.prefHasUserValue("keepassxc-mail.offline_control") &&
86 | Services.prefs.getBoolPref("keepassxc-mail.offline_control")
87 | ){
88 | this.save();
89 | this.setOfflineStartup();
90 | }
91 | else {
92 | log(
93 | "Startup offline control not activated. " +
94 | "Set the boolean keepassxc-mail.offline_control to true to enable it."
95 | );
96 | }
97 | this.stopObserving();
98 | }
99 | }
100 | const shutdownObserver = new ShutdownObserver();
101 |
102 | addSetup({
103 | setup: function(){
104 | shutdownObserver.restore();
105 | shutdownObserver.startObserving();
106 | },
107 | shutdown: function(){
108 | shutdownObserver.stopObserving();
109 | }
110 | });
--------------------------------------------------------------------------------
/modules/storeCredentials.js:
--------------------------------------------------------------------------------
1 | import { log } from "./log.js";
2 | import { keepass, isReady as isKeepassReady, handleLockedDatabase } from "./keepass.js";
3 | import { wait } from "./utils.js";
4 | import { messageModal, savingPasswordModal } from "./modal.js";
5 | import { selectedEntries } from "./selected.js";
6 |
7 | const timeoutSymbol = Symbol("timeout");
8 | async function storeCredentialsToDatabase(credentialInfo, uuid){
9 |
10 | log("Get or create password group");
11 | const group = await Promise.any([
12 | keepass.createNewGroup(null, ["KeePassXC-Mail Passwords"]),
13 | wait(1 * 60 * 1000, timeoutSymbol)
14 | ]);
15 | if (group === timeoutSymbol){
16 | log("Timeout while creating password group: using default password manager");
17 | messageModal(
18 | browser.i18n.getMessage("createPasswordGroup.timeout.title"),
19 | browser.i18n.getMessage("createPasswordGroup.timeout.message")
20 | .replace("{account}", credentialInfo.login)
21 | );
22 | return false;
23 | }
24 |
25 | log("Saving password to database for", credentialInfo.login, "at", credentialInfo.host);
26 | log("Using uuid:", uuid);
27 |
28 | const result = await Promise.any([
29 | keepass.updateCredentials(null, [
30 | uuid,
31 | credentialInfo.login, credentialInfo.password, credentialInfo.host,
32 | group.name, group.uuid
33 | ]),
34 | wait(2 * 60 * 1000, timeoutSymbol)
35 | ]);
36 | if (result === timeoutSymbol){
37 | log("Timeout while saving: using default password manager");
38 | messageModal(
39 | browser.i18n.getMessage("savePassword.timeout.title"),
40 | browser.i18n.getMessage("savePassword.timeout.message")
41 | .replace("{account}", credentialInfo.login)
42 | );
43 | return false;
44 | }
45 | else {
46 | log("Saving done");
47 | return true;
48 | }
49 | }
50 |
51 | export async function storeCredentials(credentialInfo){
52 | log("Got new password for", credentialInfo.login, "at", credentialInfo.host);
53 | const {saveNewCredentials, autoSaveNewCredentials} = (await browser.storage.local.get({
54 | saveNewCredentials: true,
55 | autoSaveNewCredentials: false
56 | }));
57 | if (!saveNewCredentials){
58 | log("password saving is disabled in the settings");
59 | return false;
60 | }
61 |
62 | await isKeepassReady();
63 | const existingCredentials = (await keepass.retrieveCredentials(
64 | false,
65 | [credentialInfo.host, credentialInfo.host]
66 | )).filter(function (credential){
67 | return (
68 | true === credentialInfo.login ||
69 | credential.login.toLowerCase?.() === credentialInfo.login.toLowerCase?.()
70 | );
71 | });
72 | if (keepass.isDatabaseClosed){
73 | if (await handleLockedDatabase()){
74 | return storeCredentials(credentialInfo);
75 | }
76 | else {
77 | log("Unable to unlock database. Cannot store.");
78 | return true;
79 | }
80 | }
81 | if (existingCredentials.some(function(credential){
82 | return credential.password === credentialInfo.password;
83 | })){
84 | log("the password is already stored");
85 | return true;
86 | }
87 |
88 | const cachedId = credentialInfo.login?
89 | `${credentialInfo.login}@${credentialInfo.host}`:
90 | credentialInfo.host;
91 | const cached = selectedEntries.get(cachedId) || null;
92 | const {save, uuid} = autoSaveNewCredentials?
93 | {save: true, uuid: cached?.uuid || null}:
94 | await savingPasswordModal(
95 | credentialInfo.host,
96 | credentialInfo.login,
97 | existingCredentials.map(function (data){
98 | return {
99 | name: data.name,
100 | login: data.login,
101 | uuid: data.uuid,
102 | preselected: data.uuid === cached?.uuid
103 | };
104 | })
105 | );
106 | if (!save){
107 | log("the user decided to not store the password");
108 | return true;
109 | }
110 |
111 | return await storeCredentialsToDatabase(credentialInfo, uuid);
112 | }
--------------------------------------------------------------------------------
/.tools/build.js:
--------------------------------------------------------------------------------
1 | const child_process = require("child_process");
2 | const process = require("process");
3 | const path = require("path");
4 |
5 | const fs = require("fs");
6 |
7 | function leftPad(number, size){
8 | const str = number.toFixed(0);
9 | const missing = size - str.length;
10 | if (missing <= 0){
11 | return str;
12 | }
13 | return "0".repeat(missing) + str;
14 | }
15 |
16 | function updateVersion(version){
17 | const parts = version.split(".");
18 | if (parts.length < 2){
19 | parts[1] = "0";
20 | }
21 | const now = new Date();
22 | const date = `${now.getFullYear()}${leftPad(now.getMonth() + 1, 2)}${leftPad(now.getDate(), 2)}`;
23 | if (parts.length < 3 || parts[2] !== date){
24 | parts[2] = date;
25 | parts[3] = "0";
26 | }
27 | else {
28 | if (parts.length < 4){
29 | parts[3] = "0";
30 | }
31 | else {
32 | parts[3] = (parseInt(parts[3], 10) + 1).toFixed(0);
33 | }
34 | }
35 | return parts.join(".");
36 | }
37 |
38 | async function updateImports(filePath, version){
39 | console.log("Updating imports in", filePath);
40 | const content = await fs.promises.readFile(filePath, {encoding: "utf-8"});
41 | const modifiedContent = content.replace(
42 | /((?:^|[\n\r;])\s*import\s+[\s{}a-zA-Z0-9_,]+\s+from\s+"\.[a-zA-Z0-9/._-]+)\??[0-9.]*";/g,
43 | `$1?${version}";`
44 | );
45 | const originalContentPath = filePath + ".omjs";
46 | await fs.promises.rename(filePath, originalContentPath);
47 | await fs.promises.writeFile(filePath, modifiedContent, {encoding: "utf-8"});
48 | return {
49 | originalFilePath: filePath,
50 | originalContentPath
51 | };
52 | }
53 |
54 | async function updateAllImports(folder, version){
55 | return Promise.all(
56 | (await fs.promises.readdir(folder, {recursive: true, withFileTypes: true})).filter(function(fileInfo){
57 | return !fileInfo.isDirectory();
58 | }).map(async function(fileInfo){
59 | const filePath = path.join(fileInfo.parentPath, fileInfo.name);
60 | return await updateImports(filePath, version);
61 | })
62 | );
63 | }
64 |
65 | async function run(){
66 | "use strict";
67 |
68 | const baseFolder = path.join(__dirname, "..");
69 |
70 | const manifest = require("../manifest.json");
71 | console.log("updating version");
72 | manifest.version = updateVersion(manifest.version);
73 | console.log("... new:", manifest.version);
74 | console.log("updating manifest.json");
75 | await fs.promises.writeFile(
76 | path.join(baseFolder, "manifest.json"),
77 | JSON.stringify(manifest, undefined, "\t"),
78 | {encoding: "utf-8"}
79 | );
80 | const modifiedModules = await updateAllImports(path.join(baseFolder, "experiment", "modules"), manifest.version);
81 |
82 | const outputFolder = path.join(baseFolder, "mail-ext-artifacts");
83 | try {
84 | await fs.promises.access(outputFolder, fs.constants.F_OK);
85 | }
86 | catch (e){
87 | if (e.code === "ENOENT"){
88 | await fs.promises.mkdir(outputFolder);
89 | }
90 | else {
91 | throw e;
92 | }
93 | }
94 |
95 | const fileName = `${manifest.name}-${manifest.version}.xpi`.replace(/\s+/g, "-");
96 | const filePath = path.join(outputFolder, fileName);
97 | try {
98 | await fs.promises.unlink(filePath);
99 | }
100 | catch (e){}
101 |
102 | const exclude = [
103 | "experiment/modules/*.omjs", "experiment/modules/**/*.omjs",
104 | "mail-ext-artifacts/", "mail-ext-artifacts/*",
105 | "versions/*",
106 | "crowdin.yml",
107 | "README.md",
108 | "node_modules/*", ".*", "**/.*", "package*", "src/"];
109 |
110 | const args = ["-r", filePath, "./", "--exclude", ...exclude];
111 |
112 | process.chdir(baseFolder);
113 |
114 | const zipProcess = child_process.spawn("zip", args, {stdio: "inherit"});
115 |
116 | zipProcess.on("exit", function(){
117 | modifiedModules.forEach(async function(module){
118 | console.log("restoring", module.originalFilePath);
119 | await fs.promises.rm(module.originalFilePath);
120 | await fs.promises.rename(module.originalContentPath, module.originalFilePath);
121 | });
122 | });
123 | }
124 |
125 | run();
126 |
--------------------------------------------------------------------------------
/experiment/modules/wrappers/oauth2Module.sys.js:
--------------------------------------------------------------------------------
1 | import { OAuth2Module } from "resource:///modules/OAuth2Module.sys.mjs";
2 | import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
3 | import { waitForCredentials, waitForPasswordStore } from "../wait.sys.js";
4 | import { addSetup } from "../setup.sys.js";
5 |
6 | const temporaryCache = new Map();
7 | const getKey = function getKey(oAuth2Module){
8 | return oAuth2Module._username + "|" + oAuth2Module._loginOrigin;
9 | };
10 | const setCache = function setCache(key, value, timeout = 2000){
11 | if (temporaryCache.has(key)){
12 | clearTimeout(temporaryCache.get(key).timeout);
13 | }
14 | temporaryCache.set(key, {
15 | value,
16 | timeout: setTimeout(function(){
17 | temporaryCache.delete(key);
18 | }, timeout)
19 | });
20 | };
21 | export const getRefreshToken = function getRefreshToken(tokenStore){
22 | const key = getKey(tokenStore);
23 | const cached = temporaryCache.get(key);
24 | if (cached){
25 | return cached.value;
26 | }
27 | const credentials = waitForCredentials({
28 | login: tokenStore._username,
29 | host: tokenStore._loginOrigin
30 | });
31 | if (
32 | credentials &&
33 | credentials.credentials.length &&
34 | (typeof credentials.credentials[0].password) === "string"
35 | ){
36 | setCache(key, credentials.credentials[0].password);
37 | return credentials.credentials[0].password;
38 | }
39 | return null;
40 | };
41 | export const setRefreshToken = function setRefreshToken(tokenStore, refreshToken){
42 | if (refreshToken === getRefreshToken(tokenStore)){
43 | return true;
44 | }
45 | setCache(getKey(tokenStore), refreshToken, 5000);
46 | const stored = waitForPasswordStore({
47 | login: tokenStore._username,
48 | password: refreshToken,
49 | host: tokenStore._loginOrigin,
50 | });
51 | return stored;
52 | };
53 | if (OAuth2Module.prototype.hasOwnProperty("getRefreshToken")){
54 | const originalGetRefreshToken = OAuth2Module.prototype.getRefreshToken;
55 | const alteredGetRefreshToken = function(){
56 | const token = getRefreshToken(this);
57 | if (token !== null){
58 | return token;
59 | }
60 | return originalGetRefreshToken.call(this);
61 | };
62 | addSetup({
63 | setup: function(){
64 | OAuth2Module.prototype.getRefreshToken = alteredGetRefreshToken;
65 | },
66 | shutdown: function(){
67 | OAuth2Module.prototype.getRefreshToken = originalGetRefreshToken;
68 | }
69 | });
70 | }
71 | if (OAuth2Module.prototype.hasOwnProperty("setRefreshToken")){
72 | const originalSetRefreshToken = OAuth2Module.prototype.setRefreshToken;
73 | const alteredSetRefreshToken = async function(refreshToken){
74 | const stored = setRefreshToken(this, refreshToken);
75 | if (!stored){
76 | return originalSetRefreshToken.call(this, refreshToken);
77 | }
78 | return refreshToken;
79 | };
80 | addSetup({
81 | setup: function(){
82 | OAuth2Module.prototype.setRefreshToken = alteredSetRefreshToken;
83 | },
84 | shutdown: function(){
85 | OAuth2Module.prototype.setRefreshToken = originalSetRefreshToken;
86 | }
87 | });
88 | }
89 |
90 | if (OAuth2Module.prototype.hasOwnProperty("refreshToken")){
91 | const originalRefreshTokenDescriptor = Object.getOwnPropertyDescriptor(OAuth2Module.prototype, "refreshToken");
92 | const alteredRefreshTokenDescriptor = Object.create(originalRefreshTokenDescriptor);
93 | alteredRefreshTokenDescriptor.get = function(){
94 | const refreshToken = getRefreshToken(this);
95 | if (refreshToken !== null){
96 | return refreshToken;
97 | }
98 | return originalRefreshTokenDescriptor.get.call(this);
99 | };
100 | alteredRefreshTokenDescriptor.set = function(refreshToken){
101 | const stored = setRefreshToken(this, refreshToken);
102 | if (!stored){
103 | return originalRefreshTokenDescriptor.set.call(this, refreshToken);
104 | }
105 | return refreshToken;
106 | };
107 | addSetup({
108 | setup: function(){
109 | Object.defineProperty(OAuth2Module.prototype, "refreshToken", alteredRefreshTokenDescriptor);
110 | },
111 | shutdown: function(){
112 | Object.defineProperty(OAuth2Module.prototype, "refreshToken", originalRefreshTokenDescriptor);
113 | }
114 | });
115 | }
--------------------------------------------------------------------------------
/modules/modal.js:
--------------------------------------------------------------------------------
1 | import { setSelectedEntry, getSelectedEntryUuid, setStoreAtEntry, getStoreAtEntry } from "./selected.js";
2 |
3 | const waitForPort = (function(){
4 | const ports = new Map();
5 | const queue = new Map();
6 | function addQueue(tabId, callback){
7 | const queueEntry = (queue.get(tabId) || []);
8 | queueEntry.push(callback);
9 | queue.set(tabId, queueEntry);
10 | }
11 | function removeQueue(tabId, callback){
12 | const remaining = (queue.get(tabId) || []).filter(c => c !== callback);
13 | if (remaining.length){
14 | queue.set(tabId, remaining);
15 | }
16 | else {
17 | queue.delete(tabId);
18 | }
19 | }
20 | browser.runtime.onConnect.addListener(function(port){
21 | const tabId = port.sender.tab.id;
22 | ports.set(tabId, port);
23 | if(queue.has(tabId)){
24 | queue.get(tabId).forEach(function(callback){
25 | callback(port);
26 | });
27 | queue.delete(tabId);
28 | }
29 | port.onDisconnect.addListener(function(){
30 | ports.delete(tabId);
31 | });
32 | });
33 | return async function waitForPort(tabId, timeout = 1500){
34 | return new Promise(function(resolve, reject){
35 | if (ports.has(tabId)){
36 | resolve(ports.get(tabId));
37 | return;
38 | }
39 | addQueue(tabId, resolve);
40 | const timeoutId = window.setTimeout(function(){
41 | removeQueue(tabId, resolve);
42 | removeQueue(tabId, cancelTimeout);
43 | reject("Timeout");
44 | }, timeout);
45 | function cancelTimeout(){
46 | window.clearTimeout(timeoutId);
47 | }
48 | addQueue(tabId, cancelTimeout);
49 | });
50 | };
51 | }());
52 |
53 | async function openModal({path, message, defaultReturnValue}){
54 | function getPortResponse(port){
55 | return new Promise(function(resolve){
56 | function resolveDefault(){
57 | resolve(defaultReturnValue);
58 | }
59 |
60 | port.onDisconnect.addListener(resolveDefault);
61 | port.onMessage.addListener(function(data){
62 | if (data.type === "response"){
63 | resolve(data.value);
64 | port.onDisconnect.removeListener(resolveDefault);
65 | }
66 | });
67 | port.postMessage({
68 | type: "start",
69 | message
70 | });
71 | });
72 | }
73 | const window = await browser.windows.create({
74 | url: browser.runtime.getURL(path),
75 | allowScriptsToClose: true,
76 | height: 100,
77 | width: 600,
78 | type: "detached_panel"
79 | });
80 | try {
81 | const port = await waitForPort(window.tabs[0].id);
82 | return getPortResponse(port);
83 | }
84 | catch (error){
85 | return defaultReturnValue;
86 | }
87 | }
88 |
89 | export async function messageModal(title, text){
90 | return await openModal({
91 | path: "modal/message/index.html",
92 | message: {
93 | title,
94 | text
95 | },
96 | defaultReturnValue: undefined
97 | });
98 | }
99 |
100 | export async function confirmModal(title, question){
101 | return await openModal({
102 | path: "modal/confirm/index.html",
103 | message: {
104 | title,
105 | question
106 | },
107 | defaultReturnValue: false
108 | });
109 | }
110 |
111 | export async function choiceModal(host, login, entries){
112 | const cachedId = login? `${login}@${host}`: host;
113 | const cachedUuid = getSelectedEntryUuid(cachedId, entries);
114 | if (cachedUuid !== undefined){
115 | return cachedUuid;
116 | }
117 | const {selectedUuid, doNotAskAgain} = (entries.length === 1 && entries[0].autoSubmit)?
118 | {selectedUuid: entries[0].uuid, doNotAskAgain: false}:
119 | await openModal({
120 | path: "modal/choice/index.html",
121 | message: {
122 | host,
123 | login,
124 | entries
125 | },
126 | defaultReturnValue: {selectedUuid: undefined, doNotAskAgain: false}
127 | });
128 |
129 | if (selectedUuid !== undefined){
130 | setSelectedEntry(cachedId, selectedUuid, doNotAskAgain);
131 | }
132 | return selectedUuid;
133 | }
134 |
135 |
136 | export async function savingPasswordModal(host, login, entries){
137 | const storeId = login? `${login}@${host}`: host;
138 | const stored = getStoreAtEntry(storeId, entries);
139 | if (stored !== undefined){
140 | return stored;
141 | }
142 | const {save, uuid, doNotAskAgain} = await openModal({
143 | path: "modal/savingPassword/index.html",
144 | message: {
145 | host,
146 | login,
147 | entries,
148 | },
149 | defaultReturnValue: {save: false, uuid: undefined, doNotAskAgain: false}
150 | });
151 | if (uuid !== undefined){
152 | setStoreAtEntry(storeId, uuid, save, doNotAskAgain);
153 | }
154 |
155 | return {save, uuid, doNotAskAgain};
156 | }
--------------------------------------------------------------------------------
/modules/keepass.js:
--------------------------------------------------------------------------------
1 | /* globals keepass, keepassClient */
2 | import { log } from "./log.js";
3 | import { connect, disconnect } from "./nativeMessaging.js";
4 | import { wait } from "./utils.js";
5 | import { confirmModal, messageModal } from "./modal.js";
6 |
7 | const k = keepass;
8 | const kc = keepassClient;
9 |
10 | export { k as keepass, kc as keepassClient };
11 |
12 | async function loadKeyRing(){
13 | let loadCount = 0;
14 | let lastLoadError = null;
15 | while (!keepass.keyRing){
16 | await wait(50);
17 | loadCount += 1;
18 | try {
19 | const item = await browser.storage.local.get({
20 | latestKeePassXC: {
21 | version: "",
22 | lastChecked: null
23 | },
24 | keyRing: {}
25 | });
26 | keepass.latestKeePassXC = item.latestKeePassXC;
27 | keepass.keyRing = item.keyRing || {};
28 | }
29 | catch (error){
30 | lastLoadError = error;
31 | }
32 | }
33 | if (lastLoadError){
34 | log("Loaded key ring", loadCount, "times", lastLoadError);
35 | }
36 | }
37 |
38 | async function checkKeyRingStorage(){
39 | function objectsEqual(obj1, obj2){
40 | const keys1 = Object.keys(obj1);
41 | const keys2 = Object.keys(obj2);
42 | if (keys1.length !== keys2.length){
43 | return false;
44 | }
45 | return keys1.every(function(key){
46 | const value1 = obj1[key];
47 | const value2 = obj2[key];
48 | if ((typeof value1) !== (typeof value2)){
49 | return false;
50 | }
51 | if ((typeof value1) === "object"){
52 | return objectsEqual(value1, value2);
53 | }
54 | return value1 === value2;
55 | });
56 | }
57 | // check if the key ring actually saved in the storage
58 | const databaseHashes = Object.keys(keepass.keyRing);
59 | if (databaseHashes.length){
60 | let storedKeyRing = (await browser.storage.local.get({keyRing: {}})).keyRing;
61 | while (!objectsEqual(keepass.keyRing, storedKeyRing)){
62 | await wait(500);
63 | log("Store key ring");
64 | try {
65 | await browser.storage.local.set({keyRing: keepass.keyRing});
66 | storedKeyRing = (await browser.storage.local.get({keyRing: {}})).keyRing;
67 | }
68 | catch (e){
69 | log("storing key ring failed:", e);
70 | }
71 | }
72 | }
73 | }
74 |
75 | export const isReady = (() => {
76 | async function initialize(){
77 | // load key ring - initially done in keepass.js but it fails sometimes...
78 | await loadKeyRing();
79 | await keepass.migrateKeyRing();
80 | await connect();
81 | await keepass.enableAutomaticReconnect();
82 | await keepass.associate();
83 | if (keepass.isDatabaseClosed){
84 | await unlockDatabase();
85 | }
86 | // check key ring storage - initially done in keepass.js but it fails sometimes...
87 | checkKeyRingStorage();
88 |
89 | return {connect, disconnect};
90 | }
91 | let keepassReady = initialize();
92 | keepassReady.catch((error) => log("Initialization failed:", error));
93 | return async function(){
94 | try {
95 | return await keepassReady;
96 | }
97 | catch (error){
98 | keepassReady = initialize();
99 | keepassReady.catch((error) => log("Initialization failed:", error));
100 | return await keepassReady;
101 | }
102 | };
103 | })();
104 |
105 | let currentUnlockAttempt = undefined;
106 | async function unlockDatabase(){
107 | try {
108 | log("Database is locked -> ask to open");
109 | if (!await confirmModal(
110 | browser.i18n.getMessage("unlockDatabase.title"),
111 | browser.i18n.getMessage("unlockDatabase.question")
112 | )){
113 | throw "user declined";
114 | }
115 | log("try to unlock");
116 | await keepass.testAssociation(undefined, [true, true]);
117 | // check for one minute every second
118 | // automatic reconnect handles the connection
119 | for (let i = 0; i < 1 * 60; i += 1){
120 | await wait(1000);
121 | if (!keepass.isDatabaseClosed){
122 | break;
123 | }
124 | }
125 | if (keepass.isDatabaseClosed){
126 | await messageModal(
127 | browser.i18n.getMessage("unlockDatabase.title"),
128 | browser.i18n.getMessage("unlockDatabase.failed")
129 | );
130 | throw "unlock timeout";
131 | }
132 | log("Unlock successful");
133 | return true;
134 | }
135 | catch (error){
136 | log("Unlock not successful:", error);
137 | return false;
138 | }
139 | finally {
140 | currentUnlockAttempt = undefined;
141 | }
142 | }
143 | export async function handleLockedDatabase(){
144 | if (currentUnlockAttempt){
145 | log("Unlock attempt already running");
146 | return currentUnlockAttempt;
147 | }
148 | currentUnlockAttempt = unlockDatabase();
149 | return currentUnlockAttempt;
150 | }
--------------------------------------------------------------------------------
/icons/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
70 |
--------------------------------------------------------------------------------
/experiment/modules/gui/utils.sys.js:
--------------------------------------------------------------------------------
1 | import { extension } from "../extension.sys.js";
2 | import { requestCredentials } from "../credentials.sys.js";
3 |
4 | function getTranslation(name, variables){
5 | const translation = extension.localeData.localizeMessage(name) || name;
6 | if (!variables){
7 | return translation;
8 | }
9 | return translation.replace(/\{\s*([^}]*?)\s*\}/g, function(m, name){
10 | const namesToTry = name.split(/\s*\|\s*/g);
11 | for (const name of namesToTry){
12 | if (name.match(/^["'].*["']$/)){
13 | return name.replace(/^['"]|['"]$/g, "");
14 | }
15 | if (variables[name]){
16 | return variables[name];
17 | }
18 | }
19 | return m;
20 | });
21 | }
22 |
23 | export function buildDialogGui(guiOperations, credentialInfo){
24 | const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
25 | const window = guiOperations.window;
26 | const document = window.document;
27 |
28 | /* add ui elements to dialog */
29 | const row = document.createElementNS(xulNS, "row");
30 | row.setAttribute("id", "credentials-row");
31 |
32 | // spacer to take up first column in layout
33 | const spacer = document.createElementNS(xulNS, "spacer");
34 | spacer.setAttribute("flex", "1");
35 | row.appendChild(spacer);
36 |
37 | // this box displays labels and also the list of entries when fetched
38 | const box = document.createElementNS(xulNS, "hbox");
39 | box.setAttribute("id", "credentials-box");
40 | box.setAttribute("align", "center");
41 | box.setAttribute("flex", "1");
42 | box.setAttribute("pack", "start");
43 |
44 | const description = document.createElementNS(xulNS, "description");
45 | description.setAttribute("id", "credentials-description");
46 | description.setAttribute("align", "start");
47 | description.setAttribute("flex", "1");
48 | description.setAttribute("value", getTranslation("loadingPasswords"));
49 | description.setAttribute("tooltiptext", getTranslation("credentialInfo", credentialInfo));
50 | box.appendChild(description);
51 |
52 | const retryButton = document.createElementNS(xulNS, "button");
53 | retryButton.setAttribute("label", getTranslation("retry"));
54 | retryButton.addEventListener("command", async function(){
55 | retryButton.style.display = "none";
56 | description.setAttribute("value", getTranslation("loadingPasswords"));
57 | const credentialDetails = await requestCredentials(credentialInfo);
58 | updateGUI(guiOperations, credentialInfo, credentialDetails);
59 | window.sizeToContent();
60 | });
61 | retryButton.setAttribute("id", "credentials-retry-button");
62 | retryButton.style.display = "none";
63 | box.appendChild(retryButton);
64 | row.appendChild(box);
65 |
66 | guiOperations.guiParent.appendChild(row);
67 |
68 | // hide "save password" checkbox
69 | const checkbox = document.getElementById("checkbox");
70 | if (checkbox?.checked){
71 | checkbox.click();
72 | }
73 | const checkboxContainer = document.getElementById("checkboxContainer");
74 | if (checkboxContainer){
75 | checkboxContainer.hidden = true;
76 | }
77 |
78 | window.sizeToContent();
79 | }
80 |
81 | export function updateGUI(guiOperations, credentialInfo, credentialDetails){
82 | const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
83 | const window = guiOperations.window;
84 | const document = window.document;
85 | const row = document.getElementById("credentials-row");
86 | const box = document.getElementById("credentials-box");
87 | const description = document.getElementById("credentials-description");
88 | if (!(row && box && description)){
89 | return;
90 | }
91 |
92 | function fillCredentials(credentials){
93 | description.value = getTranslation("pickedEntry", credentials);
94 | guiOperations.fillCredentials(credentialInfo, credentials);
95 | }
96 | const credentials = credentialDetails.credentials;
97 | if (!credentials.length){
98 | description.setAttribute("value", getTranslation("noPasswordsFound"));
99 | document.getElementById("credentials-retry-button").style.display = "";
100 | window.sizeToContent();
101 | return;
102 | }
103 |
104 | fillCredentials(credentials[0]);
105 | if (credentials.length === 1){
106 | if (credentialDetails.autoSubmit && !credentials[0].skipAutoSubmit){
107 | guiOperations.submit();
108 | }
109 | return;
110 | }
111 |
112 | const list = document.createElementNS(xulNS, "menulist");
113 | list.setAttribute("id", "credentials-list");
114 | const popup = document.createElementNS(xulNS, "menupopup");
115 |
116 | credentials.forEach(function(credentials){
117 | const item = document.createElementNS(xulNS, "menuitem");
118 | item.setAttribute("label", getTranslation("entryLabel", credentials));
119 | item.setAttribute("tooltiptext", getTranslation("entryTooltip", credentials));
120 | item.addEventListener("command", function(){
121 | fillCredentials(credentials);
122 | if (credentialDetails.autoSubmit && !credentials.skipAutoSubmit){
123 | guiOperations.submit();
124 | }
125 | });
126 | popup.appendChild(item);
127 | });
128 |
129 | list.appendChild(popup);
130 | box.appendChild(list);
131 |
132 | window.sizeToContent();
133 | }
--------------------------------------------------------------------------------
/versions/updates.json:
--------------------------------------------------------------------------------
1 | {
2 | "addons": {
3 | "keepassxc-mail@kkapsner.de": {
4 | "updates": [
5 | {
6 | "version": "0.1.2",
7 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.2.xpi"
8 | },
9 | {
10 | "version": "0.1.3",
11 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.3.xpi"
12 | },
13 | {
14 | "version": "0.1.4",
15 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.4.xpi"
16 | },
17 | {
18 | "version": "0.1.5",
19 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.5.xpi"
20 | },
21 | {
22 | "version": "0.1.6",
23 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.6.xpi"
24 | },
25 | {
26 | "version": "0.1.7",
27 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.7.xpi"
28 | },
29 | {
30 | "version": "0.1.7.1",
31 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.7.1.xpi"
32 | },
33 | {
34 | "version": "0.1.8",
35 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.8.xpi"
36 | },
37 | {
38 | "version": "0.1.9",
39 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.9.xpi"
40 | },
41 | {
42 | "version": "0.1.9.1",
43 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.9.1.xpi"
44 | },
45 | {
46 | "version": "0.1.10",
47 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.10.xpi"
48 | },
49 | {
50 | "version": "0.1.11",
51 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.11.xpi"
52 | },
53 | {
54 | "version": "0.1.12",
55 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc-mail-0.1.12.xpi"
56 | },
57 | {
58 | "version": "0.9",
59 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-0.9-tb.xpi"
60 | },
61 | {
62 | "version": "1.0",
63 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.0-tb.xpi"
64 | },
65 | {
66 | "version": "1.0.1",
67 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.0.1-tb.xpi"
68 | },
69 | {
70 | "version": "1.0.2.1",
71 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.0.2.1-tb.xpi"
72 | },
73 | {
74 | "version": "1.0.3",
75 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.0.3-tb.xpi"
76 | },
77 | {
78 | "version": "1.1",
79 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.1-tb.xpi"
80 | },
81 | {
82 | "version": "1.2",
83 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.2-tb.xpi"
84 | },
85 | {
86 | "version": "1.3",
87 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.3-tb.xpi"
88 | },
89 | {
90 | "version": "1.4",
91 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.4-tb.xpi"
92 | },
93 | {
94 | "version": "1.5",
95 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.5-tb.xpi"
96 | },
97 | {
98 | "version": "1.6",
99 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.6-tb.xpi"
100 | },
101 | {
102 | "version": "1.6.1",
103 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.6.1-tb.xpi"
104 | },
105 | {
106 | "version": "1.7",
107 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.7-tb.xpi"
108 | },
109 | {
110 | "version": "1.8",
111 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.8-tb.xpi"
112 | },
113 | {
114 | "version": "1.9",
115 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.9-tb.xpi"
116 | },
117 | {
118 | "version": "1.9.1",
119 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.9.1-tb.xpi"
120 | },
121 | {
122 | "version": "1.10",
123 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.10-tb.xpi"
124 | },
125 | {
126 | "version": "1.11.20250416.6",
127 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.11.20250416.6-tb.xpi"
128 | },
129 | {
130 | "version": "1.12.20250629.0",
131 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.12.20250629.0-tb.xpi"
132 | },
133 | {
134 | "version": "1.13.20251007.0",
135 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.13.20251007.0-tb.xpi"
136 | },
137 | {
138 | "version": "1.14.20251216.0",
139 | "update_link": "https://keepassxc-mail.kkapsner.de/versions/keepassxc_mail-1.14.20251216.0-tb.xpi"
140 | }
141 | ]
142 | }
143 | }
144 | }
--------------------------------------------------------------------------------
/experiment/implementation.js:
--------------------------------------------------------------------------------
1 | /* globals ChromeUtils, Cc, Ci, Components, XPCOMUtils, globalThis*/
2 | /* eslint eslint-comments/no-use: off */
3 | /* eslint {"indent": ["error", "tab", {"SwitchCase": 1, "outerIIFEBody": 0}]}*/
4 | "use strict";
5 | ((exports) => {
6 | function importModule(path, addExtension = true){
7 | if (ChromeUtils.import){
8 | return ChromeUtils.import(path + (addExtension? ".jsm": ""));
9 | }
10 | else if (ChromeUtils.importESModule){
11 | return ChromeUtils.importESModule(path + (addExtension? ".sys.mjs": ""));
12 | }
13 | else {
14 | throw "Unable to import module " + path;
15 | }
16 | }
17 | const { ExtensionCommon } = importModule("resource://gre/modules/ExtensionCommon");
18 | const { ExtensionParent } = importModule("resource://gre/modules/ExtensionParent");
19 |
20 | const Services = function(){
21 | let Services;
22 | try {
23 | Services = globalThis.Services;
24 | }
25 | // eslint-disable-next-line no-empty
26 | catch (error){}
27 | return Services || importModule("resource://gre/modules/Services").Services;
28 | }();
29 |
30 | // prepare to load ES modules
31 |
32 | const extension = ExtensionParent.GlobalManager.getExtension("keepassxc-mail@kkapsner.de");
33 |
34 | const resProto = Cc[
35 | "@mozilla.org/network/protocol;1?name=resource"
36 | ].getService(Ci.nsISubstitutingProtocolHandler);
37 |
38 | resProto.setSubstitutionWithFlags(
39 | "keepassxc-mail",
40 | Services.io.newURI(
41 | "experiment",
42 | null,
43 | extension.rootURI
44 | ),
45 | resProto.ALLOW_CONTENT_ACCESS
46 | );
47 |
48 | const { log } = ChromeUtils.importESModule(
49 | `resource://keepassxc-mail/experiment/modules/log.sys.js?${extension.addonData.version}`
50 | );
51 | function importOwnModule(name){
52 | try {
53 | const path = `resource://keepassxc-mail/experiment/modules/${name}?${extension.addonData.version}`;
54 | // log("Loading", path);
55 | return ChromeUtils.importESModule(path);
56 | }
57 | catch (error){
58 | log(`Unable to load ${name}:`, error);
59 | return {};
60 | }
61 | }
62 | const { passwordEmitter, passwordRequestEmitter } = importOwnModule("emitters.sys.js");
63 |
64 | importOwnModule("onlineOfflineControl.sys.js");
65 |
66 | importOwnModule("prompter/asyncPrompter.sys.js");
67 | importOwnModule("prompter/loginManagerAuthPrompter.sys.js");
68 | importOwnModule("prompter/prompter.sys.js");
69 |
70 | importOwnModule("gui/commonDialog.sys.js");
71 |
72 | importOwnModule("wrappers/cal.sys.js");
73 | importOwnModule("wrappers/oauth2Module.sys.js");
74 | importOwnModule("wrappers/oauth2.sys.js");
75 |
76 | // workaround to avoid the XPCOutParam objects problem
77 | importOwnModule("wrappers/LDAPListenerBase.sys.js");
78 |
79 | exports.credentials = class extends ExtensionCommon.ExtensionAPI {
80 | getAPI(context){
81 | return {
82 | credentials: {
83 | getThunderbirdSavedLoginsStatus: async function(){
84 | return Services.logins.findLogins("", null, "").reduce(function(status, login){
85 | status.count += 1;
86 | status.latestTimeCreated = Math.max(status.latestTimeCreated, login.timeCreated);
87 | status.latestTimeLastUsed = Math.max(status.latestTimeCreated, login.timeLastUsed);
88 | status.latestTimePasswordChanged = Math.max(
89 | status.latestTimeCreated,
90 | login.timePasswordChanged
91 | );
92 | return status;
93 | }, {
94 | count: 0,
95 | latestTimeCreated: -1,
96 | latestTimeLastUsed: -1,
97 | latestTimePasswordChanged: -1
98 | });
99 | },
100 | onCredentialRequested: new ExtensionCommon.EventManager({
101 | context,
102 | name: "credentials.onCredentialRequested",
103 | register(fire){
104 | async function callback(event, credentialInfo){
105 | try {
106 | return await fire.async(credentialInfo);
107 | }
108 | catch (e){
109 | console.error(e);
110 | return false;
111 | }
112 | }
113 |
114 | passwordRequestEmitter.add(callback);
115 | return function(){
116 | passwordRequestEmitter.remove(callback);
117 | };
118 | },
119 | }).api(),
120 | onNewCredential: new ExtensionCommon.EventManager({
121 | context,
122 | name: "credentials.onNewCredential",
123 | register(fire){
124 | async function callback(event, credentialInfo){
125 | try {
126 | const callback = credentialInfo.callback;
127 | delete credentialInfo.callback;
128 | const returnValue = await fire.async(credentialInfo);
129 | if (callback){
130 | await callback(returnValue);
131 | }
132 | return returnValue;
133 | }
134 | catch (e){
135 | console.error(e);
136 | return false;
137 | }
138 | }
139 |
140 | passwordEmitter.on("password", callback);
141 | return function(){
142 | passwordEmitter.off("password", callback);
143 | };
144 | },
145 | }).api(),
146 | },
147 | };
148 | }
149 |
150 | onShutdown(isAppShutdown){
151 | if (isAppShutdown) {
152 | return; // the application gets unloaded anyway
153 | }
154 | resProto.setSubstitution(
155 | "keepassxc-mail",
156 | null
157 | );
158 |
159 | // Flush all caches.
160 | Services.obs.notifyObservers(null, "startupcache-invalidate");
161 | }
162 | };
163 |
164 |
165 | })(this);
--------------------------------------------------------------------------------
/experiment/modules/prompter/utils.sys.js:
--------------------------------------------------------------------------------
1 | /* globals Services */
2 | import { waitForPromise } from "../wait.sys.js";
3 | import { requestCredentials } from "../credentials.sys.js";
4 | import { log } from "../log.sys.js";
5 | import { getCredentialInfoFromStrings, getCredentialInfoFromStringsAndProtocol } from "../dialogStrings.sys.js";
6 | import { addSetup } from "../setup.sys.js";
7 |
8 | export function registerPromptFunctions(promptFunctions){
9 | addSetup({
10 | setup: function(){
11 | promptFunctions.forEach(function(promptFunction){
12 | promptFunction.object[promptFunction.name] = promptFunction.replacement;
13 | });
14 | },
15 | shutdown: function(){
16 | promptFunctions.forEach(function(promptFunction){
17 | promptFunction.object[promptFunction.name] = promptFunction.original;
18 | });
19 | }
20 | });
21 | }
22 |
23 | function createPromptDataFunctions(promptFunction){
24 | const promptDataFunctions = [];
25 | if (promptFunction.dataFunction){
26 | promptDataFunctions.push(promptFunction.dataFunction);
27 | }
28 | if (promptFunction.hasOwnProperty("realmIndex")){
29 | promptDataFunctions.push(function(args){
30 | const realm = args[promptFunction.realmIndex];
31 | try {
32 | const uri = Services.io.newURI(realm);
33 | const protocol = uri.scheme === "mailbox"? "pop3": uri.scheme;
34 | const realmLogin = uri.username;
35 | const realmHost = protocol + "://" + uri.displayHostPort;
36 | // realm data provides the correct protocol but may have wrong server name
37 | const {host: stringHost, login: stringLogin, mayAddProtocol} = getCredentialInfoFromStringsAndProtocol(
38 | args[promptFunction.titleIndex],
39 | args[promptFunction.textIndex],
40 | protocol
41 | );
42 | return {
43 | mayAddProtocol,
44 | protocol,
45 | host: stringHost || realmHost,
46 | login: stringLogin || decodeURIComponent(realmLogin),
47 | realm,
48 | };
49 | }
50 | catch (error){
51 | log("Error retrieving realm data from", args, error);
52 | return false;
53 | }
54 | });
55 | }
56 | if (promptFunction.hasOwnProperty("titleIndex")){
57 | promptDataFunctions.push(function(args){
58 | return getCredentialInfoFromStrings(
59 | args[promptFunction.titleIndex],
60 | args[promptFunction.textIndex]
61 | );
62 | });
63 | }
64 | if (promptFunction.hasOwnProperty("authInfoIndex")){
65 | promptDataFunctions.push(function(args){
66 | return {
67 | host: args[promptFunction.channelIndex].URI.spec,
68 | login: args[promptFunction.authInfoIndex].username,
69 | realm: args[promptFunction.authInfoIndex].realm,
70 | };
71 | });
72 | }
73 | return promptDataFunctions;
74 | }
75 |
76 | function initPromptFunction(promptFunction, object, objectName){
77 | promptFunction.object = object;
78 | promptFunction.objectName = objectName;
79 | promptFunction.promptDataFunctions = createPromptDataFunctions(promptFunction);
80 | promptFunction.loginChangeable = promptFunction.promptType === "promptUserAndPass";
81 | promptFunction.original = object[promptFunction.name];
82 |
83 | if (!promptFunction.original){
84 | log("Unable to find", promptFunction.name, "in", objectName);
85 | promptFunction.replacement = promptFunction.original;
86 | return;
87 | }
88 |
89 | const changedFunction = async function changedFunction(...args){
90 | if (promptFunction.hasOwnProperty("savePasswordIndex")){
91 | if (promptFunction.hasOwnProperty("savePasswordValue")){
92 | args[promptFunction.savePasswordIndex] = promptFunction.savePasswordValue;
93 | }
94 | else {
95 | args[promptFunction.savePasswordIndex].value = false;
96 | }
97 | }
98 |
99 | const data = promptFunction.promptDataFunctions.reduce((data, func) => {
100 | if (!data){
101 | return func.call(this, args);
102 | }
103 | return data;
104 | }, false);
105 | if (
106 | data &&
107 | (
108 | promptFunction.hasOwnProperty("passwordObjectIndex") ||
109 | promptFunction.setCredentials
110 | )
111 | ){
112 | const { credentials } = await requestCredentials({
113 | openChoiceDialog: true,
114 | host: data.host,
115 | login: data.login,
116 | loginChangeable: promptFunction.loginChangeable,
117 | });
118 | if (credentials.length === 1){
119 | if (promptFunction.setCredentials){
120 | promptFunction.setCredentials(args, credentials[0].login, credentials[0].password);
121 | }
122 | else {
123 | args[promptFunction.passwordObjectIndex].value = credentials[0].password;
124 | }
125 |
126 | if (promptFunction.createReturnValue){
127 | const returnValue = promptFunction.createReturnValue(credentials[0]);
128 | return returnValue;
129 | }
130 | return true;
131 | }
132 | if (data.mayAddProtocol && promptFunction.hasOwnProperty("titleIndex")){
133 | args[promptFunction.titleIndex] += ` (${data.protocol})`;
134 | }
135 | }
136 | const ret = promptFunction.original.call(this, ...args);
137 | return ret;
138 | };
139 |
140 | promptFunction.replacement = promptFunction.isAsync? changedFunction: function(...args){
141 | return waitForPromise(changedFunction.call(this, ...args), false);
142 | };
143 | }
144 |
145 | export function initPromptFunctions(promptFunctions, object, objectName){
146 | promptFunctions.forEach(function(promptFunction){
147 | initPromptFunction(promptFunction, object, objectName);
148 | });
149 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KeePassXC-Mail [](https://codebeat.co/projects/github-com-kkapsner-keepassxc-mail-master)
2 |
3 | Mozilla Thunderbird extension for [KeePassXC](https://keepassxc.org/) with Native Messaging.
4 |
5 | Based on [KeePassXC-Browser](https://github.com/keepassxreboot/keepassxc-browser) and [keebird](https://github.com/kee-org/keebird).
6 |
7 | ## Installation for KeePassXC
8 |
9 | *Hopefully we can get this as simple as for KeePassXC-Browser in the future.*
10 |
11 | 1. Configure KeePassXC-Browser as described in [this document](https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_configure_keepassxc_browser). Make sure to enable the integration for Firefox.
12 | 2. Create the configuration file for Native Messaging as described below in [Native Messaging configuration](#native-messaging-configuration).
13 | 3. Install the add-on in Thunderbird. Either install it from [addons.thunderbird.net](https://addons.thunderbird.net/thunderbird/addon/keepassxc-mail/) or download the [latest prebuilt xpi](https://github.com/kkapsner/keepassxc-mail/releases/latest) or build it yourself (`npm install`, `npm run build`, the xpi will be in the `mail-ext-artifacts` directory).
14 |
15 | ## Installation for KeePass 2
16 |
17 | 1. Install KeepassNatMsg and configure it described in [this document](https://github.com/smorks/keepassnatmsg#installation)
18 | 2. Install the add-on in Thunderbird. Either install it from [addons.thunderbird.net](https://addons.thunderbird.net/thunderbird/addon/keepassxc-mail/) or download the [latest prebuilt xpi](https://github.com/kkapsner/keepassxc-mail/releases/latest) or build it yourself (`npm install`, `npm run build`, the xpi will be in the `mail-ext-artifacts` directory).
19 |
20 | ## Native Messaging configuration
21 |
22 | ### Windows
23 |
24 | Run the following commands in the PowerShell:
25 | ```PowerShell
26 | $browserJSONPath=Get-ItemPropertyValue -path 'HKCU:\Software\Mozilla\NativeMessagingHosts\org.keepassxc.keepassxc_browser' -name '(default)'
27 | $mailJSONPath=Join-Path -path (Split-Path -path $browserJSONPath) -childPath de.kkapsner.keepassxc_mail.json
28 |
29 | cat $browserJSONPath |
30 | %{$_ -replace "keepassxc-browser@keepassxc.org","keepassxc-mail@kkapsner.de"} |
31 | %{$_ -replace "org.keepassxc.keepassxc_browser","de.kkapsner.keepassxc_mail"} |
32 | Out-File -filePath $mailJSONPath -Encoding ASCII
33 |
34 | New-Item -path 'HKCU:\Software\Mozilla\NativeMessagingHosts\de.kkapsner.keepassxc_mail' -type Directory -force
35 | Set-ItemProperty -path 'HKCU:\Software\Mozilla\NativeMessagingHosts\de.kkapsner.keepassxc_mail' -name '(default)' -value $mailJSONPath
36 | ```
37 |
38 | ### Linux
39 |
40 | Run the following command in a terminal:
41 | ```Shell
42 | cat ~/.mozilla/native-messaging-hosts/org.keepassxc.keepassxc_browser.json \
43 | | sed s/keepassxc-browser@keepassxc.org/keepassxc-mail@kkapsner.de/ \
44 | | sed s/org.keepassxc.keepassxc_browser/de.kkapsner.keepassxc_mail/ \
45 | > ~/.mozilla/native-messaging-hosts/de.kkapsner.keepassxc_mail.json
46 | ```
47 |
48 | If Thunderbird is installed via Snap or flatpak you might need to enable the native messaging via the `widget.use-xdg-desktop-portal.native-messaging` setting (set it to 1 or 2) or do some deeper workarounds. See https://github.com/kkapsner/keepassxc-mail/issues/95 for further details.
49 |
50 | ### Mac OS X
51 |
52 | Run the following command in a terminal:
53 | ```Shell
54 | cat ~/Library/Application\ Support/Mozilla/NativeMessagingHosts/org.keepassxc.keepassxc_browser.json \
55 | | sed s/keepassxc-browser@keepassxc.org/keepassxc-mail@kkapsner.de/ \
56 | | sed s/org.keepassxc.keepassxc_browser/de.kkapsner.keepassxc_mail/ \
57 | > ~/Library/Application\ Support/Mozilla/NativeMessagingHosts/de.kkapsner.keepassxc_mail.json
58 | ln -s ~/Library/Application\ Support/Mozilla/NativeMessagingHosts/ ~/Library/Mozilla/
59 | ```
60 |
61 | ## Finding entries in the password database
62 |
63 | KeePassXC-Mail uses the following schema to find matching entries for a given server:
64 |
65 | * `imap://{server name}`
66 | * `smtp://{server name}`
67 | * `pop3://{server name}`
68 | * `http://{server name}`
69 | * `https://{server name}`
70 | * `nntp-1://{server name}`
71 | * `nntp-2://{server name}`
72 | * `oauth://{account}`
73 | * `masterPassword://Thunderbird`
74 | * `openpgp://{fingerprint of the private key}`
75 |
76 | ### Tipp
77 |
78 | If you have the same user and password for receiving (imap/pop3) and sending (smtp) and do not want to duplicate your entries you can go to the "Browser Integration" section of the entry definition in KeePassXC and add the second URL there.
79 |
80 | ## Translations
81 |
82 | You can contribute to keepassxc-mail by translating it and/or improving the translations. For further instructions go to https://github.com/kkapsner/keepassxc-mail/issues/30.
83 |
84 | ## Icon
85 |
86 | Icon is based on the icon of [KeePassXC-Browser](https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-browser/icons/keepassxc.svg) - just the colors are changed to some colors of the Thunderbird.
87 |
88 | ## Privacy note
89 |
90 | KeePassXC-Mail itself does not contact any server and does not store any private data. For the automatic update process (before version 0.9) a server hosted by [PixelX](https://www.pixelx.de) is contacted. Apart of the usual server logging (IP address, access time and accessed location) nothing is stored.
91 | After version 0.9 this extension is hosted on [addons.thunderbird.net](https://addons.thunderbird.net/thunderbird/addon/keepassxc-mail/).
92 |
93 | ## Used third party scripts
94 |
95 | * https://github.com/dchest/tweetnacl-js/blob/1.0.3/nacl.min.js
96 | * https://github.com/dchest/tweetnacl-util-js/blob/v0.15.0/nacl-util.min.js
97 | * https://github.com/keepassxreboot/keepassxc-browser/blob/1.9.11/keepassxc-browser/background/client.js
98 | * https://github.com/keepassxreboot/keepassxc-browser/blob/1.9.11/keepassxc-browser/background/keepass.js
99 |
--------------------------------------------------------------------------------
/options/options.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const connectionColumns = [
3 | c => c.id,
4 | c => c.hash,
5 | c => c.key.substring(0, 8) + "*".repeat(5),
6 | c => new Date(c.lastUsed).toLocaleString(),
7 | c => new Date(c.created).toLocaleDateString(),
8 | ];
9 | function createConnectionDisplay(connection){
10 | const container = document.createElement("tr");
11 |
12 | connectionColumns.forEach(function(column){
13 | const cell = document.createElement("td");
14 | cell.textContent = column(connection);
15 | cell.title = cell.textContent;
16 | container.appendChild(cell);
17 | });
18 |
19 | return container;
20 | }
21 | async function updateConnections(){
22 | const keyRing = (await browser.storage.local.get({"keyRing": {}})).keyRing;
23 |
24 | const connections = document.getElementById("connections");
25 | connections.innerHTML = "";
26 | Object.keys(keyRing).forEach(function(hash){
27 | connections.appendChild(createConnectionDisplay(keyRing[hash]));
28 | });
29 | }
30 | updateConnections();
31 |
32 | function createPrivilegeInput(extension, privileges, privilegesApi, type){
33 | const input = document.createElement("input");
34 | input.type = "checkbox";
35 | const state = privileges[type];
36 | input.checked = state;
37 | input.indeterminate = state === undefined;
38 | input.addEventListener("change", function(){
39 | privilegesApi.setPrivileges(extension.id, type, input.checked);
40 | });
41 | return input;
42 | }
43 |
44 | function createPrivilegeResetButton(extension, privileges, privilegesApi, reset){
45 | const button = document.createElement("button");
46 | button.textContent = "🗑";
47 | button.addEventListener("click", async function(){
48 | await privilegesApi.setPrivileges(extension.id, "request", undefined);
49 | await privilegesApi.setPrivileges(extension.id, "store", undefined);
50 | await reset();
51 | });
52 | return button;
53 | }
54 |
55 | const privilegeColumns = [
56 | e => {
57 | const name = document.createElement("span");
58 | name.textContent = e.name;
59 | name.title = e.id;
60 | return name;
61 | },
62 | (e, p, api) => createPrivilegeInput(e, p, api, "request"),
63 | (e, p, api) => createPrivilegeInput(e, p, api, "store"),
64 | (e, p, api, reset) => createPrivilegeResetButton(e, p, api, reset),
65 | ];
66 | async function createPrivilegesDisplay(extension, privilegesApi){
67 | const container = document.createElement("tr");
68 | async function reset(){
69 | const privileges = await privilegesApi.getPrivileges(extension.id);
70 | container.innerHTML = "";
71 | privilegeColumns
72 | .map(c => c(extension, privileges, privilegesApi, reset))
73 | .forEach(function(content){
74 | const cell = document.createElement("td");
75 | if (!(content instanceof Node)){
76 | cell.title = content;
77 | content = document.createTextNode(content);
78 | }
79 | cell.appendChild(content);
80 | container.appendChild(cell);
81 | });
82 | }
83 | reset();
84 | return container;
85 | }
86 |
87 | async function updatePrivileges(){
88 | const [privilegesApi, extensions, self] = await Promise.all([
89 | import("../modules/externalPrivileges.js"),
90 | browser.management.getAll(),
91 | browser.management.getSelf(),
92 | ]);
93 |
94 | const privileges = document.getElementById("privileges");
95 | let onePresent = false;
96 | privileges.innerHTML = "";
97 | const rows = await Promise.all(
98 | extensions
99 | .filter(extension => extension.type === "extension" && extension.id !== self.id)
100 | .map(extension => createPrivilegesDisplay(extension, privilegesApi))
101 | );
102 | rows.forEach(row => {
103 | onePresent = true;
104 | privileges.appendChild(row);
105 | });
106 | if (!onePresent){
107 | document.getElementById("privilegesSection").style.display = "none";
108 | }
109 | }
110 | updatePrivileges();
111 |
112 | const actions = {
113 | clearSelectedEntries: async function(){
114 | const backgroundPage = browser.extension.getBackgroundPage();
115 | const selectedModule = await backgroundPage.selectedModule;
116 | selectedModule.clearSelectedEntries();
117 | },
118 | reconnect: async function(){
119 | const backgroundPage = browser.extension.getBackgroundPage();
120 | const { connect, disconnect } = await backgroundPage.isKeepassReady();
121 | await disconnect();
122 | await connect(true);
123 | await backgroundPage.keepass.associate();
124 | await updateConnections();
125 | },
126 | associate: async function(){
127 | await browser.extension.getBackgroundPage().keepass.associate();
128 | await updateConnections();
129 | }
130 | };
131 |
132 | async function wait(ms){
133 | return new Promise(function(resolve){
134 | window.setTimeout(function(){
135 | resolve();
136 | }, ms);
137 | });
138 | }
139 |
140 | document.querySelectorAll(".action").forEach(async function(button){
141 | const activeMessageId = button.dataset.activeMessage;
142 | const activeMessage = activeMessageId? browser.i18n.getMessage(activeMessageId): false;
143 | let active = false;
144 | button.addEventListener("click", async function(){
145 | if (active){
146 | return;
147 | }
148 | const oldContent = button.textContent;
149 | button.disabled = true;
150 | active = true;
151 | const promises = [actions[button.id]()];
152 | if (activeMessage){
153 | button.textContent = activeMessage;
154 | promises.push(wait(500));
155 | }
156 | await Promise.all(promises);
157 | button.textContent = oldContent;
158 | active = false;
159 | button.disabled = false;
160 | });
161 | });
162 | document.querySelectorAll("input.setting").forEach(async function(input){
163 | const settingName = input.id;
164 | const currentValue = await browser.storage.local.get([settingName]);
165 | let type = typeof currentValue[settingName];
166 | if (type === "undefined"){
167 | currentValue[settingName] = JSON.parse(input.dataset.defaultValue);
168 | type = typeof currentValue[settingName];
169 | }
170 | switch (type){
171 | case "boolean":
172 | input.checked = currentValue[settingName];
173 | break;
174 | default:
175 | input.value = currentValue[settingName];
176 | }
177 | input.addEventListener("change", function(){
178 | let newValue = input.value;
179 | switch (typeof currentValue[settingName]){
180 | case "boolean":
181 | newValue = input.checked;
182 | break;
183 | case "number":
184 | newValue = parseFloat(input.value);
185 | break;
186 | }
187 | browser.storage.local.set({
188 | [settingName]: newValue
189 | });
190 | });
191 | });
192 |
193 | document.querySelectorAll("*[data-translation]").forEach(function(node){
194 | node.textContent = browser.i18n.getMessage(node.dataset.translation);
195 | });
--------------------------------------------------------------------------------
/releaseNotes.txt:
--------------------------------------------------------------------------------
1 | Version 1.15
2 |
3 | new features:
4 |
5 | - added support for ldaps
6 | - added database unlock functionality
7 | - added a notification when passwords are stored in the Thunderbird password manager
8 |
9 | changes:
10 |
11 | -
12 |
13 | fixes:
14 |
15 | - sometimes passwords were stored in the Thunderbird password manager
16 | - native password prompts for LDAP did not fetch passwords properly
17 | - use correct return values in LoginManagerAuthPrompter
18 | - use correct return values in Prompter
19 | - support for LDAP was still broken
20 |
21 | Version 1.14
22 |
23 | changes:
24 |
25 | - bump version support to 148.*
26 | - remove integration with browser request windows
27 | - update keepass.js to version 1.9.11 (no changes)
28 | - update client.js to version 1.9.11 (no changes)
29 |
30 | fixes:
31 |
32 | - "Clear storage of selected entries" caused an error
33 | - support for LDAP was broken
34 | - added missing translations for new error messages form keepassxc-browser
35 | - sometimes passwords were stored in the Thunderbird password manager
36 |
37 | Version 1.13
38 |
39 | new features:
40 |
41 | - bump version support to 145.*
42 |
43 | changes:
44 |
45 | - update keepass.js to version 1.9.10
46 | - update client.js to version 1.9.10 (no changes)
47 |
48 | Version 1.12
49 |
50 | new features:
51 |
52 | - new translations
53 | - bump version support to 141.*
54 |
55 | changes:
56 |
57 | - update keepass.js to version 1.9.8
58 | - update client.js to version 1.9.8 (no changes)
59 |
60 | Version 1.11
61 |
62 | new features:
63 |
64 | - bump version support to 139.0
65 | - add API for other mail extensions to request and store passwords
66 |
67 | changes:
68 |
69 | - new version scheme
70 | - major code refactoring
71 | - remove support for Cardbook (as requested by ATN)
72 | - bump minimal version to 128 (older not tested after code refactoring)
73 |
74 | fixes:
75 |
76 | - reduce delay when resizing a modal window
77 | - show correct code location for logging
78 |
79 | Version 1.10.1
80 |
81 | new features:
82 |
83 | - new translations
84 |
85 | changes:
86 |
87 | - update keepass.js to version 1.9.7 (no changes)
88 | - update client.js to version 1.9.7 (no changes)
89 |
90 | fixes:
91 |
92 | - fix support for Cardbook
93 |
94 | Version 1.10
95 |
96 | new features:
97 |
98 | - bump version support to 138.0
99 |
100 | changes:
101 |
102 | - update keepass.js to version 1.9.6 (no changes)
103 | - update client.js to version 1.9.6 (no changes)
104 |
105 | Version 1.9.1
106 |
107 | fixes:
108 |
109 | - added missing dependency from keepassxc-browser
110 |
111 | Version 1.9
112 |
113 | new features:
114 |
115 | - bump version support to 135.0
116 | - added experimental startup control
117 |
118 | changes:
119 |
120 | - improve display of database connections
121 | - update keepass.js to version 1.9.5
122 | - update client.js to version 1.9.5 (no changes)
123 |
124 | fixes:
125 |
126 | - auto submitted entries are not considered when storing new passwords
127 |
128 | Version 1.8
129 |
130 | new features:
131 |
132 | - bump version support to 129.0
133 |
134 | changes:
135 |
136 | - update keepass.js to version 1.9.1
137 | - update client.js to version 1.9.1
138 |
139 | fixes:
140 |
141 | - do not use innerHTML assignments
142 |
143 | Version 1.7
144 |
145 | new features:
146 |
147 | - added LDAP support
148 | - added timeout to password saving
149 | - bump version support to 126.0
150 |
151 | changes:
152 |
153 | - update keepass.js to version 1.9.0.2
154 | - update client.js to version 1.9.0.2
155 |
156 | fixes:
157 |
158 | - update OAuth handling for 126.0
159 |
160 | Version 1.6.1
161 |
162 | fixes:
163 |
164 | - was not working in 117 nightly due to changes in openpgp internationalization
165 | - also not working in 115 release
166 |
167 | Version 1.6
168 |
169 | new features:
170 |
171 | - bump version support to 117.0
172 |
173 | Version 1.5
174 |
175 | new features:
176 |
177 | - bump version support to 113.0
178 |
179 | changes:
180 |
181 | - update keepass.js to version 1.8.6
182 | - update client.js to version 1.8.6
183 | - improve support for cardbook
184 |
185 | fixes:
186 |
187 | - do not save password for CardDAV in Thunderbird password manager
188 |
189 | Version 1.4
190 |
191 | new features:
192 |
193 | - bump version support to 111.0
194 | - save oauth token to database
195 | - allow overwriting existing database entries
196 |
197 | changes:
198 |
199 | - update keepass.js to version 1.8.3.1
200 | - update client.js to version 1.8.3.1
201 | - improve modal dialog communication
202 | - do not show credential picker on oauth window when password cannot be entered
203 | - choice dialog: also respect "do not ask again" when cancel is clicked
204 | - add "do not ask again" to saving password dialog
205 | - improve logging on password saving
206 | - add cache to getter of oauth
207 |
208 | fixes:
209 |
210 | - modal dialog size
211 |
212 | Version 1.3
213 |
214 | new features:
215 |
216 | - bump version support to 107.0
217 |
218 | fixes:
219 |
220 | - make experiment multi process save
221 |
222 | Version 1.2
223 |
224 | new features:
225 |
226 | - bump version support to 106.0
227 | - prevent password prompts when possible
228 |
229 | changes:
230 |
231 | - own choice dialog respects auto submit
232 | - use own choice dialog
233 | - code cleanup
234 | - also save new password if an entry with a different password is found in the database
235 |
236 | fixes:
237 |
238 | - improve startup behaviour
239 |
240 | Version 1.1
241 |
242 | new features:
243 |
244 | - add support for openpgp private key password prompts
245 | - bump version support to 105.0
246 | - added entry selection dialog
247 |
248 | Version 1.0.3
249 |
250 | new features:
251 |
252 | - bump version support to 104.0
253 |
254 | changes:
255 |
256 | - filtering for correct login is now case insensitive
257 | - update keepass.js to version 1.8.0
258 | - added client.js from version 1.8.0
259 | - bump minimal version to 74.0 (needed for keepass.js and client.js)
260 |
261 | Version 1.0.2.1
262 |
263 | new features:
264 |
265 | - bump version support to 102.* for ESR
266 |
267 | Version 1.0.2
268 |
269 | fixes:
270 |
271 | - modal dialog not working due to too tight timing
272 |
273 | Version 1.0.1
274 |
275 | new features:
276 |
277 | - try to connect later if initial connection failed
278 |
279 | fixes:
280 |
281 | - check all native application names on reconnect
282 |
283 | Version 1.0
284 |
285 | new features:
286 |
287 | - added support for different native application names
288 |
289 | changes:
290 |
291 | - update tweetnacl to version 1.0.3
292 | - update keepass.js to version 1.7.11
293 |
294 | Version 0.9
295 |
296 | new features:
297 |
298 | - distribution is now over https://addons.thunderbird.net/thunderbird/addon/keepassxc-mail/
299 |
300 | Version 0.1.12
301 |
302 | fixes:
303 |
304 | - password prompts without a given username did not find any password in newer Thunderbird version
305 |
306 | Version 0.1.11
307 |
308 | fixes:
309 |
310 | - entries without login name were filtered when no login was expected
311 |
312 | Version 0.1.10
313 |
314 | new features:
315 |
316 | - new translations
317 |
318 | changes:
319 |
320 | - bump version support to 98.*
321 |
322 | fixes:
323 |
324 | - primary password prompt was not recognized
325 |
326 | Version 0.1.9.1
327 |
328 | fixes:
329 |
330 | - oauth authentication, gdata support and cardbook support not working due removed function spinEventLoopUntilOrShutdown in Thunderbird 91
331 |
332 | Version 0.1.9
333 |
334 | new features:
335 |
336 | - added support for Thunderbird 94.*
337 | - new translations
338 |
339 | Version 0.1.8
340 |
341 | new features:
342 |
343 | - add confirmation dialog before saving to database
344 | - if the saving is denied oauth tokens are written to the built in password manager
345 | - added support for Cardbook
346 |
347 | Version 0.1.7.1
348 |
349 | fixes:
350 |
351 | - primary password was saved too often in database
352 |
353 | Version 0.1.7
354 |
355 | new features:
356 |
357 | - added support for primary password
358 | - added support for oauth
359 |
360 | Version 0.1.6
361 |
362 | new features:
363 |
364 | - new translations
365 |
366 | changes:
367 |
368 | - bump version support to 86.*
369 |
370 | fixes:
371 |
372 | - check for already existing credential before saving
373 | - realm information may contain wrong user name
374 |
375 | Version 0.1.5
376 |
377 | new features:
378 |
379 | - hovering over the status text in a password prompt show now the password search parameters
380 | - password search parameters are logged in the console
381 |
382 | changes:
383 |
384 | - add new dialog texts
385 | - "mailbox://..." is now "pop3://..." again
386 |
387 | fixes:
388 |
389 | - key ring not initialized during startup
390 | - realm information may contain wrong server URL
391 |
392 | Version 0.1.4:
393 |
394 | new features:
395 |
396 | - hide the "save password" checkbox
397 | - added support for Thunderbird 80
398 |
399 | Version 0.1.3:
400 |
401 | new features:
402 |
403 | - save new credentials to database
404 |
405 | fixes:
406 |
407 | - enable usage of KeePass with KeePassNatMsg
408 | - fix connection display being duplicated upon reassociation
409 | - removed hard dependency on Lightning
410 |
411 | Version 0.1.2:
412 |
413 | new features:
414 |
415 | - added auto update
416 | - added oauth token storage support
417 |
418 | fixes:
419 |
420 | - do not break if a string bundle is not present
421 |
422 | Version 0.1.1:
423 |
424 | new features:
425 |
426 | - added support for "Provider for Google Calendar"
427 |
428 | fixes:
429 |
430 | - respect skipAutoSubmit
431 |
432 | Version 0.1.0:
433 |
434 | First MVP that supports IMAP, POP3, SMTP and calendar password prompts.
435 |
--------------------------------------------------------------------------------
/_locales/zh_CN/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "errorMessageUnknown": {
3 | "message": "未知错误。",
4 | "description": "Unknown error."
5 | },
6 | "errorMessageDatabaseNotOpened": {
7 | "message": "数据库未打开。",
8 | "description": "Database not opened."
9 | },
10 | "errorMessageDatabaseHash": {
11 | "message": "未收到数据库哈希。",
12 | "description": "Database hash not received."
13 | },
14 | "errorMessageClientPublicKey": {
15 | "message": "未收到客户端公钥。",
16 | "description": "Client public key not received."
17 | },
18 | "errorMessageDecrypt": {
19 | "message": "无法解密消息。",
20 | "description": "Cannot decrypt message."
21 | },
22 | "errorMessageTimeout": {
23 | "message": "无法连接到 KeePassXC。请检查 KeePassXC 设置中是否启用了浏览器集成。",
24 | "description": "Cannot connect to KeePassXC. Check that browser integration is enabled in KeePassXC settings."
25 | },
26 | "errorMessageCanceled": {
27 | "message": "操作被取消或被拒绝。",
28 | "description": "Action canceled or denied."
29 | },
30 | "errorMessageEncrypt": {
31 | "message": "消息加密失败。KeepassXC 是否已运行?",
32 | "description": "Message encryption failed. Is KeePassXC running?"
33 | },
34 | "errorMessageAssociate": {
35 | "message": "KeePassXC 关联失败,请重试。",
36 | "description": "KeePassXC association failed, try again."
37 | },
38 | "errorMessageKeyExchange": {
39 | "message": "密钥交换失败。",
40 | "description": "Key exchange was not successful."
41 | },
42 | "errorMessageEncryptionKey": {
43 | "message": "无法识别加密密钥。",
44 | "description": "Encryption key is not recognized."
45 | },
46 | "errorMessageSavedDatabases": {
47 | "message": "未找到保存的数据库。",
48 | "description": "No saved databases found."
49 | },
50 | "errorMessageIncorrectAction": {
51 | "message": "错误操作。",
52 | "description": "Incorrect action."
53 | },
54 | "errorMessageEmptyMessage": {
55 | "message": "收到的消息为空。",
56 | "description": "Empty message received."
57 | },
58 | "errorMessageNoURL": {
59 | "message": "未提供 URL。",
60 | "description": "No URL provided."
61 | },
62 | "errorMessageNoLogins": {
63 | "message": "未找到登录信息。",
64 | "description": "No logins found."
65 | },
66 | "errorMessageNoGroupsFound": {
67 | "message": "No groups found.",
68 | "description": "No groups found."
69 | },
70 | "errorMessageCannotCreateNewGroup": {
71 | "message": "Cannot create new group.",
72 | "description": "Cannot create new group."
73 | },
74 | "errorMessageNoValidUuidProvided": {
75 | "message": "No valid UUID provided.",
76 | "description": "No valid UUID provided."
77 | },
78 | "errorMessageAccessToAllEntriesDenied": {
79 | "message": "Access to all entries denied.",
80 | "description": "Access to all entries denied."
81 | },
82 | "errorMessagePasskeysAttestationNotSupported": {
83 | "message": "Attestation not supported.",
84 | "description": "Attestation not supported."
85 | },
86 | "errorMessagePasskeysCredentialIsExcluded": {
87 | "message": "Credential is excluded.",
88 | "description": "Credential is excluded."
89 | },
90 | "errorMessagePasskeysRequestCanceled": {
91 | "message": "Passkeys request canceled.",
92 | "description": "Passkeys request canceled."
93 | },
94 | "errorMessagePasskeysInvalidUserVerification": {
95 | "message": "Invalid user verification.",
96 | "description": "Invalid user verification."
97 | },
98 | "errorMessagePasskeysEmptyPublicKey": {
99 | "message": "Empty public key.",
100 | "description": "Empty public key."
101 | },
102 | "errorMessagePasskeysInvalidUrlProvided": {
103 | "message": "Invalid URL provided.",
104 | "description": "Invalid URL provided."
105 | },
106 | "errorMessagePasskeysOriginNotAllowed": {
107 | "message": "Origin is empty or not allowed.",
108 | "description": "Origin is empty or not allowed."
109 | },
110 | "errorMessagePasskeysDomainNotValid": {
111 | "message": "Effective domain is not a valid domain.",
112 | "description": "Effective domain is not a valid domain."
113 | },
114 | "errorMessagePasskeysDomainRpIdMismatch": {
115 | "message": "Origin and RP ID do not match.",
116 | "description": "Origin and RP ID do not match."
117 | },
118 | "errorMessagePasskeysNoSupportedAlgorithms": {
119 | "message": "No supported algorithms were provided.",
120 | "description": "No supported algorithms were provided."
121 | },
122 | "errorMessagePasskeysWaitforLifeTimer": {
123 | "message": "Wait for timer to expire.",
124 | "description": "Wait for timer to expire."
125 | },
126 | "errorMessagePasskeysUnknownError": {
127 | "message": "Unknown passkeys error.",
128 | "description": "Unknown passkeys error."
129 | },
130 | "errorMessagePasskeysInvalidChallenge": {
131 | "message": "Challenge is shorter than required minimum length.",
132 | "description": "Challenge is shorter than required minimum length."
133 | },
134 | "errorMessagePasskeysInvalidUserId": {
135 | "message": "user.id does not match the required length.",
136 | "description": "user.id does not match the required length."
137 | },
138 | "loadingPasswords": {
139 | "message": "正在加载密码...",
140 | "description": "Loading passwords..."
141 | },
142 | "noPasswordsFound": {
143 | "message": "找不到密码。",
144 | "description": "No passwords found."
145 | },
146 | "retry": {
147 | "message": "重试",
148 | "description": "Retry"
149 | },
150 | "pickedEntry": {
151 | "message": "已选中条目:{name|login|\"---\"}",
152 | "description": "Picked entry \"{name|login|\"---\"}\""
153 | },
154 | "entryLabel": {
155 | "message": "{name|\"Login\"}: {login}",
156 | "description": "{name|\"Login\"}: {login}"
157 | },
158 | "entryTooltip": {
159 | "message": "Password \"{password}\"",
160 | "description": "Password \"{password}\""
161 | },
162 | "credentialInfo": {
163 | "message": "URL: {host}\nLogin: {login|\"---\"}",
164 | "description": "URL: {host}\nLogin: {login}"
165 | },
166 | "options.title": {
167 | "message": "KeePassXC-Mail 设置",
168 | "description": "KeePassXC-Mail Settings"
169 | },
170 | "options.databases": {
171 | "message": "已连接数据库",
172 | "description": "Database connections"
173 | },
174 | "options.clearSelectedEntries": {
175 | "message": "Clear storage of selected entries",
176 | "description": "Clear storage of selected entries"
177 | },
178 | "options.clearingSelectedEntries": {
179 | "message": "Clearing storage of selected entries...",
180 | "description": "Clearing storage of selected entries..."
181 | },
182 | "options.reconnect": {
183 | "message": "重新连接到 Native Messaging",
184 | "description": "Reconnect to Native Messaging"
185 | },
186 | "options.reconnecting": {
187 | "message": "正在重新连接到 Native Messaging...",
188 | "description": "Reconnecting to Native Messaging..."
189 | },
190 | "options.associate": {
191 | "message": "连接 KeePassXC",
192 | "description": "Connect to KeePassXC"
193 | },
194 | "options.associating": {
195 | "message": "正在连接到 KeePassXC...",
196 | "description": "Connecting to KeePassXC..."
197 | },
198 | "options.autoSubmit.label": {
199 | "message": "自动提交",
200 | "description": "Auto submit"
201 | },
202 | "options.saveNewCredentials.label": {
203 | "message": "保存新凭据",
204 | "description": "Save new credentials"
205 | },
206 | "options.autoSaveNewCredentials.label": {
207 | "message": "保存新凭据时不提示",
208 | "description": "Save new credentials without prompt"
209 | },
210 | "options.connections.id": {
211 | "message": "标识符",
212 | "description": "ID"
213 | },
214 | "options.connections.dbHash": {
215 | "message": "数据库哈希",
216 | "description": "DB hash"
217 | },
218 | "options.connections.key": {
219 | "message": "密钥",
220 | "description": "Key"
221 | },
222 | "options.connections.lastUsed": {
223 | "message": "上次使用",
224 | "description": "Last used"
225 | },
226 | "options.connections.created": {
227 | "message": "创建时间",
228 | "description": "Created"
229 | },
230 | "options.externalPrivileges": {
231 | "message": "External privileges",
232 | "description": "External privileges"
233 | },
234 | "options.externalPrivileges.extensionName": {
235 | "message": "extension",
236 | "description": "extension"
237 | },
238 | "options.externalPrivileges.request": {
239 | "message": "request",
240 | "description": "request"
241 | },
242 | "options.externalPrivileges.store": {
243 | "message": "store",
244 | "description": "store"
245 | },
246 | "modal.savingPassword.title": {
247 | "message": "保存 {host} 的密码到KeePass 数据库?",
248 | "description": "Save password for {host} to KeePass database?"
249 | },
250 | "modal.savingPassword.questionWithLogin": {
251 | "message": "你想保存 {login} 在 {host} 邮箱服务密码到 KeePass 数据库吗?",
252 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?"
253 | },
254 | "modal.savingPassword.questionWithoutLogin": {
255 | "message": "你想保存 {host} 邮箱服务密码到 KeePass 数据库吗?",
256 | "description": "Do you want to save the entered password for {host} to the KeePass database?"
257 | },
258 | "modal.savingPassword.newEntry": {
259 | "message": " - 创建新条目 - ",
260 | "description": " - create new entry - "
261 | },
262 | "modal.savingPassword.yes": {
263 | "message": "是",
264 | "description": "Yes"
265 | },
266 | "modal.savingPassword.no": {
267 | "message": "否",
268 | "description": "No"
269 | },
270 | "modal.choice.title": {
271 | "message": "选择正确的登录配置项来登录 {host}",
272 | "description": "Select correct entry for {host}"
273 | },
274 | "modal.choice.textWithLogin": {
275 | "message": "请为 {login} 在 {host} 邮箱服务中选择正确的登录配置项。",
276 | "description": "Please select the correct entry for {login} on {host}."
277 | },
278 | "modal.choice.textWithoutLogin": {
279 | "message": "请为 {host} 邮箱服务中选择正确的登录配置项。",
280 | "description": "Please select the correct entry for {host}."
281 | },
282 | "modal.choice.doNotAskAgain": {
283 | "message": "不再询问。",
284 | "description": "Do not ask again."
285 | },
286 | "modal.choice.ok": {
287 | "message": "确定",
288 | "description": "OK"
289 | },
290 | "modal.choice.cancel": {
291 | "message": "取消",
292 | "description": "Cancel"
293 | },
294 | "modal.confirm.yes": {
295 | "message": "Yes",
296 | "description": "Yes"
297 | },
298 | "modal.confirm.no": {
299 | "message": "No",
300 | "description": "No"
301 | },
302 | "modal.message.ok": {
303 | "message": "好的",
304 | "description": "OK"
305 | },
306 | "createPasswordGroup.timeout.title": {
307 | "message": "创建密码分组超时",
308 | "description": "Creating password group timeout"
309 | },
310 | "createPasswordGroup.timeout.message": {
311 | "message": "{account} 的密码保存失败。密码已保存到Thunderbird的内置密码管理器中。\n\n创建密码分组超时。请检查 KeePassXC 设置是否允许创建群组。",
312 | "description": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nCreating the password group timed out. Please check your KeePassXC settings to allow group creation."
313 | },
314 | "savePassword.timeout.title": {
315 | "message": "保存密码超时",
316 | "description": "Saving password timeout"
317 | },
318 | "savePassword.timeout.message": {
319 | "message": "{account} 的密码保存失败。密码已保存到Thunderbird的内置密码管理器中。\n\n请检查 KeePassXC 设置是否允许保存或更新密码。",
320 | "description": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nPlease check your KeePassXC settings to allow password saving and updating."
321 | },
322 | "unlockDatabase.title": {
323 | "message": "Locked database",
324 | "description": "Locked database"
325 | },
326 | "unlockDatabase.question": {
327 | "message": "Your database is locked.\n\nDo you want to unlock it?",
328 | "description": "Your database is locked.\n\nDo you want to unlock it?"
329 | },
330 | "unlockDatabase.failed": {
331 | "message": "The database was not unlocked in time.\n\nPassword retrieval and storage is not possible with a locked database.",
332 | "description": "The database was not unlocked in time.\n\nPassword retrieval and storage is not possible with a locked database."
333 | },
334 | "passwordsStoredInThunderbird.title": {
335 | "message": "Password stored in Thunderbird",
336 | "description": "Password stored in Thunderbird"
337 | },
338 | "passwordsStoredInThunderbird.message": {
339 | "message": "There are passwords stored in the Thunderbird password vault.\n\nPlease review if this is intended.",
340 | "description": "There are passwords stored in the Thunderbird password vault.\n\nPlease review if this is intended."
341 | },
342 | "passwordsStoredInThunderbird.newStored": {
343 | "message": "New passwords were added to the Thunderbird password vault.\n\nPlease review if this is intended.",
344 | "description": "New passwords were added to the Thunderbird password vault.\n\nPlease review if this is intended."
345 | },
346 | "privilegeRequest.title": {
347 | "message": "Privilege request",
348 | "description": "Privilege request"
349 | },
350 | "privilegeRequest.message": {
351 | "message": "{typeMessage}\n\nDo you want to allow this?\n\nThis decision will be stored for further requests and can be changed in the extension preferences.",
352 | "description": "{typeMessage}\n\nDo you want to allow this?\n\nThis decision will be stored for further requests and can be changed in the extension preferences."
353 | },
354 | "privilegeRequest.message.request": {
355 | "message": "The extension {extensionName} wants to request passwords.",
356 | "description": "The extension {extensionName} wants to request passwords."
357 | },
358 | "privilegeRequest.message.store": {
359 | "message": "The extension {extensionName} wants to store passwords.",
360 | "description": "The extension {extensionName} wants to store passwords."
361 | }
362 | }
--------------------------------------------------------------------------------
/experiment/modules/dialogStrings.sys.js:
--------------------------------------------------------------------------------
1 | /* globals Components, Localization */
2 | import { log } from "./log.sys.js";
3 |
4 | export const {getCredentialInfoFromStrings, getCredentialInfoFromStringsAndProtocol, addDialogType} = function(){
5 | const stringBundleService = Components.classes["@mozilla.org/intl/stringbundle;1"]
6 | .getService(Components.interfaces.nsIStringBundleService);
7 |
8 | const bundles = {
9 | commonDialog: stringBundleService.createBundle("chrome://global/locale/commonDialogs.properties"),
10 | compose: stringBundleService.createBundle("chrome://messenger/locale/messengercompose/composeMsgs.properties"),
11 | imap: stringBundleService.createBundle("chrome://messenger/locale/imapMsgs.properties"),
12 | ldap: stringBundleService.createBundle("chrome://mozldap/locale/ldap.properties"),
13 | local: stringBundleService.createBundle("chrome://messenger/locale/localMsgs.properties"),
14 | messenger: stringBundleService.createBundle("chrome://messenger/locale/messenger.properties"),
15 | news: stringBundleService.createBundle("chrome://messenger/locale/news.properties"),
16 | wcap: stringBundleService.createBundle("chrome://calendar/locale/wcap.properties"),
17 | pipnss: stringBundleService.createBundle("chrome://pipnss/locale/pipnss.properties"),
18 | };
19 |
20 | const dialogTypes = [];
21 | function getBundleString(bundleName, stringName){
22 | try {
23 | return bundles[bundleName].GetStringFromName(stringName);
24 | }
25 | catch(e){
26 | log("unable to get", stringName, "from bundle", bundleName);
27 | return {bundleName, stringName, replace: () => "", indexOf: () => -1, notFound: true};
28 | }
29 | }
30 | function getDialogType({
31 | protocol, title, titleRegExp, dialog, hostPlaceholder, loginPlaceholder, otherPlaceholders, noLoginRequired
32 | }){
33 | if (Array.isArray(title)){
34 | title = getBundleString(...title);
35 | }
36 | if (Array.isArray(dialog)){
37 | dialog = getBundleString(...dialog);
38 | }
39 | const hostPosition = hostPlaceholder? dialog.indexOf(hostPlaceholder): -1;
40 | const loginPosition = loginPlaceholder? dialog.indexOf(loginPlaceholder): -1;
41 | let dialogRegExpString = dialog.replace(/([\\+*?[^\]$(){}=!|.])/g, "\\$1");
42 | if (hostPlaceholder){
43 | dialogRegExpString = dialogRegExpString.replace(hostPlaceholder.replace(/\$/g, "\\$"), "(.+)");
44 | }
45 | if (loginPlaceholder){
46 | dialogRegExpString = dialogRegExpString.replace(loginPlaceholder.replace(/\$/g, "\\$"), "(.+)");
47 | }
48 | if (otherPlaceholders){
49 | otherPlaceholders.forEach(function(otherPlaceholder){
50 | dialogRegExpString = dialogRegExpString.replace(otherPlaceholder.replace(/\$/g, "\\$"), ".+");
51 | });
52 | }
53 | const dialogRegExp = new RegExp(dialogRegExpString);
54 |
55 | return {
56 | useable: !title.notFound && !dialog.notFound,
57 | protocol,
58 | title: titleRegExp?
59 | new RegExp(title.replace(/([\\+*?[^\]$(){}=!|.])/g, "\\$1").replace(/%(?:\d+\\\$)?S/, ".+")):
60 | title,
61 | dialog,
62 | dialogRegExp,
63 | hostGroup: hostPosition === -1? false: loginPosition === -1 || loginPosition > hostPosition? 1: 2,
64 | loginGroup: loginPosition === -1? false: hostPosition === -1 || hostPosition > loginPosition? 1: 2,
65 | otherProtocolExists: false,
66 | noLoginRequired,
67 | createTypeWithProtocolInTitle: function(){
68 | const newTitle = title + ` (${protocol})`;
69 | const withTitleType = addDialogType({
70 | protocol, title: newTitle, titleRegExp, dialog,
71 | hostPlaceholder, loginPlaceholder, otherPlaceholders,
72 | noLoginRequired
73 | });
74 | this.createTypeWithProtocolInTitle = () => {};
75 | },
76 | };
77 | }
78 | function addDialogType(data){
79 | const dialogType = getDialogType(data);
80 | if (dialogType.useable){
81 | dialogTypes.some(function(otherDialogType){
82 | if (
83 | otherDialogType.protocol !== dialogType.protocol &&
84 | otherDialogType.title.toString() === dialogType.title.toString() &&
85 | otherDialogType.dialogRegExp.toString() === dialogType.dialogRegExp.toString()
86 | ){
87 | if (!otherDialogType.otherProtocolExists){
88 | otherDialogType.createTypeWithProtocolInTitle();
89 | }
90 | otherDialogType.otherProtocolExists = true;
91 | dialogType.createTypeWithProtocolInTitle();
92 | dialogType.otherProtocolExists = true;
93 | return true;
94 | }
95 | return false;
96 | });
97 | dialogTypes.push(dialogType);
98 | }
99 | else {
100 | // log("Not useable dialog type:", data);
101 | }
102 | return dialogType;
103 | }
104 |
105 | addDialogType({
106 | protocol: "smtp",
107 | title: getBundleString("compose", "smtpEnterPasswordPromptTitle"),
108 | dialog: getBundleString("compose", "smtpEnterPasswordPromptWithUsername"),
109 | hostPlaceholder: "%1$S",
110 | loginPlaceholder: "%2$S"
111 | });
112 | addDialogType({
113 | protocol: "smtp",
114 | title: getBundleString("compose", "smtpEnterPasswordPromptTitle"),
115 | dialog: getBundleString("compose", "smtpEnterPasswordPrompt"),
116 | hostPlaceholder: "%S",
117 | loginPlaceholder: ""
118 | });
119 | addDialogType({
120 | protocol: "smtp",
121 | title: getBundleString("compose", "smtpEnterPasswordPromptTitleWithHostname"),
122 | titleRegExp: true,
123 | dialog: getBundleString("compose", "smtpEnterPasswordPromptWithUsername"),
124 | hostPlaceholder: "%1$S",
125 | loginPlaceholder: "%2$S"
126 | });
127 | addDialogType({
128 | protocol: "smtp",
129 | title: getBundleString("compose", "smtpEnterPasswordPromptTitleWithHostname"),
130 | titleRegExp: true,
131 | dialog: getBundleString("compose", "smtpEnterPasswordPrompt"),
132 | hostPlaceholder: "%S",
133 | loginPlaceholder: ""
134 | });
135 | addDialogType({
136 | protocol: "imap",
137 | title: getBundleString("imap", "imapEnterPasswordPromptTitle"),
138 | dialog: getBundleString("imap", "imapEnterServerPasswordPrompt"),
139 | hostPlaceholder: "%2$S",
140 | loginPlaceholder: "%1$S"
141 | });
142 | addDialogType({
143 | protocol: "imap",
144 | title: getBundleString("imap", "imapEnterPasswordPromptTitleWithUsername"),
145 | titleRegExp: true,
146 | dialog: getBundleString("imap", "imapEnterServerPasswordPrompt"),
147 | hostPlaceholder: "%2$S",
148 | loginPlaceholder: "%1$S"
149 | });
150 | addDialogType({
151 | protocol: "pop3",
152 | title: getBundleString("local", "pop3EnterPasswordPromptTitle"),
153 | dialog: getBundleString("local", "pop3EnterPasswordPrompt"),
154 | hostPlaceholder: "%2$S",
155 | loginPlaceholder: "%1$S"
156 | });
157 | addDialogType({
158 | protocol: "pop3",
159 | title: getBundleString("local", "pop3EnterPasswordPromptTitle"),
160 | dialog: getBundleString("local", "pop3PreviouslyEnteredPasswordIsInvalidPrompt"),
161 | hostPlaceholder: "%2$S",
162 | loginPlaceholder: "%1$S"
163 | });
164 | addDialogType({
165 | protocol: "pop3",
166 | title: getBundleString("local", "pop3EnterPasswordPromptTitleWithUsername"),
167 | titleRegExp: true,
168 | dialog: getBundleString("local", "pop3EnterPasswordPrompt"),
169 | hostPlaceholder: "%2$S",
170 | loginPlaceholder: "%1$S"
171 | });
172 | addDialogType({
173 | protocol: "pop3",
174 | title: getBundleString("local", "pop3EnterPasswordPromptTitleWithUsername"),
175 | titleRegExp: true,
176 | dialog: getBundleString("local", "pop3PreviouslyEnteredPasswordIsInvalidPrompt"),
177 | hostPlaceholder: "%2$S",
178 | loginPlaceholder: "%1$S"
179 | });
180 | addDialogType({
181 | protocol: "nntp-1",
182 | title: getBundleString("news", "enterUserPassTitle"),
183 | dialog: getBundleString("news", "enterUserPassServer"),
184 | hostPlaceholder: "%S",
185 | loginPlaceholder: ""
186 | });
187 | addDialogType({
188 | protocol: "nntp-2",
189 | title: getBundleString("news", "enterUserPassTitle"),
190 | dialog: getBundleString("news", "enterUserPassGroup"),
191 | hostPlaceholder: "%2$S",
192 | loginPlaceholder: "%1$S"
193 | });
194 | ["ldaps", "ldap"].forEach(function(protocol){
195 | addDialogType({
196 | protocol,
197 | title: getBundleString("ldap", "authPromptTitle"),
198 | dialog: getBundleString("ldap", "authPromptText"),
199 | hostPlaceholder: "%1$S",
200 | loginPlaceholder: "",
201 | noLoginRequired: true,
202 | });
203 | });
204 |
205 | addDialogType({
206 | protocol: false,
207 | title: getBundleString("wcap", "loginDialog.label"),
208 | dialog: getBundleString("commonDialog", "EnterPasswordFor"),
209 | hostPlaceholder: "%2$S",
210 | loginPlaceholder: "%1$S"
211 | });
212 | addDialogType({
213 | protocol: false,
214 | title: getBundleString("wcap", "loginDialog.label"),
215 | dialog: getBundleString("commonDialog", "EnterUserPasswordFor2"),
216 | hostPlaceholder: "%1$S",
217 | loginPlaceholder: ""
218 | });
219 | addDialogType({
220 | protocol: false,
221 | title: getBundleString("commonDialog", "PromptUsernameAndPassword2"),
222 | dialog: getBundleString("commonDialog", "EnterLoginForRealm3"),
223 | hostPlaceholder: "%2$S",
224 | loginPlaceholder: ""
225 | });
226 | addDialogType({
227 | protocol: false,
228 | title: getBundleString("commonDialog", "PromptUsernameAndPassword3"),
229 | titleRegExp: true,
230 | dialog: getBundleString("commonDialog", "EnterLoginForRealm3"),
231 | hostPlaceholder: "%2$S",
232 | loginPlaceholder: ""
233 | });
234 | addDialogType({
235 | protocol: false,
236 | title: getBundleString("commonDialog", "PromptUsernameAndPassword2"),
237 | dialog: getBundleString("commonDialog", "EnterUserPasswordFor2"),
238 | hostPlaceholder: "%1$S",
239 | loginPlaceholder: ""
240 | });
241 | addDialogType({
242 | protocol: false,
243 | title: getBundleString("commonDialog", "PromptUsernameAndPassword3"),
244 | titleRegExp: true,
245 | dialog: getBundleString("commonDialog", "EnterUserPasswordFor2"),
246 | hostPlaceholder: "%1$S",
247 | loginPlaceholder: ""
248 | });
249 | addDialogType({
250 | protocol: false,
251 | title: getBundleString("commonDialog", "PromptPassword2"),
252 | dialog: getBundleString("commonDialog", "EnterPasswordFor"),
253 | hostPlaceholder: "%2$S",
254 | loginPlaceholder: "%1$S"
255 | });
256 | ["CertPassPromptDefault", "CertPasswordPromptDefault"].forEach(function(dialogStringName){
257 | // master password is called primary password in the UI
258 | const masterType = addDialogType({
259 | protocol: false,
260 | title: getBundleString("commonDialog", "PromptPassword3"),
261 | dialog: getBundleString("pipnss", dialogStringName),
262 | titleRegExp: true,
263 | hostPlaceholder: "",
264 | loginPlaceholder: "",
265 | noLoginRequired: true,
266 | });
267 | masterType.forcedHost = "masterPassword://Thunderbird";
268 | });
269 |
270 | const pgpI10n = new Localization(["messenger/openpgp/keyWizard.ftl"], true);
271 | if (pgpI10n){
272 | const title = pgpI10n.formatValueSync("openpgp-passphrase-prompt-title");
273 | const dialogs = [pgpI10n.formatValueSync("openpgp-passphrase-prompt", {key: "%1$S, %2$S, %3$S"})];
274 | const pgpI10n2 = new Localization(["messenger/openpgp/openpgp.ftl"], true);
275 | if (pgpI10n2){
276 | ["passphrase-prompt2-sub", "passphrase-prompt2"].forEach(function(id){
277 | dialogs.push(pgpI10n2.formatValueSync(id, {
278 | subkey: "%2$S",
279 | key: "%1$S",
280 | date: "%3$S",
281 | username_and_email: "%4$S",
282 | }));
283 | });
284 | }
285 |
286 | dialogs.filter(function(dialog){return dialog;}).forEach(function(dialog){
287 | addDialogType({
288 | protocol: "openpgp",
289 | title,
290 | dialog,
291 | hostPlaceholder: "%1$S",
292 | // loginPlaceholder: "%2$S",
293 | otherPlaceholders: ["%2$S", "%3$S", "%4$S"],
294 | noLoginRequired: true,
295 | });
296 | });
297 | }
298 |
299 | function getCredentialInfos(dialogTypes, title, text){
300 | const credentialInfos = dialogTypes.filter(function(dialogType){
301 | return dialogType.title === title || (dialogType.title.test && dialogType.title.test(title));
302 | }).map(function(dialogType){
303 | const ret = Object.create(dialogType);
304 | ret.match = text.match(dialogType.dialogRegExp);
305 | return ret;
306 | }).filter(function(dialogType){
307 | return dialogType.match;
308 | }).map(function(type){
309 | const host = type.hostGroup?
310 | (
311 | (type.protocol? type.protocol + "://": "") +
312 | type.match[type.hostGroup]
313 | ): type.forcedHost || false;
314 | let login = type.loginGroup?
315 | type.match[type.loginGroup]:
316 | type.noLoginRequired || false;
317 | return {host, login, mayAddProtocol: type.otherProtocolExists};
318 | }).filter(function(credentialInfo, index, credentialInfos){
319 | for (let i = 0; i < index; i += 1){
320 | if (
321 | credentialInfos[i].host === credentialInfo.host ||
322 | credentialInfos[i].login === credentialInfo.login
323 | ){
324 | return false;
325 | }
326 | }
327 | return true;
328 | });
329 | if (credentialInfos.length){
330 | return credentialInfos[0];
331 | }
332 | return false;
333 | }
334 |
335 | return {
336 | getCredentialInfoFromStrings: function getCredentialInfoFromStrings(title, text){
337 | return getCredentialInfos(dialogTypes.filter(function(dialogType){
338 | return !dialogType.otherProtocolExists;
339 | }), title, text);
340 | },
341 | getCredentialInfoFromStringsAndProtocol:
342 | function getCredentialInfoFromStringsAndProtocol(title, text, knownProtocol){
343 | return getCredentialInfos(dialogTypes.filter(function(dialogType){
344 | return dialogType.protocol === knownProtocol;
345 | }), title, text);
346 | },
347 | addDialogType
348 | };
349 | }();
--------------------------------------------------------------------------------
/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "errorMessageUnknown": {
3 | "message": "原因不明のエラー",
4 | "description": "Unknown error."
5 | },
6 | "errorMessageDatabaseNotOpened": {
7 | "message": "データベースが開かれていません",
8 | "description": "Database not opened."
9 | },
10 | "errorMessageDatabaseHash": {
11 | "message": "データベースのハッシュを受信できませんでした",
12 | "description": "Database hash not received."
13 | },
14 | "errorMessageClientPublicKey": {
15 | "message": "Client public key not received.",
16 | "description": "Client public key not received."
17 | },
18 | "errorMessageDecrypt": {
19 | "message": "メッセージを復号できません",
20 | "description": "Cannot decrypt message."
21 | },
22 | "errorMessageTimeout": {
23 | "message": "Cannot connect to KeePassXC. Check that browser integration is enabled in KeePassXC settings.",
24 | "description": "Cannot connect to KeePassXC. Check that browser integration is enabled in KeePassXC settings."
25 | },
26 | "errorMessageCanceled": {
27 | "message": "操作がキャンセルまたは拒否されました",
28 | "description": "Action canceled or denied."
29 | },
30 | "errorMessageEncrypt": {
31 | "message": "メッセージの暗号化に失敗しました。KeePassXCを実行していますか?",
32 | "description": "Message encryption failed. Is KeePassXC running?"
33 | },
34 | "errorMessageAssociate": {
35 | "message": "KeePassXCの関連付けに失敗しました。もう一度やり直してください",
36 | "description": "KeePassXC association failed, try again."
37 | },
38 | "errorMessageKeyExchange": {
39 | "message": "Key exchange was not successful.",
40 | "description": "Key exchange was not successful."
41 | },
42 | "errorMessageEncryptionKey": {
43 | "message": "暗号化キーが認識されません",
44 | "description": "Encryption key is not recognized."
45 | },
46 | "errorMessageSavedDatabases": {
47 | "message": "保存されたデータベースが見つかりません。",
48 | "description": "No saved databases found."
49 | },
50 | "errorMessageIncorrectAction": {
51 | "message": "不正な操作です",
52 | "description": "Incorrect action."
53 | },
54 | "errorMessageEmptyMessage": {
55 | "message": "空のメッセージを受信しました",
56 | "description": "Empty message received."
57 | },
58 | "errorMessageNoURL": {
59 | "message": "URLが指定されていません。",
60 | "description": "No URL provided."
61 | },
62 | "errorMessageNoLogins": {
63 | "message": "ログイン情報が見つかりません",
64 | "description": "No logins found."
65 | },
66 | "errorMessageNoGroupsFound": {
67 | "message": "No groups found.",
68 | "description": "No groups found."
69 | },
70 | "errorMessageCannotCreateNewGroup": {
71 | "message": "Cannot create new group.",
72 | "description": "Cannot create new group."
73 | },
74 | "errorMessageNoValidUuidProvided": {
75 | "message": "No valid UUID provided.",
76 | "description": "No valid UUID provided."
77 | },
78 | "errorMessageAccessToAllEntriesDenied": {
79 | "message": "Access to all entries denied.",
80 | "description": "Access to all entries denied."
81 | },
82 | "errorMessagePasskeysAttestationNotSupported": {
83 | "message": "Attestation not supported.",
84 | "description": "Attestation not supported."
85 | },
86 | "errorMessagePasskeysCredentialIsExcluded": {
87 | "message": "Credential is excluded.",
88 | "description": "Credential is excluded."
89 | },
90 | "errorMessagePasskeysRequestCanceled": {
91 | "message": "Passkeys request canceled.",
92 | "description": "Passkeys request canceled."
93 | },
94 | "errorMessagePasskeysInvalidUserVerification": {
95 | "message": "Invalid user verification.",
96 | "description": "Invalid user verification."
97 | },
98 | "errorMessagePasskeysEmptyPublicKey": {
99 | "message": "Empty public key.",
100 | "description": "Empty public key."
101 | },
102 | "errorMessagePasskeysInvalidUrlProvided": {
103 | "message": "Invalid URL provided.",
104 | "description": "Invalid URL provided."
105 | },
106 | "errorMessagePasskeysOriginNotAllowed": {
107 | "message": "Origin is empty or not allowed.",
108 | "description": "Origin is empty or not allowed."
109 | },
110 | "errorMessagePasskeysDomainNotValid": {
111 | "message": "Effective domain is not a valid domain.",
112 | "description": "Effective domain is not a valid domain."
113 | },
114 | "errorMessagePasskeysDomainRpIdMismatch": {
115 | "message": "Origin and RP ID do not match.",
116 | "description": "Origin and RP ID do not match."
117 | },
118 | "errorMessagePasskeysNoSupportedAlgorithms": {
119 | "message": "No supported algorithms were provided.",
120 | "description": "No supported algorithms were provided."
121 | },
122 | "errorMessagePasskeysWaitforLifeTimer": {
123 | "message": "Wait for timer to expire.",
124 | "description": "Wait for timer to expire."
125 | },
126 | "errorMessagePasskeysUnknownError": {
127 | "message": "Unknown passkeys error.",
128 | "description": "Unknown passkeys error."
129 | },
130 | "errorMessagePasskeysInvalidChallenge": {
131 | "message": "Challenge is shorter than required minimum length.",
132 | "description": "Challenge is shorter than required minimum length."
133 | },
134 | "errorMessagePasskeysInvalidUserId": {
135 | "message": "user.id does not match the required length.",
136 | "description": "user.id does not match the required length."
137 | },
138 | "loadingPasswords": {
139 | "message": "パスワードを読み込んでいます...",
140 | "description": "Loading passwords..."
141 | },
142 | "noPasswordsFound": {
143 | "message": "パスワードが見つかりませんでした",
144 | "description": "No passwords found."
145 | },
146 | "retry": {
147 | "message": "再試行",
148 | "description": "Retry"
149 | },
150 | "pickedEntry": {
151 | "message": "Picked entry \"{name|login|\"---\"}\"",
152 | "description": "Picked entry \"{name|login|\"---\"}\""
153 | },
154 | "entryLabel": {
155 | "message": "{name|\"ログイン\"}: {login}",
156 | "description": "{name|\"Login\"}: {login}"
157 | },
158 | "entryTooltip": {
159 | "message": "パスワード \"{password}\"",
160 | "description": "Password \"{password}\""
161 | },
162 | "credentialInfo": {
163 | "message": "URL: {host}\nログイン: {login|\"--\"}",
164 | "description": "URL: {host}\nLogin: {login}"
165 | },
166 | "options.title": {
167 | "message": "KeePassXC-Mailの設定",
168 | "description": "KeePassXC-Mail Settings"
169 | },
170 | "options.databases": {
171 | "message": "データベース接続",
172 | "description": "Database connections"
173 | },
174 | "options.clearSelectedEntries": {
175 | "message": "Clear storage of selected entries",
176 | "description": "Clear storage of selected entries"
177 | },
178 | "options.clearingSelectedEntries": {
179 | "message": "Clearing storage of selected entries...",
180 | "description": "Clearing storage of selected entries..."
181 | },
182 | "options.reconnect": {
183 | "message": "Native Messagingに再接続",
184 | "description": "Reconnect to Native Messaging"
185 | },
186 | "options.reconnecting": {
187 | "message": "Reconnecting to Native Messaging...",
188 | "description": "Reconnecting to Native Messaging..."
189 | },
190 | "options.associate": {
191 | "message": "KeePassXCに接続する",
192 | "description": "Connect to KeePassXC"
193 | },
194 | "options.associating": {
195 | "message": "Connecting to KeePassXC...",
196 | "description": "Connecting to KeePassXC..."
197 | },
198 | "options.autoSubmit.label": {
199 | "message": "自動送信",
200 | "description": "Auto submit"
201 | },
202 | "options.saveNewCredentials.label": {
203 | "message": "新しい認証情報を保存",
204 | "description": "Save new credentials"
205 | },
206 | "options.autoSaveNewCredentials.label": {
207 | "message": "Save new credentials without prompt",
208 | "description": "Save new credentials without prompt"
209 | },
210 | "options.connections.id": {
211 | "message": "ID",
212 | "description": "ID"
213 | },
214 | "options.connections.dbHash": {
215 | "message": "DB hash",
216 | "description": "DB hash"
217 | },
218 | "options.connections.key": {
219 | "message": "Key",
220 | "description": "Key"
221 | },
222 | "options.connections.lastUsed": {
223 | "message": "Last used",
224 | "description": "Last used"
225 | },
226 | "options.connections.created": {
227 | "message": "Created",
228 | "description": "Created"
229 | },
230 | "options.externalPrivileges": {
231 | "message": "External privileges",
232 | "description": "External privileges"
233 | },
234 | "options.externalPrivileges.extensionName": {
235 | "message": "extension",
236 | "description": "extension"
237 | },
238 | "options.externalPrivileges.request": {
239 | "message": "request",
240 | "description": "request"
241 | },
242 | "options.externalPrivileges.store": {
243 | "message": "store",
244 | "description": "store"
245 | },
246 | "modal.savingPassword.title": {
247 | "message": "Save password for {host} to KeePass database?",
248 | "description": "Save password for {host} to KeePass database?"
249 | },
250 | "modal.savingPassword.questionWithLogin": {
251 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?",
252 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?"
253 | },
254 | "modal.savingPassword.questionWithoutLogin": {
255 | "message": "Do you want to save the entered password for {host} to the KeePass database?",
256 | "description": "Do you want to save the entered password for {host} to the KeePass database?"
257 | },
258 | "modal.savingPassword.newEntry": {
259 | "message": " - create new entry - ",
260 | "description": " - create new entry - "
261 | },
262 | "modal.savingPassword.yes": {
263 | "message": "Yes",
264 | "description": "Yes"
265 | },
266 | "modal.savingPassword.no": {
267 | "message": "No",
268 | "description": "No"
269 | },
270 | "modal.choice.title": {
271 | "message": "Select correct entry for {host}",
272 | "description": "Select correct entry for {host}"
273 | },
274 | "modal.choice.textWithLogin": {
275 | "message": "Please select the correct entry for {login} on {host}.",
276 | "description": "Please select the correct entry for {login} on {host}."
277 | },
278 | "modal.choice.textWithoutLogin": {
279 | "message": "Please select the correct entry for {host}.",
280 | "description": "Please select the correct entry for {host}."
281 | },
282 | "modal.choice.doNotAskAgain": {
283 | "message": "Do not ask again.",
284 | "description": "Do not ask again."
285 | },
286 | "modal.choice.ok": {
287 | "message": "OK",
288 | "description": "OK"
289 | },
290 | "modal.choice.cancel": {
291 | "message": "Cancel",
292 | "description": "Cancel"
293 | },
294 | "modal.confirm.yes": {
295 | "message": "Yes",
296 | "description": "Yes"
297 | },
298 | "modal.confirm.no": {
299 | "message": "No",
300 | "description": "No"
301 | },
302 | "modal.message.ok": {
303 | "message": "OK",
304 | "description": "OK"
305 | },
306 | "createPasswordGroup.timeout.title": {
307 | "message": "Creating password group timeout",
308 | "description": "Creating password group timeout"
309 | },
310 | "createPasswordGroup.timeout.message": {
311 | "message": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nCreating the password group timed out. Please check your KeePassXC settings to allow group creation.",
312 | "description": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nCreating the password group timed out. Please check your KeePassXC settings to allow group creation."
313 | },
314 | "savePassword.timeout.title": {
315 | "message": "Saving password timeout",
316 | "description": "Saving password timeout"
317 | },
318 | "savePassword.timeout.message": {
319 | "message": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nPlease check your KeePassXC settings to allow password saving and updating.",
320 | "description": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nPlease check your KeePassXC settings to allow password saving and updating."
321 | },
322 | "unlockDatabase.title": {
323 | "message": "Locked database",
324 | "description": "Locked database"
325 | },
326 | "unlockDatabase.question": {
327 | "message": "Your database is locked.\n\nDo you want to unlock it?",
328 | "description": "Your database is locked.\n\nDo you want to unlock it?"
329 | },
330 | "unlockDatabase.failed": {
331 | "message": "The database was not unlocked in time.\n\nPassword retrieval and storage is not possible with a locked database.",
332 | "description": "The database was not unlocked in time.\n\nPassword retrieval and storage is not possible with a locked database."
333 | },
334 | "passwordsStoredInThunderbird.title": {
335 | "message": "Password stored in Thunderbird",
336 | "description": "Password stored in Thunderbird"
337 | },
338 | "passwordsStoredInThunderbird.message": {
339 | "message": "There are passwords stored in the Thunderbird password vault.\n\nPlease review if this is intended.",
340 | "description": "There are passwords stored in the Thunderbird password vault.\n\nPlease review if this is intended."
341 | },
342 | "passwordsStoredInThunderbird.newStored": {
343 | "message": "New passwords were added to the Thunderbird password vault.\n\nPlease review if this is intended.",
344 | "description": "New passwords were added to the Thunderbird password vault.\n\nPlease review if this is intended."
345 | },
346 | "privilegeRequest.title": {
347 | "message": "Privilege request",
348 | "description": "Privilege request"
349 | },
350 | "privilegeRequest.message": {
351 | "message": "{typeMessage}\n\nDo you want to allow this?\n\nThis decision will be stored for further requests and can be changed in the extension preferences.",
352 | "description": "{typeMessage}\n\nDo you want to allow this?\n\nThis decision will be stored for further requests and can be changed in the extension preferences."
353 | },
354 | "privilegeRequest.message.request": {
355 | "message": "The extension {extensionName} wants to request passwords.",
356 | "description": "The extension {extensionName} wants to request passwords."
357 | },
358 | "privilegeRequest.message.store": {
359 | "message": "The extension {extensionName} wants to store passwords.",
360 | "description": "The extension {extensionName} wants to store passwords."
361 | }
362 | }
--------------------------------------------------------------------------------
/_locales/ar/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "errorMessageUnknown": {
3 | "message": "Unknown error.",
4 | "description": "Unknown error."
5 | },
6 | "errorMessageDatabaseNotOpened": {
7 | "message": "Database not opened.",
8 | "description": "Database not opened."
9 | },
10 | "errorMessageDatabaseHash": {
11 | "message": "Database hash not received.",
12 | "description": "Database hash not received."
13 | },
14 | "errorMessageClientPublicKey": {
15 | "message": "Client public key not received.",
16 | "description": "Client public key not received."
17 | },
18 | "errorMessageDecrypt": {
19 | "message": "Cannot decrypt message.",
20 | "description": "Cannot decrypt message."
21 | },
22 | "errorMessageTimeout": {
23 | "message": "Cannot connect to KeePassXC. Check that browser integration is enabled in KeePassXC settings.",
24 | "description": "Cannot connect to KeePassXC. Check that browser integration is enabled in KeePassXC settings."
25 | },
26 | "errorMessageCanceled": {
27 | "message": "Action canceled or denied.",
28 | "description": "Action canceled or denied."
29 | },
30 | "errorMessageEncrypt": {
31 | "message": "Message encryption failed. Is KeePassXC running?",
32 | "description": "Message encryption failed. Is KeePassXC running?"
33 | },
34 | "errorMessageAssociate": {
35 | "message": "KeePassXC association failed, try again.",
36 | "description": "KeePassXC association failed, try again."
37 | },
38 | "errorMessageKeyExchange": {
39 | "message": "Key exchange was not successful.",
40 | "description": "Key exchange was not successful."
41 | },
42 | "errorMessageEncryptionKey": {
43 | "message": "Encryption key is not recognized.",
44 | "description": "Encryption key is not recognized."
45 | },
46 | "errorMessageSavedDatabases": {
47 | "message": "No saved databases found.",
48 | "description": "No saved databases found."
49 | },
50 | "errorMessageIncorrectAction": {
51 | "message": "Incorrect action.",
52 | "description": "Incorrect action."
53 | },
54 | "errorMessageEmptyMessage": {
55 | "message": "Empty message received.",
56 | "description": "Empty message received."
57 | },
58 | "errorMessageNoURL": {
59 | "message": "No URL provided.",
60 | "description": "No URL provided."
61 | },
62 | "errorMessageNoLogins": {
63 | "message": "No logins found.",
64 | "description": "No logins found."
65 | },
66 | "errorMessageNoGroupsFound": {
67 | "message": "No groups found.",
68 | "description": "No groups found."
69 | },
70 | "errorMessageCannotCreateNewGroup": {
71 | "message": "Cannot create new group.",
72 | "description": "Cannot create new group."
73 | },
74 | "errorMessageNoValidUuidProvided": {
75 | "message": "No valid UUID provided.",
76 | "description": "No valid UUID provided."
77 | },
78 | "errorMessageAccessToAllEntriesDenied": {
79 | "message": "Access to all entries denied.",
80 | "description": "Access to all entries denied."
81 | },
82 | "errorMessagePasskeysAttestationNotSupported": {
83 | "message": "Attestation not supported.",
84 | "description": "Attestation not supported."
85 | },
86 | "errorMessagePasskeysCredentialIsExcluded": {
87 | "message": "Credential is excluded.",
88 | "description": "Credential is excluded."
89 | },
90 | "errorMessagePasskeysRequestCanceled": {
91 | "message": "Passkeys request canceled.",
92 | "description": "Passkeys request canceled."
93 | },
94 | "errorMessagePasskeysInvalidUserVerification": {
95 | "message": "Invalid user verification.",
96 | "description": "Invalid user verification."
97 | },
98 | "errorMessagePasskeysEmptyPublicKey": {
99 | "message": "Empty public key.",
100 | "description": "Empty public key."
101 | },
102 | "errorMessagePasskeysInvalidUrlProvided": {
103 | "message": "Invalid URL provided.",
104 | "description": "Invalid URL provided."
105 | },
106 | "errorMessagePasskeysOriginNotAllowed": {
107 | "message": "Origin is empty or not allowed.",
108 | "description": "Origin is empty or not allowed."
109 | },
110 | "errorMessagePasskeysDomainNotValid": {
111 | "message": "Effective domain is not a valid domain.",
112 | "description": "Effective domain is not a valid domain."
113 | },
114 | "errorMessagePasskeysDomainRpIdMismatch": {
115 | "message": "Origin and RP ID do not match.",
116 | "description": "Origin and RP ID do not match."
117 | },
118 | "errorMessagePasskeysNoSupportedAlgorithms": {
119 | "message": "No supported algorithms were provided.",
120 | "description": "No supported algorithms were provided."
121 | },
122 | "errorMessagePasskeysWaitforLifeTimer": {
123 | "message": "Wait for timer to expire.",
124 | "description": "Wait for timer to expire."
125 | },
126 | "errorMessagePasskeysUnknownError": {
127 | "message": "Unknown passkeys error.",
128 | "description": "Unknown passkeys error."
129 | },
130 | "errorMessagePasskeysInvalidChallenge": {
131 | "message": "Challenge is shorter than required minimum length.",
132 | "description": "Challenge is shorter than required minimum length."
133 | },
134 | "errorMessagePasskeysInvalidUserId": {
135 | "message": "user.id does not match the required length.",
136 | "description": "user.id does not match the required length."
137 | },
138 | "loadingPasswords": {
139 | "message": "Loading passwords...",
140 | "description": "Loading passwords..."
141 | },
142 | "noPasswordsFound": {
143 | "message": "No passwords found.",
144 | "description": "No passwords found."
145 | },
146 | "retry": {
147 | "message": "Retry",
148 | "description": "Retry"
149 | },
150 | "pickedEntry": {
151 | "message": "Picked entry \"{name|login|\"---\"}\"",
152 | "description": "Picked entry \"{name|login|\"---\"}\""
153 | },
154 | "entryLabel": {
155 | "message": "{name|\"Login\"}: {login}",
156 | "description": "{name|\"Login\"}: {login}"
157 | },
158 | "entryTooltip": {
159 | "message": "Password \"{password}\"",
160 | "description": "Password \"{password}\""
161 | },
162 | "credentialInfo": {
163 | "message": "URL: {host}\nLogin: {login|\"---\"}",
164 | "description": "URL: {host}\nLogin: {login}"
165 | },
166 | "options.title": {
167 | "message": "KeePassXC-Mail Settings",
168 | "description": "KeePassXC-Mail Settings"
169 | },
170 | "options.databases": {
171 | "message": "Database connections",
172 | "description": "Database connections"
173 | },
174 | "options.clearSelectedEntries": {
175 | "message": "Clear storage of selected entries",
176 | "description": "Clear storage of selected entries"
177 | },
178 | "options.clearingSelectedEntries": {
179 | "message": "Clearing storage of selected entries...",
180 | "description": "Clearing storage of selected entries..."
181 | },
182 | "options.reconnect": {
183 | "message": "Reconnect to Native Messaging",
184 | "description": "Reconnect to Native Messaging"
185 | },
186 | "options.reconnecting": {
187 | "message": "Reconnecting to Native Messaging...",
188 | "description": "Reconnecting to Native Messaging..."
189 | },
190 | "options.associate": {
191 | "message": "Connect to KeePassXC",
192 | "description": "Connect to KeePassXC"
193 | },
194 | "options.associating": {
195 | "message": "Connecting to KeePassXC...",
196 | "description": "Connecting to KeePassXC..."
197 | },
198 | "options.autoSubmit.label": {
199 | "message": "Auto submit",
200 | "description": "Auto submit"
201 | },
202 | "options.saveNewCredentials.label": {
203 | "message": "Save new credentials",
204 | "description": "Save new credentials"
205 | },
206 | "options.autoSaveNewCredentials.label": {
207 | "message": "Save new credentials without prompt",
208 | "description": "Save new credentials without prompt"
209 | },
210 | "options.connections.id": {
211 | "message": "ID",
212 | "description": "ID"
213 | },
214 | "options.connections.dbHash": {
215 | "message": "DB hash",
216 | "description": "DB hash"
217 | },
218 | "options.connections.key": {
219 | "message": "Key",
220 | "description": "Key"
221 | },
222 | "options.connections.lastUsed": {
223 | "message": "Last used",
224 | "description": "Last used"
225 | },
226 | "options.connections.created": {
227 | "message": "Created",
228 | "description": "Created"
229 | },
230 | "options.externalPrivileges": {
231 | "message": "External privileges",
232 | "description": "External privileges"
233 | },
234 | "options.externalPrivileges.extensionName": {
235 | "message": "extension",
236 | "description": "extension"
237 | },
238 | "options.externalPrivileges.request": {
239 | "message": "request",
240 | "description": "request"
241 | },
242 | "options.externalPrivileges.store": {
243 | "message": "store",
244 | "description": "store"
245 | },
246 | "modal.savingPassword.title": {
247 | "message": "Save password for {host} to KeePass database?",
248 | "description": "Save password for {host} to KeePass database?"
249 | },
250 | "modal.savingPassword.questionWithLogin": {
251 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?",
252 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?"
253 | },
254 | "modal.savingPassword.questionWithoutLogin": {
255 | "message": "Do you want to save the entered password for {host} to the KeePass database?",
256 | "description": "Do you want to save the entered password for {host} to the KeePass database?"
257 | },
258 | "modal.savingPassword.newEntry": {
259 | "message": " - create new entry - ",
260 | "description": " - create new entry - "
261 | },
262 | "modal.savingPassword.yes": {
263 | "message": "Yes",
264 | "description": "Yes"
265 | },
266 | "modal.savingPassword.no": {
267 | "message": "No",
268 | "description": "No"
269 | },
270 | "modal.choice.title": {
271 | "message": "Select correct entry for {host}",
272 | "description": "Select correct entry for {host}"
273 | },
274 | "modal.choice.textWithLogin": {
275 | "message": "Please select the correct entry for {login} on {host}.",
276 | "description": "Please select the correct entry for {login} on {host}."
277 | },
278 | "modal.choice.textWithoutLogin": {
279 | "message": "Please select the correct entry for {host}.",
280 | "description": "Please select the correct entry for {host}."
281 | },
282 | "modal.choice.doNotAskAgain": {
283 | "message": "Do not ask again.",
284 | "description": "Do not ask again."
285 | },
286 | "modal.choice.ok": {
287 | "message": "OK",
288 | "description": "OK"
289 | },
290 | "modal.choice.cancel": {
291 | "message": "Cancel",
292 | "description": "Cancel"
293 | },
294 | "modal.confirm.yes": {
295 | "message": "Yes",
296 | "description": "Yes"
297 | },
298 | "modal.confirm.no": {
299 | "message": "No",
300 | "description": "No"
301 | },
302 | "modal.message.ok": {
303 | "message": "OK",
304 | "description": "OK"
305 | },
306 | "createPasswordGroup.timeout.title": {
307 | "message": "Creating password group timeout",
308 | "description": "Creating password group timeout"
309 | },
310 | "createPasswordGroup.timeout.message": {
311 | "message": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nCreating the password group timed out. Please check your KeePassXC settings to allow group creation.",
312 | "description": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nCreating the password group timed out. Please check your KeePassXC settings to allow group creation."
313 | },
314 | "savePassword.timeout.title": {
315 | "message": "Saving password timeout",
316 | "description": "Saving password timeout"
317 | },
318 | "savePassword.timeout.message": {
319 | "message": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nPlease check your KeePassXC settings to allow password saving and updating.",
320 | "description": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nPlease check your KeePassXC settings to allow password saving and updating."
321 | },
322 | "unlockDatabase.title": {
323 | "message": "Locked database",
324 | "description": "Locked database"
325 | },
326 | "unlockDatabase.question": {
327 | "message": "Your database is locked.\n\nDo you want to unlock it?",
328 | "description": "Your database is locked.\n\nDo you want to unlock it?"
329 | },
330 | "unlockDatabase.failed": {
331 | "message": "The database was not unlocked in time.\n\nPassword retrieval and storage is not possible with a locked database.",
332 | "description": "The database was not unlocked in time.\n\nPassword retrieval and storage is not possible with a locked database."
333 | },
334 | "passwordsStoredInThunderbird.title": {
335 | "message": "Password stored in Thunderbird",
336 | "description": "Password stored in Thunderbird"
337 | },
338 | "passwordsStoredInThunderbird.message": {
339 | "message": "There are passwords stored in the Thunderbird password vault.\n\nPlease review if this is intended.",
340 | "description": "There are passwords stored in the Thunderbird password vault.\n\nPlease review if this is intended."
341 | },
342 | "passwordsStoredInThunderbird.newStored": {
343 | "message": "New passwords were added to the Thunderbird password vault.\n\nPlease review if this is intended.",
344 | "description": "New passwords were added to the Thunderbird password vault.\n\nPlease review if this is intended."
345 | },
346 | "privilegeRequest.title": {
347 | "message": "Privilege request",
348 | "description": "Privilege request"
349 | },
350 | "privilegeRequest.message": {
351 | "message": "{typeMessage}\n\nDo you want to allow this?\n\nThis decision will be stored for further requests and can be changed in the extension preferences.",
352 | "description": "{typeMessage}\n\nDo you want to allow this?\n\nThis decision will be stored for further requests and can be changed in the extension preferences."
353 | },
354 | "privilegeRequest.message.request": {
355 | "message": "The extension {extensionName} wants to request passwords.",
356 | "description": "The extension {extensionName} wants to request passwords."
357 | },
358 | "privilegeRequest.message.store": {
359 | "message": "The extension {extensionName} wants to store passwords.",
360 | "description": "The extension {extensionName} wants to store passwords."
361 | }
362 | }
--------------------------------------------------------------------------------
/_locales/bg/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "errorMessageUnknown": {
3 | "message": "Unknown error.",
4 | "description": "Unknown error."
5 | },
6 | "errorMessageDatabaseNotOpened": {
7 | "message": "Database not opened.",
8 | "description": "Database not opened."
9 | },
10 | "errorMessageDatabaseHash": {
11 | "message": "Database hash not received.",
12 | "description": "Database hash not received."
13 | },
14 | "errorMessageClientPublicKey": {
15 | "message": "Client public key not received.",
16 | "description": "Client public key not received."
17 | },
18 | "errorMessageDecrypt": {
19 | "message": "Cannot decrypt message.",
20 | "description": "Cannot decrypt message."
21 | },
22 | "errorMessageTimeout": {
23 | "message": "Cannot connect to KeePassXC. Check that browser integration is enabled in KeePassXC settings.",
24 | "description": "Cannot connect to KeePassXC. Check that browser integration is enabled in KeePassXC settings."
25 | },
26 | "errorMessageCanceled": {
27 | "message": "Action canceled or denied.",
28 | "description": "Action canceled or denied."
29 | },
30 | "errorMessageEncrypt": {
31 | "message": "Message encryption failed. Is KeePassXC running?",
32 | "description": "Message encryption failed. Is KeePassXC running?"
33 | },
34 | "errorMessageAssociate": {
35 | "message": "KeePassXC association failed, try again.",
36 | "description": "KeePassXC association failed, try again."
37 | },
38 | "errorMessageKeyExchange": {
39 | "message": "Key exchange was not successful.",
40 | "description": "Key exchange was not successful."
41 | },
42 | "errorMessageEncryptionKey": {
43 | "message": "Encryption key is not recognized.",
44 | "description": "Encryption key is not recognized."
45 | },
46 | "errorMessageSavedDatabases": {
47 | "message": "No saved databases found.",
48 | "description": "No saved databases found."
49 | },
50 | "errorMessageIncorrectAction": {
51 | "message": "Incorrect action.",
52 | "description": "Incorrect action."
53 | },
54 | "errorMessageEmptyMessage": {
55 | "message": "Empty message received.",
56 | "description": "Empty message received."
57 | },
58 | "errorMessageNoURL": {
59 | "message": "No URL provided.",
60 | "description": "No URL provided."
61 | },
62 | "errorMessageNoLogins": {
63 | "message": "No logins found.",
64 | "description": "No logins found."
65 | },
66 | "errorMessageNoGroupsFound": {
67 | "message": "No groups found.",
68 | "description": "No groups found."
69 | },
70 | "errorMessageCannotCreateNewGroup": {
71 | "message": "Cannot create new group.",
72 | "description": "Cannot create new group."
73 | },
74 | "errorMessageNoValidUuidProvided": {
75 | "message": "No valid UUID provided.",
76 | "description": "No valid UUID provided."
77 | },
78 | "errorMessageAccessToAllEntriesDenied": {
79 | "message": "Access to all entries denied.",
80 | "description": "Access to all entries denied."
81 | },
82 | "errorMessagePasskeysAttestationNotSupported": {
83 | "message": "Attestation not supported.",
84 | "description": "Attestation not supported."
85 | },
86 | "errorMessagePasskeysCredentialIsExcluded": {
87 | "message": "Credential is excluded.",
88 | "description": "Credential is excluded."
89 | },
90 | "errorMessagePasskeysRequestCanceled": {
91 | "message": "Passkeys request canceled.",
92 | "description": "Passkeys request canceled."
93 | },
94 | "errorMessagePasskeysInvalidUserVerification": {
95 | "message": "Invalid user verification.",
96 | "description": "Invalid user verification."
97 | },
98 | "errorMessagePasskeysEmptyPublicKey": {
99 | "message": "Empty public key.",
100 | "description": "Empty public key."
101 | },
102 | "errorMessagePasskeysInvalidUrlProvided": {
103 | "message": "Invalid URL provided.",
104 | "description": "Invalid URL provided."
105 | },
106 | "errorMessagePasskeysOriginNotAllowed": {
107 | "message": "Origin is empty or not allowed.",
108 | "description": "Origin is empty or not allowed."
109 | },
110 | "errorMessagePasskeysDomainNotValid": {
111 | "message": "Effective domain is not a valid domain.",
112 | "description": "Effective domain is not a valid domain."
113 | },
114 | "errorMessagePasskeysDomainRpIdMismatch": {
115 | "message": "Origin and RP ID do not match.",
116 | "description": "Origin and RP ID do not match."
117 | },
118 | "errorMessagePasskeysNoSupportedAlgorithms": {
119 | "message": "No supported algorithms were provided.",
120 | "description": "No supported algorithms were provided."
121 | },
122 | "errorMessagePasskeysWaitforLifeTimer": {
123 | "message": "Wait for timer to expire.",
124 | "description": "Wait for timer to expire."
125 | },
126 | "errorMessagePasskeysUnknownError": {
127 | "message": "Unknown passkeys error.",
128 | "description": "Unknown passkeys error."
129 | },
130 | "errorMessagePasskeysInvalidChallenge": {
131 | "message": "Challenge is shorter than required minimum length.",
132 | "description": "Challenge is shorter than required minimum length."
133 | },
134 | "errorMessagePasskeysInvalidUserId": {
135 | "message": "user.id does not match the required length.",
136 | "description": "user.id does not match the required length."
137 | },
138 | "loadingPasswords": {
139 | "message": "Loading passwords...",
140 | "description": "Loading passwords..."
141 | },
142 | "noPasswordsFound": {
143 | "message": "No passwords found.",
144 | "description": "No passwords found."
145 | },
146 | "retry": {
147 | "message": "Retry",
148 | "description": "Retry"
149 | },
150 | "pickedEntry": {
151 | "message": "Picked entry \"{name|login|\"---\"}\"",
152 | "description": "Picked entry \"{name|login|\"---\"}\""
153 | },
154 | "entryLabel": {
155 | "message": "{name|\"Login\"}: {login}",
156 | "description": "{name|\"Login\"}: {login}"
157 | },
158 | "entryTooltip": {
159 | "message": "Password \"{password}\"",
160 | "description": "Password \"{password}\""
161 | },
162 | "credentialInfo": {
163 | "message": "URL: {host}\nLogin: {login|\"---\"}",
164 | "description": "URL: {host}\nLogin: {login}"
165 | },
166 | "options.title": {
167 | "message": "KeePassXC-Mail Settings",
168 | "description": "KeePassXC-Mail Settings"
169 | },
170 | "options.databases": {
171 | "message": "Database connections",
172 | "description": "Database connections"
173 | },
174 | "options.clearSelectedEntries": {
175 | "message": "Clear storage of selected entries",
176 | "description": "Clear storage of selected entries"
177 | },
178 | "options.clearingSelectedEntries": {
179 | "message": "Clearing storage of selected entries...",
180 | "description": "Clearing storage of selected entries..."
181 | },
182 | "options.reconnect": {
183 | "message": "Reconnect to Native Messaging",
184 | "description": "Reconnect to Native Messaging"
185 | },
186 | "options.reconnecting": {
187 | "message": "Reconnecting to Native Messaging...",
188 | "description": "Reconnecting to Native Messaging..."
189 | },
190 | "options.associate": {
191 | "message": "Connect to KeePassXC",
192 | "description": "Connect to KeePassXC"
193 | },
194 | "options.associating": {
195 | "message": "Connecting to KeePassXC...",
196 | "description": "Connecting to KeePassXC..."
197 | },
198 | "options.autoSubmit.label": {
199 | "message": "Auto submit",
200 | "description": "Auto submit"
201 | },
202 | "options.saveNewCredentials.label": {
203 | "message": "Save new credentials",
204 | "description": "Save new credentials"
205 | },
206 | "options.autoSaveNewCredentials.label": {
207 | "message": "Save new credentials without prompt",
208 | "description": "Save new credentials without prompt"
209 | },
210 | "options.connections.id": {
211 | "message": "ID",
212 | "description": "ID"
213 | },
214 | "options.connections.dbHash": {
215 | "message": "DB hash",
216 | "description": "DB hash"
217 | },
218 | "options.connections.key": {
219 | "message": "Key",
220 | "description": "Key"
221 | },
222 | "options.connections.lastUsed": {
223 | "message": "Last used",
224 | "description": "Last used"
225 | },
226 | "options.connections.created": {
227 | "message": "Created",
228 | "description": "Created"
229 | },
230 | "options.externalPrivileges": {
231 | "message": "External privileges",
232 | "description": "External privileges"
233 | },
234 | "options.externalPrivileges.extensionName": {
235 | "message": "extension",
236 | "description": "extension"
237 | },
238 | "options.externalPrivileges.request": {
239 | "message": "request",
240 | "description": "request"
241 | },
242 | "options.externalPrivileges.store": {
243 | "message": "store",
244 | "description": "store"
245 | },
246 | "modal.savingPassword.title": {
247 | "message": "Save password for {host} to KeePass database?",
248 | "description": "Save password for {host} to KeePass database?"
249 | },
250 | "modal.savingPassword.questionWithLogin": {
251 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?",
252 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?"
253 | },
254 | "modal.savingPassword.questionWithoutLogin": {
255 | "message": "Do you want to save the entered password for {host} to the KeePass database?",
256 | "description": "Do you want to save the entered password for {host} to the KeePass database?"
257 | },
258 | "modal.savingPassword.newEntry": {
259 | "message": " - create new entry - ",
260 | "description": " - create new entry - "
261 | },
262 | "modal.savingPassword.yes": {
263 | "message": "Yes",
264 | "description": "Yes"
265 | },
266 | "modal.savingPassword.no": {
267 | "message": "No",
268 | "description": "No"
269 | },
270 | "modal.choice.title": {
271 | "message": "Select correct entry for {host}",
272 | "description": "Select correct entry for {host}"
273 | },
274 | "modal.choice.textWithLogin": {
275 | "message": "Please select the correct entry for {login} on {host}.",
276 | "description": "Please select the correct entry for {login} on {host}."
277 | },
278 | "modal.choice.textWithoutLogin": {
279 | "message": "Please select the correct entry for {host}.",
280 | "description": "Please select the correct entry for {host}."
281 | },
282 | "modal.choice.doNotAskAgain": {
283 | "message": "Do not ask again.",
284 | "description": "Do not ask again."
285 | },
286 | "modal.choice.ok": {
287 | "message": "OK",
288 | "description": "OK"
289 | },
290 | "modal.choice.cancel": {
291 | "message": "Cancel",
292 | "description": "Cancel"
293 | },
294 | "modal.confirm.yes": {
295 | "message": "Yes",
296 | "description": "Yes"
297 | },
298 | "modal.confirm.no": {
299 | "message": "No",
300 | "description": "No"
301 | },
302 | "modal.message.ok": {
303 | "message": "OK",
304 | "description": "OK"
305 | },
306 | "createPasswordGroup.timeout.title": {
307 | "message": "Creating password group timeout",
308 | "description": "Creating password group timeout"
309 | },
310 | "createPasswordGroup.timeout.message": {
311 | "message": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nCreating the password group timed out. Please check your KeePassXC settings to allow group creation.",
312 | "description": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nCreating the password group timed out. Please check your KeePassXC settings to allow group creation."
313 | },
314 | "savePassword.timeout.title": {
315 | "message": "Saving password timeout",
316 | "description": "Saving password timeout"
317 | },
318 | "savePassword.timeout.message": {
319 | "message": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nPlease check your KeePassXC settings to allow password saving and updating.",
320 | "description": "Saving the password for {account} was not successful. The password was saved to the built-in Thunderbird password manager.\n\nPlease check your KeePassXC settings to allow password saving and updating."
321 | },
322 | "unlockDatabase.title": {
323 | "message": "Locked database",
324 | "description": "Locked database"
325 | },
326 | "unlockDatabase.question": {
327 | "message": "Your database is locked.\n\nDo you want to unlock it?",
328 | "description": "Your database is locked.\n\nDo you want to unlock it?"
329 | },
330 | "unlockDatabase.failed": {
331 | "message": "The database was not unlocked in time.\n\nPassword retrieval and storage is not possible with a locked database.",
332 | "description": "The database was not unlocked in time.\n\nPassword retrieval and storage is not possible with a locked database."
333 | },
334 | "passwordsStoredInThunderbird.title": {
335 | "message": "Password stored in Thunderbird",
336 | "description": "Password stored in Thunderbird"
337 | },
338 | "passwordsStoredInThunderbird.message": {
339 | "message": "There are passwords stored in the Thunderbird password vault.\n\nPlease review if this is intended.",
340 | "description": "There are passwords stored in the Thunderbird password vault.\n\nPlease review if this is intended."
341 | },
342 | "passwordsStoredInThunderbird.newStored": {
343 | "message": "New passwords were added to the Thunderbird password vault.\n\nPlease review if this is intended.",
344 | "description": "New passwords were added to the Thunderbird password vault.\n\nPlease review if this is intended."
345 | },
346 | "privilegeRequest.title": {
347 | "message": "Privilege request",
348 | "description": "Privilege request"
349 | },
350 | "privilegeRequest.message": {
351 | "message": "{typeMessage}\n\nDo you want to allow this?\n\nThis decision will be stored for further requests and can be changed in the extension preferences.",
352 | "description": "{typeMessage}\n\nDo you want to allow this?\n\nThis decision will be stored for further requests and can be changed in the extension preferences."
353 | },
354 | "privilegeRequest.message.request": {
355 | "message": "The extension {extensionName} wants to request passwords.",
356 | "description": "The extension {extensionName} wants to request passwords."
357 | },
358 | "privilegeRequest.message.store": {
359 | "message": "The extension {extensionName} wants to store passwords.",
360 | "description": "The extension {extensionName} wants to store passwords."
361 | }
362 | }
--------------------------------------------------------------------------------