├── .codebeatignore ├── .codebeatsettings ├── .eslintignore ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .tools ├── build.js └── translate.js ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── _locales ├── ar │ └── messages.json ├── bg │ └── messages.json ├── ca │ └── messages.json ├── cs │ └── messages.json ├── da │ └── messages.json ├── de │ └── messages.json ├── el │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── et │ └── messages.json ├── fi │ └── messages.json ├── fr │ └── messages.json ├── he │ └── messages.json ├── hi │ └── messages.json ├── hr │ └── messages.json ├── hu │ └── messages.json ├── id │ └── messages.json ├── it │ └── messages.json ├── ja │ └── messages.json ├── ko │ └── messages.json ├── la │ └── messages.json ├── lt │ └── messages.json ├── ml │ └── messages.json ├── nb │ └── messages.json ├── nl │ └── messages.json ├── pl │ └── messages.json ├── pt │ └── messages.json ├── pt_BR │ └── messages.json ├── ro │ └── messages.json ├── ru │ └── messages.json ├── sv │ └── messages.json ├── th │ └── messages.json ├── tr │ └── messages.json ├── uk │ └── messages.json ├── zh_CN │ └── messages.json └── zh_TW │ └── messages.json ├── crowdin.yml ├── experiment ├── implementation.js ├── modules │ ├── Services.sys.js │ ├── credentials.sys.js │ ├── dialogStrings.sys.js │ ├── emitters.sys.js │ ├── extension.sys.js │ ├── gui │ │ ├── browserRequest.sys.js │ │ ├── commonDialog.sys.js │ │ ├── utils.sys.js │ │ └── windowListener.sys.js │ ├── log.sys.js │ ├── onlineOfflineControl.sys.js │ ├── prompter │ │ ├── asyncPrompter.sys.js │ │ ├── loginManagerAuthPrompter.sys.js │ │ ├── prompter.sys.js │ │ └── utils.sys.js │ ├── setup.sys.js │ ├── wait.sys.js │ └── wrappers │ │ ├── cal.sys.js │ │ ├── oauth2.sys.js │ │ └── oauth2Module.sys.js └── schema.json ├── from-keepassxc-browser ├── client.js ├── keepass.js ├── nacl-util.min.js └── nacl.min.js ├── global.js ├── icons └── icon.svg ├── main.js ├── manifest.json ├── modal ├── choice │ ├── choice.css │ ├── choice.js │ └── index.html ├── confirm │ ├── confirm.css │ ├── confirm.js │ └── index.html ├── message │ ├── index.html │ ├── message.css │ └── message.js ├── modal.css ├── modalUtils.js └── savingPassword │ ├── index.html │ ├── savingPassword.css │ └── savingPassword.js ├── modules ├── externalPrivileges.js ├── externalRequests.js ├── getCredentials.js ├── keepass.js ├── log.js ├── modal.js ├── nativeMessaging.js ├── selected.js ├── storeCredentials.js └── utils.js ├── options ├── options.css ├── options.html └── options.js ├── package-lock.json ├── package.json ├── releaseNotes.txt └── versions ├── .htaccess └── updates.json /.codebeatignore: -------------------------------------------------------------------------------- 1 | from-keepassxc-browser/** -------------------------------------------------------------------------------- /.codebeatsettings: -------------------------------------------------------------------------------- 1 | { 2 | "JAVASCRIPT": { 3 | "LOC": [80, 90, 100, 120], 4 | "BLOCK_NESTING": [4, 5, 6, 7] 5 | } 6 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | from-keepassxc-browser/* -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mail-ext-artifacts/ 2 | versions/*.xpi 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | }()); -------------------------------------------------------------------------------- /.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 | "menulist", 18 | "menupopup", 19 | "messengercompose", 20 | "mozldap", 21 | "Msgs", 22 | "nacl", 23 | "nntp", 24 | "oauth", 25 | "openpgp", 26 | "Passwd", 27 | "pipnss", 28 | "stringbundle", 29 | "tooltiptext", 30 | "tweetnacl", 31 | "wcap", 32 | "XPCOM" 33 | ], 34 | "cSpell.language": "en,de,en-GB", 35 | "cSpell.ignorePaths": [ 36 | "**/package-lock.json", 37 | "**/node_modules/**", 38 | "**/from-keepassxc-browser/**", 39 | "**/vscode-extension/**", 40 | "**/.git/objects/**", 41 | ".vscode", 42 | ".eslintrc.json" 43 | ], 44 | "eslint.lintTask.enable": true 45 | } -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KeePassXC-Mail [![codebeat badge](https://codebeat.co/badges/0365004b-6336-4f7c-8611-bbd217f29aa0)](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 | *NOTE*: if Thunderbird is installed via Snap or flatpak it might not work (see #95 for further details) 41 | 42 | Run the following command in a terminal: 43 | ```Shell 44 | cat ~/.mozilla/native-messaging-hosts/org.keepassxc.keepassxc_browser.json \ 45 | | sed s/keepassxc-browser@keepassxc.org/keepassxc-mail@kkapsner.de/ \ 46 | | sed s/org.keepassxc.keepassxc_browser/de.kkapsner.keepassxc_mail/ \ 47 | > ~/.mozilla/native-messaging-hosts/de.kkapsner.keepassxc_mail.json 48 | ``` 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.8/keepassxc-browser/background/client.js 98 | * https://github.com/keepassxreboot/keepassxc-browser/blob/1.9.8/keepassxc-browser/background/keepass.js 99 | -------------------------------------------------------------------------------- /_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 | "loadingPasswords": { 67 | "message": "Loading passwords...", 68 | "description": "Loading passwords..." 69 | }, 70 | "noPasswordsFound": { 71 | "message": "No passwords found.", 72 | "description": "No passwords found." 73 | }, 74 | "retry": { 75 | "message": "Retry", 76 | "description": "Retry" 77 | }, 78 | "pickedEntry": { 79 | "message": "Picked entry \"{name|login|\"---\"}\"", 80 | "description": "Picked entry \"{name|login|\"---\"}\"" 81 | }, 82 | "entryLabel": { 83 | "message": "{name|\"Login\"}: {login}", 84 | "description": "{name|\"Login\"}: {login}" 85 | }, 86 | "entryTooltip": { 87 | "message": "Password \"{password}\"", 88 | "description": "Password \"{password}\"" 89 | }, 90 | "credentialInfo": { 91 | "message": "URL: {host}\nLogin: {login|\"---\"}", 92 | "description": "URL: {host}\nLogin: {login}" 93 | }, 94 | "options.title": { 95 | "message": "KeePassXC-Mail Settings", 96 | "description": "KeePassXC-Mail Settings" 97 | }, 98 | "options.databases": { 99 | "message": "Database connections", 100 | "description": "Database connections" 101 | }, 102 | "options.clearSelectedEntries": { 103 | "message": "Clear storage of selected entries", 104 | "description": "Clear storage of selected entries" 105 | }, 106 | "options.clearingSelectedEntries": { 107 | "message": "Clearing storage of selected entries...", 108 | "description": "Clearing storage of selected entries..." 109 | }, 110 | "options.reconnect": { 111 | "message": "Reconnect to Native Messaging", 112 | "description": "Reconnect to Native Messaging" 113 | }, 114 | "options.reconnecting": { 115 | "message": "Reconnecting to Native Messaging...", 116 | "description": "Reconnecting to Native Messaging..." 117 | }, 118 | "options.associate": { 119 | "message": "Connect to KeePassXC", 120 | "description": "Connect to KeePassXC" 121 | }, 122 | "options.associating": { 123 | "message": "Connecting to KeePassXC...", 124 | "description": "Connecting to KeePassXC..." 125 | }, 126 | "options.autoSubmit.label": { 127 | "message": "Auto submit", 128 | "description": "Auto submit" 129 | }, 130 | "options.saveNewCredentials.label": { 131 | "message": "Save new credentials", 132 | "description": "Save new credentials" 133 | }, 134 | "options.autoSaveNewCredentials.label": { 135 | "message": "Save new credentials without prompt", 136 | "description": "Save new credentials without prompt" 137 | }, 138 | "options.connections.id": { 139 | "message": "ID", 140 | "description": "ID" 141 | }, 142 | "options.connections.dbHash": { 143 | "message": "DB hash", 144 | "description": "DB hash" 145 | }, 146 | "options.connections.key": { 147 | "message": "Key", 148 | "description": "Key" 149 | }, 150 | "options.connections.lastUsed": { 151 | "message": "Last used", 152 | "description": "Last used" 153 | }, 154 | "options.connections.created": { 155 | "message": "Created", 156 | "description": "Created" 157 | }, 158 | "options.externalPrivileges": { 159 | "message": "External privileges", 160 | "description": "External privileges" 161 | }, 162 | "options.externalPrivileges.extensionName": { 163 | "message": "extension", 164 | "description": "extension" 165 | }, 166 | "options.externalPrivileges.request": { 167 | "message": "request", 168 | "description": "request" 169 | }, 170 | "options.externalPrivileges.store": { 171 | "message": "store", 172 | "description": "store" 173 | }, 174 | "modal.savingPassword.title": { 175 | "message": "Save password for {host} to KeePass database?", 176 | "description": "Save password for {host} to KeePass database?" 177 | }, 178 | "modal.savingPassword.questionWithLogin": { 179 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?", 180 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?" 181 | }, 182 | "modal.savingPassword.questionWithoutLogin": { 183 | "message": "Do you want to save the entered password for {host} to the KeePass database?", 184 | "description": "Do you want to save the entered password for {host} to the KeePass database?" 185 | }, 186 | "modal.savingPassword.newEntry": { 187 | "message": " - create new entry - ", 188 | "description": " - create new entry - " 189 | }, 190 | "modal.savingPassword.yes": { 191 | "message": "Yes", 192 | "description": "Yes" 193 | }, 194 | "modal.savingPassword.no": { 195 | "message": "No", 196 | "description": "No" 197 | }, 198 | "modal.choice.title": { 199 | "message": "Select correct entry for {host}", 200 | "description": "Select correct entry for {host}" 201 | }, 202 | "modal.choice.textWithLogin": { 203 | "message": "Please select the correct entry for {login} on {host}.", 204 | "description": "Please select the correct entry for {login} on {host}." 205 | }, 206 | "modal.choice.textWithoutLogin": { 207 | "message": "Please select the correct entry for {host}.", 208 | "description": "Please select the correct entry for {host}." 209 | }, 210 | "modal.choice.doNotAskAgain": { 211 | "message": "Do not ask again.", 212 | "description": "Do not ask again." 213 | }, 214 | "modal.choice.ok": { 215 | "message": "OK", 216 | "description": "OK" 217 | }, 218 | "modal.choice.cancel": { 219 | "message": "Cancel", 220 | "description": "Cancel" 221 | }, 222 | "modal.confirm.yes": { 223 | "message": "Yes", 224 | "description": "Yes" 225 | }, 226 | "modal.confirm.no": { 227 | "message": "No", 228 | "description": "No" 229 | }, 230 | "modal.message.ok": { 231 | "message": "OK", 232 | "description": "OK" 233 | }, 234 | "createPasswordGroup.timeout.title": { 235 | "message": "Creating password group timeout", 236 | "description": "Creating password group timeout" 237 | }, 238 | "createPasswordGroup.timeout.message": { 239 | "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.", 240 | "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." 241 | }, 242 | "savePassword.timeout.title": { 243 | "message": "Saving password timeout", 244 | "description": "Saving password timeout" 245 | }, 246 | "savePassword.timeout.message": { 247 | "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.", 248 | "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." 249 | }, 250 | "privilegeRequest.title": { 251 | "message": "Privilege request", 252 | "description": "Privilege request" 253 | }, 254 | "privilegeRequest.message": { 255 | "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.", 256 | "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." 257 | }, 258 | "privilegeRequest.message.request": { 259 | "message": "The extension {extensionName} wants to request passwords.", 260 | "description": "The extension {extensionName} wants to request passwords." 261 | }, 262 | "privilegeRequest.message.store": { 263 | "message": "The extension {extensionName} wants to store passwords.", 264 | "description": "The extension {extensionName} wants to store passwords." 265 | } 266 | } -------------------------------------------------------------------------------- /_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 | "loadingPasswords": { 67 | "message": "Loading passwords...", 68 | "description": "Loading passwords..." 69 | }, 70 | "noPasswordsFound": { 71 | "message": "No passwords found.", 72 | "description": "No passwords found." 73 | }, 74 | "retry": { 75 | "message": "Retry", 76 | "description": "Retry" 77 | }, 78 | "pickedEntry": { 79 | "message": "Picked entry \"{name|login|\"---\"}\"", 80 | "description": "Picked entry \"{name|login|\"---\"}\"" 81 | }, 82 | "entryLabel": { 83 | "message": "{name|\"Login\"}: {login}", 84 | "description": "{name|\"Login\"}: {login}" 85 | }, 86 | "entryTooltip": { 87 | "message": "Password \"{password}\"", 88 | "description": "Password \"{password}\"" 89 | }, 90 | "credentialInfo": { 91 | "message": "URL: {host}\nLogin: {login|\"---\"}", 92 | "description": "URL: {host}\nLogin: {login}" 93 | }, 94 | "options.title": { 95 | "message": "KeePassXC-Mail Settings", 96 | "description": "KeePassXC-Mail Settings" 97 | }, 98 | "options.databases": { 99 | "message": "Database connections", 100 | "description": "Database connections" 101 | }, 102 | "options.clearSelectedEntries": { 103 | "message": "Clear storage of selected entries", 104 | "description": "Clear storage of selected entries" 105 | }, 106 | "options.clearingSelectedEntries": { 107 | "message": "Clearing storage of selected entries...", 108 | "description": "Clearing storage of selected entries..." 109 | }, 110 | "options.reconnect": { 111 | "message": "Reconnect to Native Messaging", 112 | "description": "Reconnect to Native Messaging" 113 | }, 114 | "options.reconnecting": { 115 | "message": "Reconnecting to Native Messaging...", 116 | "description": "Reconnecting to Native Messaging..." 117 | }, 118 | "options.associate": { 119 | "message": "Connect to KeePassXC", 120 | "description": "Connect to KeePassXC" 121 | }, 122 | "options.associating": { 123 | "message": "Connecting to KeePassXC...", 124 | "description": "Connecting to KeePassXC..." 125 | }, 126 | "options.autoSubmit.label": { 127 | "message": "Auto submit", 128 | "description": "Auto submit" 129 | }, 130 | "options.saveNewCredentials.label": { 131 | "message": "Save new credentials", 132 | "description": "Save new credentials" 133 | }, 134 | "options.autoSaveNewCredentials.label": { 135 | "message": "Save new credentials without prompt", 136 | "description": "Save new credentials without prompt" 137 | }, 138 | "options.connections.id": { 139 | "message": "ID", 140 | "description": "ID" 141 | }, 142 | "options.connections.dbHash": { 143 | "message": "DB hash", 144 | "description": "DB hash" 145 | }, 146 | "options.connections.key": { 147 | "message": "Key", 148 | "description": "Key" 149 | }, 150 | "options.connections.lastUsed": { 151 | "message": "Last used", 152 | "description": "Last used" 153 | }, 154 | "options.connections.created": { 155 | "message": "Created", 156 | "description": "Created" 157 | }, 158 | "options.externalPrivileges": { 159 | "message": "External privileges", 160 | "description": "External privileges" 161 | }, 162 | "options.externalPrivileges.extensionName": { 163 | "message": "extension", 164 | "description": "extension" 165 | }, 166 | "options.externalPrivileges.request": { 167 | "message": "request", 168 | "description": "request" 169 | }, 170 | "options.externalPrivileges.store": { 171 | "message": "store", 172 | "description": "store" 173 | }, 174 | "modal.savingPassword.title": { 175 | "message": "Save password for {host} to KeePass database?", 176 | "description": "Save password for {host} to KeePass database?" 177 | }, 178 | "modal.savingPassword.questionWithLogin": { 179 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?", 180 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?" 181 | }, 182 | "modal.savingPassword.questionWithoutLogin": { 183 | "message": "Do you want to save the entered password for {host} to the KeePass database?", 184 | "description": "Do you want to save the entered password for {host} to the KeePass database?" 185 | }, 186 | "modal.savingPassword.newEntry": { 187 | "message": " - create new entry - ", 188 | "description": " - create new entry - " 189 | }, 190 | "modal.savingPassword.yes": { 191 | "message": "Yes", 192 | "description": "Yes" 193 | }, 194 | "modal.savingPassword.no": { 195 | "message": "No", 196 | "description": "No" 197 | }, 198 | "modal.choice.title": { 199 | "message": "Select correct entry for {host}", 200 | "description": "Select correct entry for {host}" 201 | }, 202 | "modal.choice.textWithLogin": { 203 | "message": "Please select the correct entry for {login} on {host}.", 204 | "description": "Please select the correct entry for {login} on {host}." 205 | }, 206 | "modal.choice.textWithoutLogin": { 207 | "message": "Please select the correct entry for {host}.", 208 | "description": "Please select the correct entry for {host}." 209 | }, 210 | "modal.choice.doNotAskAgain": { 211 | "message": "Do not ask again.", 212 | "description": "Do not ask again." 213 | }, 214 | "modal.choice.ok": { 215 | "message": "OK", 216 | "description": "OK" 217 | }, 218 | "modal.choice.cancel": { 219 | "message": "Cancel", 220 | "description": "Cancel" 221 | }, 222 | "modal.confirm.yes": { 223 | "message": "Yes", 224 | "description": "Yes" 225 | }, 226 | "modal.confirm.no": { 227 | "message": "No", 228 | "description": "No" 229 | }, 230 | "modal.message.ok": { 231 | "message": "OK", 232 | "description": "OK" 233 | }, 234 | "createPasswordGroup.timeout.title": { 235 | "message": "Creating password group timeout", 236 | "description": "Creating password group timeout" 237 | }, 238 | "createPasswordGroup.timeout.message": { 239 | "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.", 240 | "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." 241 | }, 242 | "savePassword.timeout.title": { 243 | "message": "Saving password timeout", 244 | "description": "Saving password timeout" 245 | }, 246 | "savePassword.timeout.message": { 247 | "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.", 248 | "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." 249 | }, 250 | "privilegeRequest.title": { 251 | "message": "Privilege request", 252 | "description": "Privilege request" 253 | }, 254 | "privilegeRequest.message": { 255 | "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.", 256 | "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." 257 | }, 258 | "privilegeRequest.message.request": { 259 | "message": "The extension {extensionName} wants to request passwords.", 260 | "description": "The extension {extensionName} wants to request passwords." 261 | }, 262 | "privilegeRequest.message.store": { 263 | "message": "The extension {extensionName} wants to store passwords.", 264 | "description": "The extension {extensionName} wants to store passwords." 265 | } 266 | } -------------------------------------------------------------------------------- /_locales/ca/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 | "loadingPasswords": { 67 | "message": "Loading passwords...", 68 | "description": "Loading passwords..." 69 | }, 70 | "noPasswordsFound": { 71 | "message": "No passwords found.", 72 | "description": "No passwords found." 73 | }, 74 | "retry": { 75 | "message": "Retry", 76 | "description": "Retry" 77 | }, 78 | "pickedEntry": { 79 | "message": "Picked entry \"{name|login|\"---\"}\"", 80 | "description": "Picked entry \"{name|login|\"---\"}\"" 81 | }, 82 | "entryLabel": { 83 | "message": "{name|\"Login\"}: {login}", 84 | "description": "{name|\"Login\"}: {login}" 85 | }, 86 | "entryTooltip": { 87 | "message": "Password \"{password}\"", 88 | "description": "Password \"{password}\"" 89 | }, 90 | "credentialInfo": { 91 | "message": "URL: {host}\nLogin: {login|\"---\"}", 92 | "description": "URL: {host}\nLogin: {login}" 93 | }, 94 | "options.title": { 95 | "message": "KeePassXC-Mail Settings", 96 | "description": "KeePassXC-Mail Settings" 97 | }, 98 | "options.databases": { 99 | "message": "Database connections", 100 | "description": "Database connections" 101 | }, 102 | "options.clearSelectedEntries": { 103 | "message": "Clear storage of selected entries", 104 | "description": "Clear storage of selected entries" 105 | }, 106 | "options.clearingSelectedEntries": { 107 | "message": "Clearing storage of selected entries...", 108 | "description": "Clearing storage of selected entries..." 109 | }, 110 | "options.reconnect": { 111 | "message": "Reconnect to Native Messaging", 112 | "description": "Reconnect to Native Messaging" 113 | }, 114 | "options.reconnecting": { 115 | "message": "Reconnecting to Native Messaging...", 116 | "description": "Reconnecting to Native Messaging..." 117 | }, 118 | "options.associate": { 119 | "message": "Connect to KeePassXC", 120 | "description": "Connect to KeePassXC" 121 | }, 122 | "options.associating": { 123 | "message": "Connecting to KeePassXC...", 124 | "description": "Connecting to KeePassXC..." 125 | }, 126 | "options.autoSubmit.label": { 127 | "message": "Auto submit", 128 | "description": "Auto submit" 129 | }, 130 | "options.saveNewCredentials.label": { 131 | "message": "Save new credentials", 132 | "description": "Save new credentials" 133 | }, 134 | "options.autoSaveNewCredentials.label": { 135 | "message": "Save new credentials without prompt", 136 | "description": "Save new credentials without prompt" 137 | }, 138 | "options.connections.id": { 139 | "message": "ID", 140 | "description": "ID" 141 | }, 142 | "options.connections.dbHash": { 143 | "message": "DB hash", 144 | "description": "DB hash" 145 | }, 146 | "options.connections.key": { 147 | "message": "Key", 148 | "description": "Key" 149 | }, 150 | "options.connections.lastUsed": { 151 | "message": "Last used", 152 | "description": "Last used" 153 | }, 154 | "options.connections.created": { 155 | "message": "Created", 156 | "description": "Created" 157 | }, 158 | "options.externalPrivileges": { 159 | "message": "External privileges", 160 | "description": "External privileges" 161 | }, 162 | "options.externalPrivileges.extensionName": { 163 | "message": "extension", 164 | "description": "extension" 165 | }, 166 | "options.externalPrivileges.request": { 167 | "message": "request", 168 | "description": "request" 169 | }, 170 | "options.externalPrivileges.store": { 171 | "message": "store", 172 | "description": "store" 173 | }, 174 | "modal.savingPassword.title": { 175 | "message": "Save password for {host} to KeePass database?", 176 | "description": "Save password for {host} to KeePass database?" 177 | }, 178 | "modal.savingPassword.questionWithLogin": { 179 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?", 180 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?" 181 | }, 182 | "modal.savingPassword.questionWithoutLogin": { 183 | "message": "Do you want to save the entered password for {host} to the KeePass database?", 184 | "description": "Do you want to save the entered password for {host} to the KeePass database?" 185 | }, 186 | "modal.savingPassword.newEntry": { 187 | "message": " - create new entry - ", 188 | "description": " - create new entry - " 189 | }, 190 | "modal.savingPassword.yes": { 191 | "message": "Yes", 192 | "description": "Yes" 193 | }, 194 | "modal.savingPassword.no": { 195 | "message": "No", 196 | "description": "No" 197 | }, 198 | "modal.choice.title": { 199 | "message": "Select correct entry for {host}", 200 | "description": "Select correct entry for {host}" 201 | }, 202 | "modal.choice.textWithLogin": { 203 | "message": "Please select the correct entry for {login} on {host}.", 204 | "description": "Please select the correct entry for {login} on {host}." 205 | }, 206 | "modal.choice.textWithoutLogin": { 207 | "message": "Please select the correct entry for {host}.", 208 | "description": "Please select the correct entry for {host}." 209 | }, 210 | "modal.choice.doNotAskAgain": { 211 | "message": "Do not ask again.", 212 | "description": "Do not ask again." 213 | }, 214 | "modal.choice.ok": { 215 | "message": "OK", 216 | "description": "OK" 217 | }, 218 | "modal.choice.cancel": { 219 | "message": "Cancel", 220 | "description": "Cancel" 221 | }, 222 | "modal.confirm.yes": { 223 | "message": "Yes", 224 | "description": "Yes" 225 | }, 226 | "modal.confirm.no": { 227 | "message": "No", 228 | "description": "No" 229 | }, 230 | "modal.message.ok": { 231 | "message": "OK", 232 | "description": "OK" 233 | }, 234 | "createPasswordGroup.timeout.title": { 235 | "message": "Creating password group timeout", 236 | "description": "Creating password group timeout" 237 | }, 238 | "createPasswordGroup.timeout.message": { 239 | "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.", 240 | "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." 241 | }, 242 | "savePassword.timeout.title": { 243 | "message": "Saving password timeout", 244 | "description": "Saving password timeout" 245 | }, 246 | "savePassword.timeout.message": { 247 | "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.", 248 | "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." 249 | }, 250 | "privilegeRequest.title": { 251 | "message": "Privilege request", 252 | "description": "Privilege request" 253 | }, 254 | "privilegeRequest.message": { 255 | "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.", 256 | "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." 257 | }, 258 | "privilegeRequest.message.request": { 259 | "message": "The extension {extensionName} wants to request passwords.", 260 | "description": "The extension {extensionName} wants to request passwords." 261 | }, 262 | "privilegeRequest.message.store": { 263 | "message": "The extension {extensionName} wants to store passwords.", 264 | "description": "The extension {extensionName} wants to store passwords." 265 | } 266 | } -------------------------------------------------------------------------------- /_locales/cs/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 | "loadingPasswords": { 67 | "message": "Loading passwords...", 68 | "description": "Loading passwords..." 69 | }, 70 | "noPasswordsFound": { 71 | "message": "No passwords found.", 72 | "description": "No passwords found." 73 | }, 74 | "retry": { 75 | "message": "Retry", 76 | "description": "Retry" 77 | }, 78 | "pickedEntry": { 79 | "message": "Picked entry \"{name|login|\"---\"}\"", 80 | "description": "Picked entry \"{name|login|\"---\"}\"" 81 | }, 82 | "entryLabel": { 83 | "message": "{name|\"Login\"}: {login}", 84 | "description": "{name|\"Login\"}: {login}" 85 | }, 86 | "entryTooltip": { 87 | "message": "Password \"{password}\"", 88 | "description": "Password \"{password}\"" 89 | }, 90 | "credentialInfo": { 91 | "message": "URL: {host}\nLogin: {login|\"---\"}", 92 | "description": "URL: {host}\nLogin: {login}" 93 | }, 94 | "options.title": { 95 | "message": "KeePassXC-Mail Settings", 96 | "description": "KeePassXC-Mail Settings" 97 | }, 98 | "options.databases": { 99 | "message": "Database connections", 100 | "description": "Database connections" 101 | }, 102 | "options.clearSelectedEntries": { 103 | "message": "Clear storage of selected entries", 104 | "description": "Clear storage of selected entries" 105 | }, 106 | "options.clearingSelectedEntries": { 107 | "message": "Clearing storage of selected entries...", 108 | "description": "Clearing storage of selected entries..." 109 | }, 110 | "options.reconnect": { 111 | "message": "Reconnect to Native Messaging", 112 | "description": "Reconnect to Native Messaging" 113 | }, 114 | "options.reconnecting": { 115 | "message": "Reconnecting to Native Messaging...", 116 | "description": "Reconnecting to Native Messaging..." 117 | }, 118 | "options.associate": { 119 | "message": "Connect to KeePassXC", 120 | "description": "Connect to KeePassXC" 121 | }, 122 | "options.associating": { 123 | "message": "Connecting to KeePassXC...", 124 | "description": "Connecting to KeePassXC..." 125 | }, 126 | "options.autoSubmit.label": { 127 | "message": "Auto submit", 128 | "description": "Auto submit" 129 | }, 130 | "options.saveNewCredentials.label": { 131 | "message": "Save new credentials", 132 | "description": "Save new credentials" 133 | }, 134 | "options.autoSaveNewCredentials.label": { 135 | "message": "Save new credentials without prompt", 136 | "description": "Save new credentials without prompt" 137 | }, 138 | "options.connections.id": { 139 | "message": "ID", 140 | "description": "ID" 141 | }, 142 | "options.connections.dbHash": { 143 | "message": "DB hash", 144 | "description": "DB hash" 145 | }, 146 | "options.connections.key": { 147 | "message": "Key", 148 | "description": "Key" 149 | }, 150 | "options.connections.lastUsed": { 151 | "message": "Last used", 152 | "description": "Last used" 153 | }, 154 | "options.connections.created": { 155 | "message": "Created", 156 | "description": "Created" 157 | }, 158 | "options.externalPrivileges": { 159 | "message": "External privileges", 160 | "description": "External privileges" 161 | }, 162 | "options.externalPrivileges.extensionName": { 163 | "message": "extension", 164 | "description": "extension" 165 | }, 166 | "options.externalPrivileges.request": { 167 | "message": "request", 168 | "description": "request" 169 | }, 170 | "options.externalPrivileges.store": { 171 | "message": "store", 172 | "description": "store" 173 | }, 174 | "modal.savingPassword.title": { 175 | "message": "Save password for {host} to KeePass database?", 176 | "description": "Save password for {host} to KeePass database?" 177 | }, 178 | "modal.savingPassword.questionWithLogin": { 179 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?", 180 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?" 181 | }, 182 | "modal.savingPassword.questionWithoutLogin": { 183 | "message": "Do you want to save the entered password for {host} to the KeePass database?", 184 | "description": "Do you want to save the entered password for {host} to the KeePass database?" 185 | }, 186 | "modal.savingPassword.newEntry": { 187 | "message": " - create new entry - ", 188 | "description": " - create new entry - " 189 | }, 190 | "modal.savingPassword.yes": { 191 | "message": "Yes", 192 | "description": "Yes" 193 | }, 194 | "modal.savingPassword.no": { 195 | "message": "No", 196 | "description": "No" 197 | }, 198 | "modal.choice.title": { 199 | "message": "Select correct entry for {host}", 200 | "description": "Select correct entry for {host}" 201 | }, 202 | "modal.choice.textWithLogin": { 203 | "message": "Please select the correct entry for {login} on {host}.", 204 | "description": "Please select the correct entry for {login} on {host}." 205 | }, 206 | "modal.choice.textWithoutLogin": { 207 | "message": "Please select the correct entry for {host}.", 208 | "description": "Please select the correct entry for {host}." 209 | }, 210 | "modal.choice.doNotAskAgain": { 211 | "message": "Do not ask again.", 212 | "description": "Do not ask again." 213 | }, 214 | "modal.choice.ok": { 215 | "message": "OK", 216 | "description": "OK" 217 | }, 218 | "modal.choice.cancel": { 219 | "message": "Cancel", 220 | "description": "Cancel" 221 | }, 222 | "modal.confirm.yes": { 223 | "message": "Yes", 224 | "description": "Yes" 225 | }, 226 | "modal.confirm.no": { 227 | "message": "No", 228 | "description": "No" 229 | }, 230 | "modal.message.ok": { 231 | "message": "OK", 232 | "description": "OK" 233 | }, 234 | "createPasswordGroup.timeout.title": { 235 | "message": "Creating password group timeout", 236 | "description": "Creating password group timeout" 237 | }, 238 | "createPasswordGroup.timeout.message": { 239 | "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.", 240 | "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." 241 | }, 242 | "savePassword.timeout.title": { 243 | "message": "Saving password timeout", 244 | "description": "Saving password timeout" 245 | }, 246 | "savePassword.timeout.message": { 247 | "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.", 248 | "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." 249 | }, 250 | "privilegeRequest.title": { 251 | "message": "Privilege request", 252 | "description": "Privilege request" 253 | }, 254 | "privilegeRequest.message": { 255 | "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.", 256 | "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." 257 | }, 258 | "privilegeRequest.message.request": { 259 | "message": "The extension {extensionName} wants to request passwords.", 260 | "description": "The extension {extensionName} wants to request passwords." 261 | }, 262 | "privilegeRequest.message.store": { 263 | "message": "The extension {extensionName} wants to store passwords.", 264 | "description": "The extension {extensionName} wants to store passwords." 265 | } 266 | } -------------------------------------------------------------------------------- /_locales/da/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 | "loadingPasswords": { 67 | "message": "Loading passwords...", 68 | "description": "Loading passwords..." 69 | }, 70 | "noPasswordsFound": { 71 | "message": "No passwords found.", 72 | "description": "No passwords found." 73 | }, 74 | "retry": { 75 | "message": "Retry", 76 | "description": "Retry" 77 | }, 78 | "pickedEntry": { 79 | "message": "Picked entry \"{name|login|\"---\"}\"", 80 | "description": "Picked entry \"{name|login|\"---\"}\"" 81 | }, 82 | "entryLabel": { 83 | "message": "{name|\"Login\"}: {login}", 84 | "description": "{name|\"Login\"}: {login}" 85 | }, 86 | "entryTooltip": { 87 | "message": "Password \"{password}\"", 88 | "description": "Password \"{password}\"" 89 | }, 90 | "credentialInfo": { 91 | "message": "URL: {host}\nLogin: {login|\"---\"}", 92 | "description": "URL: {host}\nLogin: {login}" 93 | }, 94 | "options.title": { 95 | "message": "KeePassXC-Mail Settings", 96 | "description": "KeePassXC-Mail Settings" 97 | }, 98 | "options.databases": { 99 | "message": "Database connections", 100 | "description": "Database connections" 101 | }, 102 | "options.clearSelectedEntries": { 103 | "message": "Clear storage of selected entries", 104 | "description": "Clear storage of selected entries" 105 | }, 106 | "options.clearingSelectedEntries": { 107 | "message": "Clearing storage of selected entries...", 108 | "description": "Clearing storage of selected entries..." 109 | }, 110 | "options.reconnect": { 111 | "message": "Reconnect to Native Messaging", 112 | "description": "Reconnect to Native Messaging" 113 | }, 114 | "options.reconnecting": { 115 | "message": "Reconnecting to Native Messaging...", 116 | "description": "Reconnecting to Native Messaging..." 117 | }, 118 | "options.associate": { 119 | "message": "Connect to KeePassXC", 120 | "description": "Connect to KeePassXC" 121 | }, 122 | "options.associating": { 123 | "message": "Connecting to KeePassXC...", 124 | "description": "Connecting to KeePassXC..." 125 | }, 126 | "options.autoSubmit.label": { 127 | "message": "Auto submit", 128 | "description": "Auto submit" 129 | }, 130 | "options.saveNewCredentials.label": { 131 | "message": "Save new credentials", 132 | "description": "Save new credentials" 133 | }, 134 | "options.autoSaveNewCredentials.label": { 135 | "message": "Save new credentials without prompt", 136 | "description": "Save new credentials without prompt" 137 | }, 138 | "options.connections.id": { 139 | "message": "ID", 140 | "description": "ID" 141 | }, 142 | "options.connections.dbHash": { 143 | "message": "DB hash", 144 | "description": "DB hash" 145 | }, 146 | "options.connections.key": { 147 | "message": "Key", 148 | "description": "Key" 149 | }, 150 | "options.connections.lastUsed": { 151 | "message": "Last used", 152 | "description": "Last used" 153 | }, 154 | "options.connections.created": { 155 | "message": "Created", 156 | "description": "Created" 157 | }, 158 | "options.externalPrivileges": { 159 | "message": "External privileges", 160 | "description": "External privileges" 161 | }, 162 | "options.externalPrivileges.extensionName": { 163 | "message": "extension", 164 | "description": "extension" 165 | }, 166 | "options.externalPrivileges.request": { 167 | "message": "request", 168 | "description": "request" 169 | }, 170 | "options.externalPrivileges.store": { 171 | "message": "store", 172 | "description": "store" 173 | }, 174 | "modal.savingPassword.title": { 175 | "message": "Save password for {host} to KeePass database?", 176 | "description": "Save password for {host} to KeePass database?" 177 | }, 178 | "modal.savingPassword.questionWithLogin": { 179 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?", 180 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?" 181 | }, 182 | "modal.savingPassword.questionWithoutLogin": { 183 | "message": "Do you want to save the entered password for {host} to the KeePass database?", 184 | "description": "Do you want to save the entered password for {host} to the KeePass database?" 185 | }, 186 | "modal.savingPassword.newEntry": { 187 | "message": " - create new entry - ", 188 | "description": " - create new entry - " 189 | }, 190 | "modal.savingPassword.yes": { 191 | "message": "Yes", 192 | "description": "Yes" 193 | }, 194 | "modal.savingPassword.no": { 195 | "message": "No", 196 | "description": "No" 197 | }, 198 | "modal.choice.title": { 199 | "message": "Select correct entry for {host}", 200 | "description": "Select correct entry for {host}" 201 | }, 202 | "modal.choice.textWithLogin": { 203 | "message": "Please select the correct entry for {login} on {host}.", 204 | "description": "Please select the correct entry for {login} on {host}." 205 | }, 206 | "modal.choice.textWithoutLogin": { 207 | "message": "Please select the correct entry for {host}.", 208 | "description": "Please select the correct entry for {host}." 209 | }, 210 | "modal.choice.doNotAskAgain": { 211 | "message": "Do not ask again.", 212 | "description": "Do not ask again." 213 | }, 214 | "modal.choice.ok": { 215 | "message": "OK", 216 | "description": "OK" 217 | }, 218 | "modal.choice.cancel": { 219 | "message": "Cancel", 220 | "description": "Cancel" 221 | }, 222 | "modal.confirm.yes": { 223 | "message": "Yes", 224 | "description": "Yes" 225 | }, 226 | "modal.confirm.no": { 227 | "message": "No", 228 | "description": "No" 229 | }, 230 | "modal.message.ok": { 231 | "message": "OK", 232 | "description": "OK" 233 | }, 234 | "createPasswordGroup.timeout.title": { 235 | "message": "Creating password group timeout", 236 | "description": "Creating password group timeout" 237 | }, 238 | "createPasswordGroup.timeout.message": { 239 | "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.", 240 | "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." 241 | }, 242 | "savePassword.timeout.title": { 243 | "message": "Saving password timeout", 244 | "description": "Saving password timeout" 245 | }, 246 | "savePassword.timeout.message": { 247 | "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.", 248 | "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." 249 | }, 250 | "privilegeRequest.title": { 251 | "message": "Privilege request", 252 | "description": "Privilege request" 253 | }, 254 | "privilegeRequest.message": { 255 | "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.", 256 | "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." 257 | }, 258 | "privilegeRequest.message.request": { 259 | "message": "The extension {extensionName} wants to request passwords.", 260 | "description": "The extension {extensionName} wants to request passwords." 261 | }, 262 | "privilegeRequest.message.store": { 263 | "message": "The extension {extensionName} wants to store passwords.", 264 | "description": "The extension {extensionName} wants to store passwords." 265 | } 266 | } -------------------------------------------------------------------------------- /_locales/en/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 | "loadingPasswords": { 67 | "message": "Loading passwords...", 68 | "description": "Loading passwords..." 69 | }, 70 | "noPasswordsFound": { 71 | "message": "No passwords found.", 72 | "description": "No passwords found." 73 | }, 74 | "retry": { 75 | "message": "Retry", 76 | "description": "Retry" 77 | }, 78 | "pickedEntry": { 79 | "message": "Picked entry \"{name|login|\"---\"}\"", 80 | "description": "Picked entry \"{name|login|\"---\"}\"" 81 | }, 82 | "entryLabel": { 83 | "message": "{name|\"Login\"}: {login}", 84 | "description": "{name|\"Login\"}: {login}" 85 | }, 86 | "entryTooltip": { 87 | "message": "Password \"{password}\"", 88 | "description": "Password \"{password}\"" 89 | }, 90 | "credentialInfo": { 91 | "message": "URL: {host}\nLogin: {login|\"---\"}", 92 | "description": "URL: {host}\nLogin: {login}" 93 | }, 94 | "options.title": { 95 | "message": "KeePassXC-Mail Settings", 96 | "description": "KeePassXC-Mail Settings" 97 | }, 98 | "options.databases": { 99 | "message": "Database connections", 100 | "description": "Database connections" 101 | }, 102 | "options.clearSelectedEntries": { 103 | "message": "Clear storage of selected entries", 104 | "description": "Clear storage of selected entries" 105 | }, 106 | "options.clearingSelectedEntries": { 107 | "message": "Clearing storage of selected entries...", 108 | "description": "Clearing storage of selected entries..." 109 | }, 110 | "options.reconnect": { 111 | "message": "Reconnect to Native Messaging", 112 | "description": "Reconnect to Native Messaging" 113 | }, 114 | "options.reconnecting": { 115 | "message": "Reconnecting to Native Messaging...", 116 | "description": "Reconnecting to Native Messaging..." 117 | }, 118 | "options.associate": { 119 | "message": "Connect to KeePassXC", 120 | "description": "Connect to KeePassXC" 121 | }, 122 | "options.associating": { 123 | "message": "Connecting to KeePassXC...", 124 | "description": "Connecting to KeePassXC..." 125 | }, 126 | "options.autoSubmit.label": { 127 | "message": "Auto submit", 128 | "description": "Auto submit" 129 | }, 130 | "options.saveNewCredentials.label": { 131 | "message": "Save new credentials", 132 | "description": "Save new credentials" 133 | }, 134 | "options.autoSaveNewCredentials.label": { 135 | "message": "Save new credentials without prompt", 136 | "description": "Save new credentials without prompt" 137 | }, 138 | "options.connections.id": { 139 | "message": "ID", 140 | "description": "ID" 141 | }, 142 | "options.connections.dbHash": { 143 | "message": "DB hash", 144 | "description": "DB hash" 145 | }, 146 | "options.connections.key": { 147 | "message": "Key", 148 | "description": "Key" 149 | }, 150 | "options.connections.lastUsed": { 151 | "message": "Last used", 152 | "description": "Last used" 153 | }, 154 | "options.connections.created": { 155 | "message": "Created", 156 | "description": "Created" 157 | }, 158 | "options.externalPrivileges": { 159 | "message": "External privileges", 160 | "description": "External privileges" 161 | }, 162 | "options.externalPrivileges.extensionName": { 163 | "message": "extension", 164 | "description": "extension" 165 | }, 166 | "options.externalPrivileges.request": { 167 | "message": "request", 168 | "description": "request" 169 | }, 170 | "options.externalPrivileges.store": { 171 | "message": "store", 172 | "description": "store" 173 | }, 174 | "modal.savingPassword.title":{ 175 | "message": "Save password for {host} to KeePass database?", 176 | "description": "Save password for {host} to KeePass database?" 177 | }, 178 | "modal.savingPassword.questionWithLogin":{ 179 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?", 180 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?" 181 | }, 182 | "modal.savingPassword.questionWithoutLogin":{ 183 | "message": "Do you want to save the entered password for {host} to the KeePass database?", 184 | "description": "Do you want to save the entered password for {host} to the KeePass database?" 185 | }, 186 | "modal.savingPassword.newEntry":{ 187 | "message": " - create new entry - ", 188 | "description": " - create new entry - " 189 | }, 190 | "modal.savingPassword.yes":{ 191 | "message": "Yes", 192 | "description": "Yes" 193 | }, 194 | "modal.savingPassword.no":{ 195 | "message": "No", 196 | "description": "No" 197 | }, 198 | "modal.choice.title":{ 199 | "message": "Select correct entry for {host}", 200 | "description": "Select correct entry for {host}" 201 | }, 202 | "modal.choice.textWithLogin":{ 203 | "message": "Please select the correct entry for {login} on {host}.", 204 | "description": "Please select the correct entry for {login} on {host}." 205 | }, 206 | "modal.choice.textWithoutLogin":{ 207 | "message": "Please select the correct entry for {host}.", 208 | "description": "Please select the correct entry for {host}." 209 | }, 210 | "modal.choice.doNotAskAgain":{ 211 | "message": "Do not ask again.", 212 | "description": "Do not ask again." 213 | }, 214 | "modal.choice.ok":{ 215 | "message": "OK", 216 | "description": "OK" 217 | }, 218 | "modal.choice.cancel":{ 219 | "message": "Cancel", 220 | "description": "Cancel" 221 | }, 222 | "modal.confirm.yes":{ 223 | "message": "Yes", 224 | "description": "Yes" 225 | }, 226 | "modal.confirm.no":{ 227 | "message": "No", 228 | "description": "No" 229 | }, 230 | "modal.message.ok":{ 231 | "message": "OK", 232 | "description": "OK" 233 | }, 234 | "createPasswordGroup.timeout.title":{ 235 | "message": "Creating password group timeout", 236 | "description": "Creating password group timeout" 237 | }, 238 | "createPasswordGroup.timeout.message":{ 239 | "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.", 240 | "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." 241 | }, 242 | "savePassword.timeout.title":{ 243 | "message": "Saving password timeout", 244 | "description": "Saving password timeout" 245 | }, 246 | "savePassword.timeout.message":{ 247 | "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.", 248 | "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." 249 | }, 250 | "privilegeRequest.title": { 251 | "message": "Privilege request", 252 | "description": "Privilege request" 253 | }, 254 | "privilegeRequest.message": { 255 | "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.", 256 | "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." 257 | }, 258 | "privilegeRequest.message.request": { 259 | "message": "The extension {extensionName} wants to request passwords.", 260 | "description": "The extension {extensionName} wants to request passwords." 261 | }, 262 | "privilegeRequest.message.store": { 263 | "message": "The extension {extensionName} wants to store passwords.", 264 | "description": "The extension {extensionName} wants to store passwords." 265 | } 266 | } -------------------------------------------------------------------------------- /_locales/et/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 | "loadingPasswords": { 67 | "message": "Loading passwords...", 68 | "description": "Loading passwords..." 69 | }, 70 | "noPasswordsFound": { 71 | "message": "No passwords found.", 72 | "description": "No passwords found." 73 | }, 74 | "retry": { 75 | "message": "Retry", 76 | "description": "Retry" 77 | }, 78 | "pickedEntry": { 79 | "message": "Picked entry \"{name|login|\"---\"}\"", 80 | "description": "Picked entry \"{name|login|\"---\"}\"" 81 | }, 82 | "entryLabel": { 83 | "message": "{name|\"Login\"}: {login}", 84 | "description": "{name|\"Login\"}: {login}" 85 | }, 86 | "entryTooltip": { 87 | "message": "Password \"{password}\"", 88 | "description": "Password \"{password}\"" 89 | }, 90 | "credentialInfo": { 91 | "message": "URL: {host}\nLogin: {login|\"---\"}", 92 | "description": "URL: {host}\nLogin: {login}" 93 | }, 94 | "options.title": { 95 | "message": "KeePassXC-Mail Settings", 96 | "description": "KeePassXC-Mail Settings" 97 | }, 98 | "options.databases": { 99 | "message": "Database connections", 100 | "description": "Database connections" 101 | }, 102 | "options.clearSelectedEntries": { 103 | "message": "Clear storage of selected entries", 104 | "description": "Clear storage of selected entries" 105 | }, 106 | "options.clearingSelectedEntries": { 107 | "message": "Clearing storage of selected entries...", 108 | "description": "Clearing storage of selected entries..." 109 | }, 110 | "options.reconnect": { 111 | "message": "Reconnect to Native Messaging", 112 | "description": "Reconnect to Native Messaging" 113 | }, 114 | "options.reconnecting": { 115 | "message": "Reconnecting to Native Messaging...", 116 | "description": "Reconnecting to Native Messaging..." 117 | }, 118 | "options.associate": { 119 | "message": "Connect to KeePassXC", 120 | "description": "Connect to KeePassXC" 121 | }, 122 | "options.associating": { 123 | "message": "Connecting to KeePassXC...", 124 | "description": "Connecting to KeePassXC..." 125 | }, 126 | "options.autoSubmit.label": { 127 | "message": "Auto submit", 128 | "description": "Auto submit" 129 | }, 130 | "options.saveNewCredentials.label": { 131 | "message": "Save new credentials", 132 | "description": "Save new credentials" 133 | }, 134 | "options.autoSaveNewCredentials.label": { 135 | "message": "Save new credentials without prompt", 136 | "description": "Save new credentials without prompt" 137 | }, 138 | "options.connections.id": { 139 | "message": "ID", 140 | "description": "ID" 141 | }, 142 | "options.connections.dbHash": { 143 | "message": "DB hash", 144 | "description": "DB hash" 145 | }, 146 | "options.connections.key": { 147 | "message": "Key", 148 | "description": "Key" 149 | }, 150 | "options.connections.lastUsed": { 151 | "message": "Last used", 152 | "description": "Last used" 153 | }, 154 | "options.connections.created": { 155 | "message": "Created", 156 | "description": "Created" 157 | }, 158 | "options.externalPrivileges": { 159 | "message": "External privileges", 160 | "description": "External privileges" 161 | }, 162 | "options.externalPrivileges.extensionName": { 163 | "message": "extension", 164 | "description": "extension" 165 | }, 166 | "options.externalPrivileges.request": { 167 | "message": "request", 168 | "description": "request" 169 | }, 170 | "options.externalPrivileges.store": { 171 | "message": "store", 172 | "description": "store" 173 | }, 174 | "modal.savingPassword.title": { 175 | "message": "Save password for {host} to KeePass database?", 176 | "description": "Save password for {host} to KeePass database?" 177 | }, 178 | "modal.savingPassword.questionWithLogin": { 179 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?", 180 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?" 181 | }, 182 | "modal.savingPassword.questionWithoutLogin": { 183 | "message": "Do you want to save the entered password for {host} to the KeePass database?", 184 | "description": "Do you want to save the entered password for {host} to the KeePass database?" 185 | }, 186 | "modal.savingPassword.newEntry": { 187 | "message": " - create new entry - ", 188 | "description": " - create new entry - " 189 | }, 190 | "modal.savingPassword.yes": { 191 | "message": "Yes", 192 | "description": "Yes" 193 | }, 194 | "modal.savingPassword.no": { 195 | "message": "No", 196 | "description": "No" 197 | }, 198 | "modal.choice.title": { 199 | "message": "Select correct entry for {host}", 200 | "description": "Select correct entry for {host}" 201 | }, 202 | "modal.choice.textWithLogin": { 203 | "message": "Please select the correct entry for {login} on {host}.", 204 | "description": "Please select the correct entry for {login} on {host}." 205 | }, 206 | "modal.choice.textWithoutLogin": { 207 | "message": "Please select the correct entry for {host}.", 208 | "description": "Please select the correct entry for {host}." 209 | }, 210 | "modal.choice.doNotAskAgain": { 211 | "message": "Do not ask again.", 212 | "description": "Do not ask again." 213 | }, 214 | "modal.choice.ok": { 215 | "message": "OK", 216 | "description": "OK" 217 | }, 218 | "modal.choice.cancel": { 219 | "message": "Cancel", 220 | "description": "Cancel" 221 | }, 222 | "modal.confirm.yes": { 223 | "message": "Yes", 224 | "description": "Yes" 225 | }, 226 | "modal.confirm.no": { 227 | "message": "No", 228 | "description": "No" 229 | }, 230 | "modal.message.ok": { 231 | "message": "OK", 232 | "description": "OK" 233 | }, 234 | "createPasswordGroup.timeout.title": { 235 | "message": "Creating password group timeout", 236 | "description": "Creating password group timeout" 237 | }, 238 | "createPasswordGroup.timeout.message": { 239 | "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.", 240 | "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." 241 | }, 242 | "savePassword.timeout.title": { 243 | "message": "Saving password timeout", 244 | "description": "Saving password timeout" 245 | }, 246 | "savePassword.timeout.message": { 247 | "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.", 248 | "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." 249 | }, 250 | "privilegeRequest.title": { 251 | "message": "Privilege request", 252 | "description": "Privilege request" 253 | }, 254 | "privilegeRequest.message": { 255 | "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.", 256 | "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." 257 | }, 258 | "privilegeRequest.message.request": { 259 | "message": "The extension {extensionName} wants to request passwords.", 260 | "description": "The extension {extensionName} wants to request passwords." 261 | }, 262 | "privilegeRequest.message.store": { 263 | "message": "The extension {extensionName} wants to store passwords.", 264 | "description": "The extension {extensionName} wants to store passwords." 265 | } 266 | } -------------------------------------------------------------------------------- /_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 | "loadingPasswords": { 67 | "message": "パスワードを読み込んでいます...", 68 | "description": "Loading passwords..." 69 | }, 70 | "noPasswordsFound": { 71 | "message": "パスワードが見つかりませんでした", 72 | "description": "No passwords found." 73 | }, 74 | "retry": { 75 | "message": "再試行", 76 | "description": "Retry" 77 | }, 78 | "pickedEntry": { 79 | "message": "Picked entry \"{name|login|\"---\"}\"", 80 | "description": "Picked entry \"{name|login|\"---\"}\"" 81 | }, 82 | "entryLabel": { 83 | "message": "{name|\"ログイン\"}: {login}", 84 | "description": "{name|\"Login\"}: {login}" 85 | }, 86 | "entryTooltip": { 87 | "message": "パスワード \"{password}\"", 88 | "description": "Password \"{password}\"" 89 | }, 90 | "credentialInfo": { 91 | "message": "URL: {host}\nログイン: {login|\"--\"}", 92 | "description": "URL: {host}\nLogin: {login}" 93 | }, 94 | "options.title": { 95 | "message": "KeePassXC-Mailの設定", 96 | "description": "KeePassXC-Mail Settings" 97 | }, 98 | "options.databases": { 99 | "message": "データベース接続", 100 | "description": "Database connections" 101 | }, 102 | "options.clearSelectedEntries": { 103 | "message": "Clear storage of selected entries", 104 | "description": "Clear storage of selected entries" 105 | }, 106 | "options.clearingSelectedEntries": { 107 | "message": "Clearing storage of selected entries...", 108 | "description": "Clearing storage of selected entries..." 109 | }, 110 | "options.reconnect": { 111 | "message": "Native Messagingに再接続", 112 | "description": "Reconnect to Native Messaging" 113 | }, 114 | "options.reconnecting": { 115 | "message": "Reconnecting to Native Messaging...", 116 | "description": "Reconnecting to Native Messaging..." 117 | }, 118 | "options.associate": { 119 | "message": "KeePassXCに接続する", 120 | "description": "Connect to KeePassXC" 121 | }, 122 | "options.associating": { 123 | "message": "Connecting to KeePassXC...", 124 | "description": "Connecting to KeePassXC..." 125 | }, 126 | "options.autoSubmit.label": { 127 | "message": "自動送信", 128 | "description": "Auto submit" 129 | }, 130 | "options.saveNewCredentials.label": { 131 | "message": "新しい認証情報を保存", 132 | "description": "Save new credentials" 133 | }, 134 | "options.autoSaveNewCredentials.label": { 135 | "message": "Save new credentials without prompt", 136 | "description": "Save new credentials without prompt" 137 | }, 138 | "options.connections.id": { 139 | "message": "ID", 140 | "description": "ID" 141 | }, 142 | "options.connections.dbHash": { 143 | "message": "DB hash", 144 | "description": "DB hash" 145 | }, 146 | "options.connections.key": { 147 | "message": "Key", 148 | "description": "Key" 149 | }, 150 | "options.connections.lastUsed": { 151 | "message": "Last used", 152 | "description": "Last used" 153 | }, 154 | "options.connections.created": { 155 | "message": "Created", 156 | "description": "Created" 157 | }, 158 | "options.externalPrivileges": { 159 | "message": "External privileges", 160 | "description": "External privileges" 161 | }, 162 | "options.externalPrivileges.extensionName": { 163 | "message": "extension", 164 | "description": "extension" 165 | }, 166 | "options.externalPrivileges.request": { 167 | "message": "request", 168 | "description": "request" 169 | }, 170 | "options.externalPrivileges.store": { 171 | "message": "store", 172 | "description": "store" 173 | }, 174 | "modal.savingPassword.title": { 175 | "message": "Save password for {host} to KeePass database?", 176 | "description": "Save password for {host} to KeePass database?" 177 | }, 178 | "modal.savingPassword.questionWithLogin": { 179 | "message": "Do you want to save the entered password for {login} on {host} to the KeePass database?", 180 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?" 181 | }, 182 | "modal.savingPassword.questionWithoutLogin": { 183 | "message": "Do you want to save the entered password for {host} to the KeePass database?", 184 | "description": "Do you want to save the entered password for {host} to the KeePass database?" 185 | }, 186 | "modal.savingPassword.newEntry": { 187 | "message": " - create new entry - ", 188 | "description": " - create new entry - " 189 | }, 190 | "modal.savingPassword.yes": { 191 | "message": "Yes", 192 | "description": "Yes" 193 | }, 194 | "modal.savingPassword.no": { 195 | "message": "No", 196 | "description": "No" 197 | }, 198 | "modal.choice.title": { 199 | "message": "Select correct entry for {host}", 200 | "description": "Select correct entry for {host}" 201 | }, 202 | "modal.choice.textWithLogin": { 203 | "message": "Please select the correct entry for {login} on {host}.", 204 | "description": "Please select the correct entry for {login} on {host}." 205 | }, 206 | "modal.choice.textWithoutLogin": { 207 | "message": "Please select the correct entry for {host}.", 208 | "description": "Please select the correct entry for {host}." 209 | }, 210 | "modal.choice.doNotAskAgain": { 211 | "message": "Do not ask again.", 212 | "description": "Do not ask again." 213 | }, 214 | "modal.choice.ok": { 215 | "message": "OK", 216 | "description": "OK" 217 | }, 218 | "modal.choice.cancel": { 219 | "message": "Cancel", 220 | "description": "Cancel" 221 | }, 222 | "modal.confirm.yes": { 223 | "message": "Yes", 224 | "description": "Yes" 225 | }, 226 | "modal.confirm.no": { 227 | "message": "No", 228 | "description": "No" 229 | }, 230 | "modal.message.ok": { 231 | "message": "OK", 232 | "description": "OK" 233 | }, 234 | "createPasswordGroup.timeout.title": { 235 | "message": "Creating password group timeout", 236 | "description": "Creating password group timeout" 237 | }, 238 | "createPasswordGroup.timeout.message": { 239 | "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.", 240 | "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." 241 | }, 242 | "savePassword.timeout.title": { 243 | "message": "Saving password timeout", 244 | "description": "Saving password timeout" 245 | }, 246 | "savePassword.timeout.message": { 247 | "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.", 248 | "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." 249 | }, 250 | "privilegeRequest.title": { 251 | "message": "Privilege request", 252 | "description": "Privilege request" 253 | }, 254 | "privilegeRequest.message": { 255 | "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.", 256 | "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." 257 | }, 258 | "privilegeRequest.message.request": { 259 | "message": "The extension {extensionName} wants to request passwords.", 260 | "description": "The extension {extensionName} wants to request passwords." 261 | }, 262 | "privilegeRequest.message.store": { 263 | "message": "The extension {extensionName} wants to store passwords.", 264 | "description": "The extension {extensionName} wants to store passwords." 265 | } 266 | } -------------------------------------------------------------------------------- /_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 | "loadingPasswords": { 67 | "message": "正在加载密码...", 68 | "description": "Loading passwords..." 69 | }, 70 | "noPasswordsFound": { 71 | "message": "找不到密码。", 72 | "description": "No passwords found." 73 | }, 74 | "retry": { 75 | "message": "重试", 76 | "description": "Retry" 77 | }, 78 | "pickedEntry": { 79 | "message": "Picked entry \"{name|login|\"---\"}\"", 80 | "description": "Picked entry \"{name|login|\"---\"}\"" 81 | }, 82 | "entryLabel": { 83 | "message": "{name|\"Login\"}: {login}", 84 | "description": "{name|\"Login\"}: {login}" 85 | }, 86 | "entryTooltip": { 87 | "message": "Password \"{password}\"", 88 | "description": "Password \"{password}\"" 89 | }, 90 | "credentialInfo": { 91 | "message": "URL: {host}\nLogin: {login|\"---\"}", 92 | "description": "URL: {host}\nLogin: {login}" 93 | }, 94 | "options.title": { 95 | "message": "KeePassXC-Mail 设置", 96 | "description": "KeePassXC-Mail Settings" 97 | }, 98 | "options.databases": { 99 | "message": "已连接数据库", 100 | "description": "Database connections" 101 | }, 102 | "options.clearSelectedEntries": { 103 | "message": "Clear storage of selected entries", 104 | "description": "Clear storage of selected entries" 105 | }, 106 | "options.clearingSelectedEntries": { 107 | "message": "Clearing storage of selected entries...", 108 | "description": "Clearing storage of selected entries..." 109 | }, 110 | "options.reconnect": { 111 | "message": "重新连接到 Native Messaging", 112 | "description": "Reconnect to Native Messaging" 113 | }, 114 | "options.reconnecting": { 115 | "message": "正在重新连接到 Native Messaging...", 116 | "description": "Reconnecting to Native Messaging..." 117 | }, 118 | "options.associate": { 119 | "message": "连接 KeePassXC", 120 | "description": "Connect to KeePassXC" 121 | }, 122 | "options.associating": { 123 | "message": "正在连接到 KeePassXC...", 124 | "description": "Connecting to KeePassXC..." 125 | }, 126 | "options.autoSubmit.label": { 127 | "message": "自动提交", 128 | "description": "Auto submit" 129 | }, 130 | "options.saveNewCredentials.label": { 131 | "message": "保存新凭据", 132 | "description": "Save new credentials" 133 | }, 134 | "options.autoSaveNewCredentials.label": { 135 | "message": "保存新凭据时不提示", 136 | "description": "Save new credentials without prompt" 137 | }, 138 | "options.connections.id": { 139 | "message": "标识符", 140 | "description": "ID" 141 | }, 142 | "options.connections.dbHash": { 143 | "message": "数据库哈希", 144 | "description": "DB hash" 145 | }, 146 | "options.connections.key": { 147 | "message": "密钥", 148 | "description": "Key" 149 | }, 150 | "options.connections.lastUsed": { 151 | "message": "上次使用", 152 | "description": "Last used" 153 | }, 154 | "options.connections.created": { 155 | "message": "创建时间", 156 | "description": "Created" 157 | }, 158 | "options.externalPrivileges": { 159 | "message": "External privileges", 160 | "description": "External privileges" 161 | }, 162 | "options.externalPrivileges.extensionName": { 163 | "message": "extension", 164 | "description": "extension" 165 | }, 166 | "options.externalPrivileges.request": { 167 | "message": "request", 168 | "description": "request" 169 | }, 170 | "options.externalPrivileges.store": { 171 | "message": "store", 172 | "description": "store" 173 | }, 174 | "modal.savingPassword.title": { 175 | "message": "保存 {host} 的密码到KeePass 数据库?", 176 | "description": "Save password for {host} to KeePass database?" 177 | }, 178 | "modal.savingPassword.questionWithLogin": { 179 | "message": "你想保存 {login} 在 {host} 邮箱服务密码到 KeePass 数据库吗?", 180 | "description": "Do you want to save the entered password for {login} on {host} to the KeePass database?" 181 | }, 182 | "modal.savingPassword.questionWithoutLogin": { 183 | "message": "你想保存 {host} 邮箱服务密码到 KeePass 数据库吗?", 184 | "description": "Do you want to save the entered password for {host} to the KeePass database?" 185 | }, 186 | "modal.savingPassword.newEntry": { 187 | "message": " - 创建新条目 - ", 188 | "description": " - create new entry - " 189 | }, 190 | "modal.savingPassword.yes": { 191 | "message": "是", 192 | "description": "Yes" 193 | }, 194 | "modal.savingPassword.no": { 195 | "message": "否", 196 | "description": "No" 197 | }, 198 | "modal.choice.title": { 199 | "message": "选择正确的登录配置项来登录 {host}", 200 | "description": "Select correct entry for {host}" 201 | }, 202 | "modal.choice.textWithLogin": { 203 | "message": "请为 {login} 在 {host} 邮箱服务中选择正确的登录配置项。", 204 | "description": "Please select the correct entry for {login} on {host}." 205 | }, 206 | "modal.choice.textWithoutLogin": { 207 | "message": "请为 {host} 邮箱服务中选择正确的登录配置项。", 208 | "description": "Please select the correct entry for {host}." 209 | }, 210 | "modal.choice.doNotAskAgain": { 211 | "message": "不再询问。", 212 | "description": "Do not ask again." 213 | }, 214 | "modal.choice.ok": { 215 | "message": "确定", 216 | "description": "OK" 217 | }, 218 | "modal.choice.cancel": { 219 | "message": "取消", 220 | "description": "Cancel" 221 | }, 222 | "modal.confirm.yes": { 223 | "message": "Yes", 224 | "description": "Yes" 225 | }, 226 | "modal.confirm.no": { 227 | "message": "No", 228 | "description": "No" 229 | }, 230 | "modal.message.ok": { 231 | "message": "好的", 232 | "description": "OK" 233 | }, 234 | "createPasswordGroup.timeout.title": { 235 | "message": "创建密码分组超时", 236 | "description": "Creating password group timeout" 237 | }, 238 | "createPasswordGroup.timeout.message": { 239 | "message": "{account} 的密码保存失败。密码已保存到Thunderbird的内置密码管理器中。\n\n创建密码分组超时。请检查 KeePassXC 设置是否允许创建群组。", 240 | "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." 241 | }, 242 | "savePassword.timeout.title": { 243 | "message": "保存密码超时", 244 | "description": "Saving password timeout" 245 | }, 246 | "savePassword.timeout.message": { 247 | "message": "{account} 的密码保存失败。密码已保存到Thunderbird的内置密码管理器中。\n\n请检查 KeePassXC 设置是否允许保存或更新密码。", 248 | "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." 249 | }, 250 | "privilegeRequest.title": { 251 | "message": "Privilege request", 252 | "description": "Privilege request" 253 | }, 254 | "privilegeRequest.message": { 255 | "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.", 256 | "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." 257 | }, 258 | "privilegeRequest.message.request": { 259 | "message": "The extension {extensionName} wants to request passwords.", 260 | "description": "The extension {extensionName} wants to request passwords." 261 | }, 262 | "privilegeRequest.message.store": { 263 | "message": "The extension {extensionName} wants to store passwords.", 264 | "description": "The extension {extensionName} wants to store passwords." 265 | } 266 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | importOwnModule("gui/browserRequest.sys.js"); 72 | 73 | importOwnModule("wrappers/cal.sys.js"); 74 | importOwnModule("wrappers/oauth2Module.sys.js"); 75 | importOwnModule("wrappers/oauth2.sys.js"); 76 | 77 | exports.credentials = class extends ExtensionCommon.ExtensionAPI { 78 | getAPI(context){ 79 | return { 80 | credentials: { 81 | onCredentialRequested: new ExtensionCommon.EventManager({ 82 | context, 83 | name: "credentials.onCredentialRequested", 84 | register(fire){ 85 | async function callback(event, credentialInfo){ 86 | try { 87 | return await fire.async(credentialInfo); 88 | } 89 | catch (e){ 90 | console.error(e); 91 | return false; 92 | } 93 | } 94 | 95 | passwordRequestEmitter.add(callback); 96 | return function(){ 97 | passwordRequestEmitter.remove(callback); 98 | }; 99 | }, 100 | }).api(), 101 | onNewCredential: new ExtensionCommon.EventManager({ 102 | context, 103 | name: "credentials.onNewCredential", 104 | register(fire){ 105 | async function callback(event, credentialInfo){ 106 | try { 107 | const callback = credentialInfo.callback; 108 | delete credentialInfo.callback; 109 | const returnValue = await fire.async(credentialInfo); 110 | if (callback){ 111 | await callback(returnValue); 112 | } 113 | return returnValue; 114 | } 115 | catch (e){ 116 | console.error(e); 117 | return false; 118 | } 119 | } 120 | 121 | passwordEmitter.on("password", callback); 122 | return function(){ 123 | passwordEmitter.off("password", callback); 124 | }; 125 | }, 126 | }).api(), 127 | }, 128 | }; 129 | } 130 | 131 | onShutdown(isAppShutdown){ 132 | if (isAppShutdown) { 133 | return; // the application gets unloaded anyway 134 | } 135 | resProto.setSubstitution( 136 | "keepassxc-mail", 137 | null 138 | ); 139 | 140 | // Flush all caches. 141 | Services.obs.notifyObservers(null, "startupcache-invalidate"); 142 | } 143 | }; 144 | 145 | 146 | })(this); -------------------------------------------------------------------------------- /experiment/modules/Services.sys.js: -------------------------------------------------------------------------------- 1 | /* globals globalThis*/ 2 | export const Services = globalThis.Services; -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /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"); -------------------------------------------------------------------------------- /experiment/modules/gui/browserRequest.sys.js: -------------------------------------------------------------------------------- 1 | /* globals Components */ 2 | import { cal } from "resource:///modules/calendar/calUtils.sys.mjs"; 3 | import { addWindowListener } from "./windowListener.sys.js"; 4 | 5 | const getCredentialInfoForGdata = function(){ 6 | return function getCredentialInfoForGdata(window){ 7 | const request = window.arguments[0].wrappedJSObject; 8 | 9 | return { 10 | host: request.url, 11 | login: request.account.extraAuthParams.filter(p => p[0] === "login_hint").map(p => p[1])[0], 12 | loginChangeable: true 13 | }; 14 | }; 15 | }(); 16 | const getGuiOperationsForGdata = function(){ 17 | const STATE_STOP = Components.interfaces.nsIWebProgressListener.STATE_STOP; 18 | 19 | return function(window){ 20 | const requestFrame = window.document.getElementById("requestFrame"); 21 | let resolveLoginForm; 22 | let resolvePasswordForm; 23 | const loginFormPromise = new Promise(function(resolve){resolveLoginForm = resolve;}); 24 | const passwordFormPromise = new Promise(function(resolve){resolvePasswordForm = resolve;}); 25 | 26 | const progressListener = { 27 | QueryInterface: cal.generateQI([ 28 | Components.interfaces.nsIWebProgressListener, 29 | Components.interfaces.nsISupportsWeakReference 30 | ]), 31 | onStateChange: function(_webProgress, _request, stateFlags, _status){ 32 | if (!(stateFlags & STATE_STOP)) return; 33 | if (!requestFrame.contentDocument){ 34 | resolveLoginForm(false); 35 | return; 36 | } 37 | const forms = requestFrame.contentDocument.forms; 38 | if (!forms || forms.length === 0) return; 39 | const form = forms[0]; 40 | 41 | if (form.Email){ 42 | resolveLoginForm(form); 43 | } 44 | if (form.Passwd){ 45 | resolvePasswordForm(form); 46 | } 47 | }, 48 | }; 49 | requestFrame.addProgressListener(progressListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL); 50 | 51 | const document = window.document; 52 | return { 53 | progressListener, 54 | window, 55 | guiParent: document.getElementById("browserRequest"), 56 | doHandle: async function(){ 57 | const loginForm = await loginFormPromise; 58 | if (loginForm){ 59 | return true; 60 | } 61 | return false; 62 | }, 63 | submit: async function(){ 64 | const loginForm = await loginFormPromise; 65 | loginForm.signIn.click(); 66 | 67 | const passwordForm = await passwordFormPromise; 68 | passwordForm.signIn.click(); 69 | }, 70 | registerOnSubmit: function(){}, 71 | fillCredentials: async function(credentialInfo, credentials){ 72 | const loginForm = await loginFormPromise; 73 | loginForm.Email.value = credentials.login; 74 | 75 | const passwordForm = await passwordFormPromise; 76 | passwordForm.Passwd.value = credentials.password; 77 | 78 | } 79 | }; 80 | }; 81 | }(); 82 | addWindowListener({ 83 | name: "gdataPasswordDialogListener", 84 | chromeURLs: [ 85 | "chrome://gdata-provider/content/browserRequest.xul", 86 | "chrome://messenger/content/browserRequest.xhtml" 87 | ], 88 | getCredentialInfo: getCredentialInfoForGdata, 89 | getGuiOperations: getGuiOperationsForGdata 90 | }); -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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}):`); -------------------------------------------------------------------------------- /experiment/modules/onlineOfflineControl.sys.js: -------------------------------------------------------------------------------- 1 | /* globals ChromeUtils */ 2 | import { log } from "./log.sys.js"; 3 | import { Services } from "./Services.sys.js"; 4 | import { OfflineStartup } from "resource:///modules/OfflineStartup.sys.mjs"; 5 | import { addSetup } from "./setup.sys.js"; 6 | 7 | // online/offline control 8 | const ALWAYS_OFFLINE = 3; 9 | const topics = [ 10 | "profile-change-net-teardown", 11 | "quit-application-granted", "quit-application", 12 | ]; 13 | class ShutdownObserver{ 14 | QueryInterface = ChromeUtils.generateQI(["nsIObserver"]); 15 | 16 | startObserving(){ 17 | topics.forEach((topic) => { 18 | log("start observing", topic); 19 | try { 20 | Services.obs.addObserver(this, topic); 21 | } 22 | catch (error){ 23 | log("unable to observer", topic, error, error.stack); 24 | } 25 | }); 26 | } 27 | stopObserving(){ 28 | topics.forEach((topic) => { 29 | Services.obs.removeObserver(this, topic); 30 | }); 31 | } 32 | 33 | save(){ 34 | if (Services.prefs.prefHasUserValue("keepassxc-mail.offline.startup_state")){ 35 | log("Startup online state already saved - do not overwrite"); 36 | return; 37 | } 38 | const valueToSave = Services.prefs.getIntPref("offline.startup_state"); 39 | log("Saving startup online state:", valueToSave); 40 | Services.prefs.setIntPref( 41 | "keepassxc-mail.offline.startup_state", 42 | valueToSave 43 | ); 44 | } 45 | setOfflineStartup(){ 46 | log("Set to offline startup"); 47 | Services.prefs.setIntPref( 48 | "offline.startup_state", 49 | ALWAYS_OFFLINE 50 | ); 51 | } 52 | restore(){ 53 | log("Restoring startup online state"); 54 | this.restore = () => {}; 55 | if (Services.prefs.prefHasUserValue("keepassxc-mail.offline.startup_state")){ 56 | const storedValue = Services.prefs.getIntPref("keepassxc-mail.offline.startup_state"); 57 | log("keepassxc-mail.offline.startup_state:", storedValue); 58 | log("offline.startup_state:", Services.prefs.getIntPref("offline.startup_state")); 59 | Services.prefs.setIntPref( 60 | "offline.startup_state", 61 | storedValue 62 | ); 63 | Services.prefs.clearUserPref("keepassxc-mail.offline.startup_state"); 64 | 65 | if (storedValue === ALWAYS_OFFLINE){ 66 | log("Offline startup -> do nothing"); 67 | return; 68 | } 69 | 70 | log("Calling OfflineStartup.prototype.onProfileStartup"); 71 | OfflineStartup.prototype.onProfileStartup(); 72 | 73 | if (Services.prefs.getBoolPref("offline.autoDetect")){ 74 | log("auto detect: need to test for online state"); 75 | Services.io.offline = false; 76 | Services.io.manageOfflineStatus = Services.prefs.getBoolPref( 77 | "offline.autoDetect" 78 | ); 79 | } 80 | } 81 | } 82 | 83 | observe(_subject, topic, _data){ 84 | log("observed", _subject, topic, _data); 85 | if ( 86 | Services.prefs.prefHasUserValue("keepassxc-mail.offline_control") && 87 | Services.prefs.getBoolPref("keepassxc-mail.offline_control") 88 | ){ 89 | this.save(); 90 | this.setOfflineStartup(); 91 | } 92 | else { 93 | log( 94 | "Startup offline control not activated. " + 95 | "Set the boolean keepassxc-mail.offline_control to true to enable it." 96 | ); 97 | } 98 | this.stopObserving(); 99 | } 100 | } 101 | const shutdownObserver = new ShutdownObserver(); 102 | 103 | addSetup({ 104 | setup: function(){ 105 | shutdownObserver.restore(); 106 | shutdownObserver.startObserving(); 107 | }, 108 | shutdown: function(){ 109 | shutdownObserver.stopObserving(); 110 | } 111 | }); -------------------------------------------------------------------------------- /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); 57 | registerPromptFunctions(promptFunctions); -------------------------------------------------------------------------------- /experiment/modules/prompter/loginManagerAuthPrompter.sys.js: -------------------------------------------------------------------------------- 1 | import { initPromptFunctions, registerPromptFunctions } from "./utils.sys.js"; 2 | import { LoginManagerAuthPrompter } from "resource://gre/modules/LoginManagerAuthPrompter.sys.mjs"; 3 | 4 | const promptFunctions = [ 5 | { 6 | name: "promptPassword", 7 | promptType: "promptPassword", 8 | titleIndex: 0, 9 | textIndex: 1, 10 | realmIndex: 2, 11 | passwordObjectIndex: 4, 12 | }, 13 | { 14 | name: "promptUsernameAndPassword", 15 | promptType: "promptUserAndPass", 16 | titleIndex: 0, 17 | textIndex: 1, 18 | realmIndex: 2, 19 | passwordObjectIndex: 5, 20 | } 21 | ]; 22 | initPromptFunctions(promptFunctions, LoginManagerAuthPrompter.prototype); 23 | registerPromptFunctions(promptFunctions); -------------------------------------------------------------------------------- /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 | promptType: "promptUserAndPass", 21 | titleIndex: 2, 22 | textIndex: 3, 23 | passwordObjectIndex: 5, 24 | }, 25 | { 26 | name: "promptPassword", 27 | promptType: "promptPassword", 28 | titleIndex: 1, 29 | textIndex: 2, 30 | passwordObjectIndex: 3, 31 | }, 32 | { 33 | name: "promptPasswordBC", 34 | promptType: "promptPassword", 35 | titleIndex: 2, 36 | textIndex: 3, 37 | passwordObjectIndex: 4, 38 | }, 39 | { 40 | name: "asyncPromptPassword", 41 | promptType: "promptPassword", 42 | titleIndex: 2, 43 | textIndex: 3, 44 | passwordObjectIndex: 4, 45 | }, 46 | { 47 | name: "promptAuth", 48 | promptType: "promptUserAndPass", 49 | channelIndex: 1, 50 | authInfoIndex: 3, 51 | passwordObjectIndex: 3, 52 | }, 53 | { 54 | name: "promptAuthBC", 55 | promptType: "promptUserAndPass", 56 | channelIndex: 2, 57 | authInfoIndex: 4, 58 | passwordObjectIndex: 4, 59 | }, 60 | { 61 | name: "asyncPromptAuth", 62 | promptType: "promptUserAndPass", 63 | channelIndex: 1, 64 | authInfoIndex: 5, 65 | passwordObjectIndex: 5, 66 | }, 67 | { 68 | name: "asyncPromptAuthBC", 69 | promptType: "promptUserAndPass", 70 | channelIndex: 2, 71 | authInfoIndex: 6, 72 | passwordObjectIndex: 6, 73 | }, 74 | ]; 75 | initPromptFunctions(promptFunctions, Prompter.prototype); 76 | registerPromptFunctions(promptFunctions); -------------------------------------------------------------------------------- /experiment/modules/prompter/utils.sys.js: -------------------------------------------------------------------------------- 1 | import { Services } from "../Services.sys.js"; 2 | import { waitForCredentials } from "../wait.sys.js"; 3 | import { getCredentialInfoFromStrings, getCredentialInfoFromStringsAndProtocol } from "../dialogStrings.sys.js"; 4 | import { addSetup } from "../setup.sys.js"; 5 | 6 | export function registerPromptFunctions(promptFunctions){ 7 | addSetup({ 8 | setup: function(){ 9 | promptFunctions.forEach(function(promptFunction){ 10 | promptFunction.object[promptFunction.name] = promptFunction.replacement; 11 | }); 12 | }, 13 | shutdown: function(){ 14 | promptFunctions.forEach(function(promptFunction){ 15 | promptFunction.object[promptFunction.name] = promptFunction.original; 16 | }); 17 | } 18 | }); 19 | } 20 | 21 | function createPromptDataFunctions(promptFunction){ 22 | const promptDataFunctions = []; 23 | if (promptFunction.dataFunction){ 24 | promptDataFunctions.push(promptFunction.dataFunction); 25 | } 26 | if (promptFunction.hasOwnProperty("realmIndex")){ 27 | promptDataFunctions.push(function(args){ 28 | const realm = args[promptFunction.realmIndex]; 29 | let [realmHost, , realmLogin] = this._getRealmInfo(realm); 30 | let protocol; 31 | if (realmHost && realmHost.startsWith("mailbox://")){ 32 | realmHost = realmHost.replace("mailbox://", "pop3://"); 33 | protocol = "pop3"; 34 | } 35 | else { 36 | protocol = realmHost && Services.io.newURI(realmHost).scheme; 37 | } 38 | // realm data provides the correct protocol but may have wrong server name 39 | const {host: stringHost, login: stringLogin, mayAddProtocol} = getCredentialInfoFromStringsAndProtocol( 40 | args[promptFunction.titleIndex], 41 | args[promptFunction.textIndex], 42 | protocol 43 | ); 44 | return { 45 | mayAddProtocol, 46 | protocol, 47 | host: stringHost || realmHost, 48 | login: stringLogin || decodeURIComponent(realmLogin), 49 | realm, 50 | }; 51 | }); 52 | } 53 | if (promptFunction.hasOwnProperty("titleIndex")){ 54 | promptDataFunctions.push(function(args){ 55 | return getCredentialInfoFromStrings( 56 | args[promptFunction.titleIndex], 57 | args[promptFunction.textIndex] 58 | ); 59 | }); 60 | } 61 | if (promptFunction.hasOwnProperty("authInfoIndex")){ 62 | promptDataFunctions.push(function(args){ 63 | return { 64 | host: args[promptFunction.channelIndex].URI.spec, 65 | login: args[promptFunction.authInfoIndex].username, 66 | realm: args[promptFunction.authInfoIndex].realm, 67 | }; 68 | }); 69 | } 70 | return promptDataFunctions; 71 | } 72 | 73 | function initPromptFunction(promptFunction, object){ 74 | promptFunction.object = object; 75 | promptFunction.promptDataFunctions = createPromptDataFunctions(promptFunction); 76 | promptFunction.loginChangeable = promptFunction.promptType === "promptUserAndPass"; 77 | promptFunction.original = object[promptFunction.name]; 78 | promptFunction.replacement = function(...args){ 79 | const data = promptFunction.promptDataFunctions.reduce((data, func) => { 80 | if (!data){ 81 | return func.call(this, args); 82 | } 83 | return data; 84 | }, false); 85 | if ( 86 | data && 87 | ( 88 | promptFunction.hasOwnProperty("passwordObjectIndex") || 89 | promptFunction.setCredentials 90 | ) 91 | ){ 92 | const { credentials } = waitForCredentials({ 93 | host: data.host, 94 | login: data.login, 95 | loginChangeable: promptFunction.loginChangeable, 96 | }); 97 | if (credentials.length === 1){ 98 | if (promptFunction.setCredentials){ 99 | promptFunction.setCredentials(args, credentials[0].login, credentials[0].password); 100 | } 101 | else { 102 | args[promptFunction.passwordObjectIndex].value = credentials[0].password; 103 | } 104 | 105 | if (promptFunction.hasOwnProperty("savePasswordIndex")){ 106 | args[promptFunction.savePasswordIndex].value = false; 107 | } 108 | return true; 109 | } 110 | if (data.mayAddProtocol && promptFunction.hasOwnProperty("titleIndex")){ 111 | args[promptFunction.titleIndex] += ` (${data.protocol})`; 112 | } 113 | } 114 | const ret = promptFunction.original.call(this, ...args); 115 | return ret; 116 | }; 117 | } 118 | 119 | export function initPromptFunctions(promptFunctions, object){ 120 | promptFunctions.forEach(function(promptFunction){ 121 | initPromptFunction(promptFunction, object); 122 | }); 123 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /experiment/modules/wait.sys.js: -------------------------------------------------------------------------------- 1 | import { Services } from "./Services.sys.js"; 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 | } -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /experiment/modules/wrappers/oauth2.sys.js: -------------------------------------------------------------------------------- 1 | import { OAuth2 } from "resource:///modules/OAuth2.sys.mjs"; 2 | import { getRefreshToken } from "./oauth2Module.sys.js"; 3 | import { Services } from "../Services.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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /experiment/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "credentials", 4 | "events": [ 5 | { 6 | "name": "onCredentialRequested", 7 | "description": "Fires when credentials are requested", 8 | "type": "function", 9 | "parameters": [{ 10 | "name": "credentialInformation", 11 | "description": "Information about the requested credentials", 12 | "type": "object", 13 | "properties": { 14 | "host": { 15 | "description": "The host including protocol for which credentials are requested", 16 | "type": "string" 17 | }, 18 | "login": { 19 | "description": "The login for which credentials are requested", 20 | "optional": true, 21 | "type": "string" 22 | }, 23 | "loginChangeable": { 24 | "description": "If the login is changeable in the password dialog", 25 | "optional": true, 26 | "type": "boolean" 27 | }, 28 | "openChoiceDialog": { 29 | "description": "If the choice dialog should be displayed if more than one entry is found or auto submit is disabled", 30 | "optional": true, 31 | "type": "boolean" 32 | } 33 | } 34 | }] 35 | }, 36 | { 37 | "name": "onNewCredential", 38 | "description": "Fires when new credentials are entered", 39 | "type": "function", 40 | "parameters": [{ 41 | "name": "credentialInformation", 42 | "description": "Information about the entered credentials", 43 | "type": "object", 44 | "properties": { 45 | "host": { 46 | "description": "The host including protocol for which credentials were entered.", 47 | "type": "string" 48 | }, 49 | "login": { 50 | "description": "The login for which credentials were entered.", 51 | "optional": true, 52 | "type": "string" 53 | }, 54 | "password": { 55 | "description": "The password that was entered.", 56 | "optional": true, 57 | "type": "string" 58 | } 59 | } 60 | }] 61 | } 62 | ] 63 | } 64 | ] -------------------------------------------------------------------------------- /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 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 | }; -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 19 | 23 | 27 | 28 | 36 | 37 | 39 | 40 | 42 | image/svg+xml 43 | 45 | 46 | 47 | 48 | 49 | 55 | 61 | 65 | 69 | 70 | -------------------------------------------------------------------------------- /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 | const getCredentialsModule = import("./modules/getCredentials.js"); 33 | browser.credentials.onCredentialRequested.addListener(async function(credentialInfo){ 34 | const { getCredentials } = await getCredentialsModule; 35 | return await getCredentials(credentialInfo); 36 | }); 37 | 38 | 39 | const storeCredentialsModule = import("./modules/storeCredentials.js"); 40 | browser.credentials.onNewCredential.addListener(async function(credentialInfo){ 41 | const { storeCredentials } = await storeCredentialsModule; 42 | return await storeCredentials(credentialInfo); 43 | }); 44 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keepassxc-mail", 3 | "description": "MailExtension to talk to keepassxc", 4 | "version": "1.12", 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": "139.0" 51 | } 52 | }, 53 | "default_locale": "en", 54 | "manifest_version": 2 55 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | }}); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modal/confirm/confirm.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.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | }}); -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /modal/savingPassword/index.html: -------------------------------------------------------------------------------- 1 | 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 | -------------------------------------------------------------------------------- /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/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 | }}); -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/getCredentials.js: -------------------------------------------------------------------------------- 1 | import { log } from "./log.js"; 2 | import { isReady as isKeepassReady, keepass } 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 | log("keepassXC provided", credentialsForHost.length, "logins"); 31 | let autoSubmit = (await browser.storage.local.get({autoSubmit: false})).autoSubmit; 32 | if (autoSubmit){ 33 | const requestId = credentialInfo.login + "|" + credentialInfo.host; 34 | const now = Date.now(); 35 | if (now - lastRequest[requestId] < 1000){ 36 | autoSubmit = false; 37 | } 38 | lastRequest[requestId] = now; 39 | } 40 | 41 | if ( 42 | credentialInfo.openChoiceDialog && 43 | credentialsForHost.length 44 | ){ 45 | const selectedUuid = await choiceModal( 46 | credentialInfo.host, 47 | credentialInfo.login, 48 | credentialsForHost.map(function (data){ 49 | return { 50 | name: data.name, 51 | login: data.login, 52 | uuid: data.uuid, 53 | autoSubmit: autoSubmit && !data.skipAutoSubmit 54 | }; 55 | }) 56 | ); 57 | const filteredCredentialsForHost = credentialsForHost.filter(e => e.uuid === selectedUuid); 58 | if (!selectedUuid || filteredCredentialsForHost.length){ 59 | credentialsForHost = filteredCredentialsForHost; 60 | } 61 | } 62 | 63 | return { 64 | autoSubmit, 65 | credentials: credentialsForHost 66 | }; 67 | } -------------------------------------------------------------------------------- /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 | 6 | const k = keepass; 7 | const kc = keepassClient; 8 | 9 | export { k as keepass, kc as keepassClient }; 10 | 11 | async function loadKeyRing(){ 12 | let loadCount = 0; 13 | let lastLoadError = null; 14 | while (!keepass.keyRing){ 15 | await wait(50); 16 | loadCount += 1; 17 | try { 18 | const item = await browser.storage.local.get({ 19 | latestKeePassXC: { 20 | version: "", 21 | lastChecked: null 22 | }, 23 | keyRing: {} 24 | }); 25 | keepass.latestKeePassXC = item.latestKeePassXC; 26 | keepass.keyRing = item.keyRing || {}; 27 | } 28 | catch (error){ 29 | lastLoadError = error; 30 | } 31 | } 32 | if (lastLoadError){ 33 | log("Loaded key ring", loadCount, "times", lastLoadError); 34 | } 35 | } 36 | 37 | async function checkKeyRingStorage(){ 38 | function objectsEqual(obj1, obj2){ 39 | const keys1 = Object.keys(obj1); 40 | const keys2 = Object.keys(obj2); 41 | if (keys1.length !== keys2.length){ 42 | return false; 43 | } 44 | return keys1.every(function(key){ 45 | const value1 = obj1[key]; 46 | const value2 = obj2[key]; 47 | if ((typeof value1) !== (typeof value2)){ 48 | return false; 49 | } 50 | if ((typeof value1) === "object"){ 51 | return objectsEqual(value1, value2); 52 | } 53 | return value1 === value2; 54 | }); 55 | } 56 | // check if the key ring actually saved in the storage 57 | const databaseHashes = Object.keys(keepass.keyRing); 58 | if (databaseHashes.length){ 59 | let storedKeyRing = (await browser.storage.local.get({keyRing: {}})).keyRing; 60 | while (!objectsEqual(keepass.keyRing, storedKeyRing)){ 61 | await wait(500); 62 | log("Store key ring"); 63 | try { 64 | await browser.storage.local.set({keyRing: keepass.keyRing}); 65 | storedKeyRing = (await browser.storage.local.get({keyRing: {}})).keyRing; 66 | } 67 | catch (e){ 68 | log("storing key ring failed:", e); 69 | } 70 | } 71 | } 72 | } 73 | 74 | export const isReady = (() => { 75 | async function initialize(){ 76 | // load key ring - initially done in keepass.js but it fails sometimes... 77 | await loadKeyRing(); 78 | await keepass.migrateKeyRing(); 79 | await connect(); 80 | await keepass.enableAutomaticReconnect(); 81 | await keepass.associate(); 82 | // check key ring storage - initially done in keepass.js but it fails sometimes... 83 | checkKeyRingStorage(); 84 | 85 | return {connect, disconnect}; 86 | } 87 | let keepassReady = initialize(); 88 | keepassReady.catch((error) => log("Initialization failed:", error)); 89 | return async function(){ 90 | try { 91 | return await keepassReady; 92 | } 93 | catch (error){ 94 | keepassReady = initialize(); 95 | keepassReady.catch((error) => log("Initialization failed:", error)); 96 | return await keepassReady; 97 | } 98 | }; 99 | })(); -------------------------------------------------------------------------------- /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 | }(); -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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); 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 | } -------------------------------------------------------------------------------- /modules/storeCredentials.js: -------------------------------------------------------------------------------- 1 | import { log } from "./log.js"; 2 | import { keepass, isReady as isKeepassReady } 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 (existingCredentials.some(function(credential){ 73 | return credential.password === credentialInfo.password; 74 | })){ 75 | log("the password is already stored"); 76 | return true; 77 | } 78 | 79 | const cachedId = credentialInfo.login? 80 | `${credentialInfo.login}@${credentialInfo.host}`: 81 | credentialInfo.host; 82 | const cached = selectedEntries.get(cachedId) || null; 83 | const {save, uuid} = autoSaveNewCredentials? 84 | {save: true, uuid: cached?.uuid || null}: 85 | await savingPasswordModal( 86 | credentialInfo.host, 87 | credentialInfo.login, 88 | existingCredentials.map(function (data){ 89 | return { 90 | name: data.name, 91 | login: data.login, 92 | uuid: data.uuid, 93 | preselected: data.uuid === cached?.uuid 94 | }; 95 | }) 96 | ); 97 | if (!save){ 98 | log("the user decided to not store the password"); 99 | return true; 100 | } 101 | 102 | return await storeCredentialsToDatabase(credentialInfo, uuid); 103 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
extensionrequeststore
32 |
33 | 34 |

Database connections

35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
IDDB hashkeylast usedcreated
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /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 | backgroundPage.clearSelectedEntries(); 116 | }, 117 | reconnect: async function(){ 118 | const backgroundPage = browser.extension.getBackgroundPage(); 119 | const { connect, disconnect } = await backgroundPage.isKeepassReady(); 120 | await disconnect(); 121 | await connect(true); 122 | await backgroundPage.keepass.associate(); 123 | await updateConnections(); 124 | }, 125 | associate: async function(){ 126 | await browser.extension.getBackgroundPage().keepass.associate(); 127 | await updateConnections(); 128 | } 129 | }; 130 | 131 | async function wait(ms){ 132 | return new Promise(function(resolve){ 133 | window.setTimeout(function(){ 134 | resolve(); 135 | }, ms); 136 | }); 137 | } 138 | 139 | document.querySelectorAll(".action").forEach(async function(button){ 140 | const activeMessageId = button.dataset.activeMessage; 141 | const activeMessage = activeMessageId? browser.i18n.getMessage(activeMessageId): false; 142 | let active = false; 143 | button.addEventListener("click", async function(){ 144 | if (active){ 145 | return; 146 | } 147 | const oldContent = button.textContent; 148 | button.disabled = true; 149 | active = true; 150 | const promises = [actions[button.id]()]; 151 | if (activeMessage){ 152 | button.textContent = activeMessage; 153 | promises.push(wait(500)); 154 | } 155 | await Promise.all(promises); 156 | button.textContent = oldContent; 157 | active = false; 158 | button.disabled = false; 159 | }); 160 | }); 161 | document.querySelectorAll("input.setting").forEach(async function(input){ 162 | const settingName = input.id; 163 | const currentValue = await browser.storage.local.get([settingName]); 164 | let type = typeof currentValue[settingName]; 165 | if (type === "undefined"){ 166 | currentValue[settingName] = JSON.parse(input.dataset.defaultValue); 167 | type = typeof currentValue[settingName]; 168 | } 169 | switch (type){ 170 | case "boolean": 171 | input.checked = currentValue[settingName]; 172 | break; 173 | default: 174 | input.value = currentValue[settingName]; 175 | } 176 | input.addEventListener("change", function(){ 177 | let newValue = input.value; 178 | switch (typeof currentValue[settingName]){ 179 | case "boolean": 180 | newValue = input.checked; 181 | break; 182 | case "number": 183 | newValue = parseFloat(input.value); 184 | break; 185 | } 186 | browser.storage.local.set({ 187 | [settingName]: newValue 188 | }); 189 | }); 190 | }); 191 | 192 | document.querySelectorAll("*[data-translation]").forEach(function(node){ 193 | node.textContent = browser.i18n.getMessage(node.dataset.translation); 194 | }); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /releaseNotes.txt: -------------------------------------------------------------------------------- 1 | Version 1.12 2 | 3 | new features: 4 | 5 | - 6 | 7 | changes: 8 | 9 | - update keepass.js to version 1.9.8 10 | - update client.js to version 1.9.8 (no changes) 11 | 12 | fixes: 13 | 14 | - 15 | 16 | Version 1.11 17 | 18 | new features: 19 | 20 | - bump version support to 139.0 21 | - add API for other mail extensions to request and store passwords 22 | 23 | changes: 24 | 25 | - new version scheme 26 | - major code refactoring 27 | - remove support for Cardbook (as requested by ATN) 28 | - bump minimal version to 128 (older not tested after code refactoring) 29 | 30 | fixes: 31 | 32 | - reduce delay when resizing a modal window 33 | - show correct code location for logging 34 | 35 | Version 1.10.1 36 | 37 | new features: 38 | 39 | - new translations 40 | 41 | changes: 42 | 43 | - update keepass.js to version 1.9.7 (no changes) 44 | - update client.js to version 1.9.7 (no changes) 45 | 46 | fixes: 47 | 48 | - fix support for Cardbook 49 | 50 | Version 1.10 51 | 52 | new features: 53 | 54 | - bump version support to 138.0 55 | 56 | changes: 57 | 58 | - update keepass.js to version 1.9.6 (no changes) 59 | - update client.js to version 1.9.6 (no changes) 60 | 61 | Version 1.9.1 62 | 63 | fixes: 64 | 65 | - added missing dependency from keepassxc-browser 66 | 67 | Version 1.9 68 | 69 | new features: 70 | 71 | - bump version support to 135.0 72 | - added experimental startup control 73 | 74 | changes: 75 | 76 | - improve display of database connections 77 | - update keepass.js to version 1.9.5 78 | - update client.js to version 1.9.5 (no changes) 79 | 80 | fixes: 81 | 82 | - auto submitted entries are not considered when storing new passwords 83 | 84 | Version 1.8 85 | 86 | new features: 87 | 88 | - bump version support to 129.0 89 | 90 | changes: 91 | 92 | - update keepass.js to version 1.9.1 93 | - update client.js to version 1.9.1 94 | 95 | fixes: 96 | 97 | - do not use innerHTML assignments 98 | 99 | Version 1.7 100 | 101 | new features: 102 | 103 | - added LDAP support 104 | - added timeout to password saving 105 | - bump version support to 126.0 106 | 107 | changes: 108 | 109 | - update keepass.js to version 1.9.0.2 110 | - update client.js to version 1.9.0.2 111 | 112 | fixes: 113 | 114 | - update OAuth handling for 126.0 115 | 116 | Version 1.6.1 117 | 118 | fixes: 119 | 120 | - was not working in 117 nightly due to changes in openpgp internationalization 121 | - also not working in 115 release 122 | 123 | Version 1.6 124 | 125 | new features: 126 | 127 | - bump version support to 117.0 128 | 129 | Version 1.5 130 | 131 | new features: 132 | 133 | - bump version support to 113.0 134 | 135 | changes: 136 | 137 | - update keepass.js to version 1.8.6 138 | - update client.js to version 1.8.6 139 | - improve support for cardbook 140 | 141 | fixes: 142 | 143 | - do not save password for CardDAV in Thunderbird password manager 144 | 145 | Version 1.4 146 | 147 | new features: 148 | 149 | - bump version support to 111.0 150 | - save oauth token to database 151 | - allow overwriting existing database entries 152 | 153 | changes: 154 | 155 | - update keepass.js to version 1.8.3.1 156 | - update client.js to version 1.8.3.1 157 | - improve modal dialog communication 158 | - do not show credential picker on oauth window when password cannot be entered 159 | - choice dialog: also respect "do not ask again" when cancel is clicked 160 | - add "do not ask again" to saving password dialog 161 | - improve logging on password saving 162 | - add cache to getter of oauth 163 | 164 | fixes: 165 | 166 | - modal dialog size 167 | 168 | Version 1.3 169 | 170 | new features: 171 | 172 | - bump version support to 107.0 173 | 174 | fixes: 175 | 176 | - make experiment multi process save 177 | 178 | Version 1.2 179 | 180 | new features: 181 | 182 | - bump version support to 106.0 183 | - prevent password prompts when possible 184 | 185 | changes: 186 | 187 | - own choice dialog respects auto submit 188 | - use own choice dialog 189 | - code cleanup 190 | - also save new password if an entry with a different password is found in the database 191 | 192 | fixes: 193 | 194 | - improve startup behaviour 195 | 196 | Version 1.1 197 | 198 | new features: 199 | 200 | - add support for openpgp private key password prompts 201 | - bump version support to 105.0 202 | - added entry selection dialog 203 | 204 | Version 1.0.3 205 | 206 | new features: 207 | 208 | - bump version support to 104.0 209 | 210 | changes: 211 | 212 | - filtering for correct login is now case insensitive 213 | - update keepass.js to version 1.8.0 214 | - added client.js from version 1.8.0 215 | - bump minimal version to 74.0 (needed for keepass.js and client.js) 216 | 217 | Version 1.0.2.1 218 | 219 | new features: 220 | 221 | - bump version support to 102.* for ESR 222 | 223 | Version 1.0.2 224 | 225 | fixes: 226 | 227 | - modal dialog not working due to too tight timing 228 | 229 | Version 1.0.1 230 | 231 | new features: 232 | 233 | - try to connect later if initial connection failed 234 | 235 | fixes: 236 | 237 | - check all native application names on reconnect 238 | 239 | Version 1.0 240 | 241 | new features: 242 | 243 | - added support for different native application names 244 | 245 | changes: 246 | 247 | - update tweetnacl to version 1.0.3 248 | - update keepass.js to version 1.7.11 249 | 250 | Version 0.9 251 | 252 | new features: 253 | 254 | - distribution is now over https://addons.thunderbird.net/thunderbird/addon/keepassxc-mail/ 255 | 256 | Version 0.1.12 257 | 258 | fixes: 259 | 260 | - password prompts without a given username did not find any password in newer Thunderbird version 261 | 262 | Version 0.1.11 263 | 264 | fixes: 265 | 266 | - entries without login name were filtered when no login was expected 267 | 268 | Version 0.1.10 269 | 270 | new features: 271 | 272 | - new translations 273 | 274 | changes: 275 | 276 | - bump version support to 98.* 277 | 278 | fixes: 279 | 280 | - primary password prompt was not recognized 281 | 282 | Version 0.1.9.1 283 | 284 | fixes: 285 | 286 | - oauth authentication, gdata support and cardbook support not working due removed function spinEventLoopUntilOrShutdown in Thunderbird 91 287 | 288 | Version 0.1.9 289 | 290 | new features: 291 | 292 | - added support for Thunderbird 94.* 293 | - new translations 294 | 295 | Version 0.1.8 296 | 297 | new features: 298 | 299 | - add confirmation dialog before saving to database 300 | - if the saving is denied oauth tokens are written to the built in password manager 301 | - added support for Cardbook 302 | 303 | Version 0.1.7.1 304 | 305 | fixes: 306 | 307 | - primary password was saved too often in database 308 | 309 | Version 0.1.7 310 | 311 | new features: 312 | 313 | - added support for primary password 314 | - added support for oauth 315 | 316 | Version 0.1.6 317 | 318 | new features: 319 | 320 | - new translations 321 | 322 | changes: 323 | 324 | - bump version support to 86.* 325 | 326 | fixes: 327 | 328 | - check for already existing credential before saving 329 | - realm information may contain wrong user name 330 | 331 | Version 0.1.5 332 | 333 | new features: 334 | 335 | - hovering over the status text in a password prompt show now the password search parameters 336 | - password search parameters are logged in the console 337 | 338 | changes: 339 | 340 | - add new dialog texts 341 | - "mailbox://..." is now "pop3://..." again 342 | 343 | fixes: 344 | 345 | - key ring not initialized during startup 346 | - realm information may contain wrong server URL 347 | 348 | Version 0.1.4: 349 | 350 | new features: 351 | 352 | - hide the "save password" checkbox 353 | - added support for Thunderbird 80 354 | 355 | Version 0.1.3: 356 | 357 | new features: 358 | 359 | - save new credentials to database 360 | 361 | fixes: 362 | 363 | - enable usage of KeePass with KeePassNatMsg 364 | - fix connection display being duplicated upon reassociation 365 | - removed hard dependency on Lightning 366 | 367 | Version 0.1.2: 368 | 369 | new features: 370 | 371 | - added auto update 372 | - added oauth token storage support 373 | 374 | fixes: 375 | 376 | - do not break if a string bundle is not present 377 | 378 | Version 0.1.1: 379 | 380 | new features: 381 | 382 | - added support for "Provider for Google Calendar" 383 | 384 | fixes: 385 | 386 | - respect skipAutoSubmit 387 | 388 | Version 0.1.0: 389 | 390 | First MVP that supports IMAP, POP3, SMTP and calendar password prompts. 391 | -------------------------------------------------------------------------------- /versions/.htaccess: -------------------------------------------------------------------------------- 1 | Options +Indexes 2 | -------------------------------------------------------------------------------- /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 | } 131 | } 132 | } --------------------------------------------------------------------------------