├── .CHANGELOG.MD.un~ ├── mail_extension ├── files_in_xpi.lst ├── images │ ├── cb-128px.png │ ├── cb-16px.png │ ├── cb-256px.png │ ├── cb-32px.png │ ├── cb-48px.png │ └── cb-64px.png ├── cb_options.css ├── manifest.json ├── cb_api_schema.json ├── cb_options.js ├── cb_options.html ├── cb_api.js ├── modules │ └── cb_tools.mjs └── cb_background.js ├── documentation ├── Slide1.JPG ├── 20201127_cb_thunderlink_architecture.gif ├── 50_cb_thunderlink │ ├── thumbnail_1200x800.jpg │ ├── 5_architecture │ │ ├── arch.jpg │ │ ├── 20201127_cb_thunderlink_architecture.pptx │ │ └── en.md │ ├── jj-ying-PDxYfXVlK2M-unsplash.jpg │ ├── 2_installation │ │ ├── en.md │ │ ├── 1_windows │ │ │ └── en.md │ │ └── 2_linux │ │ │ └── en.md │ ├── en.md │ └── 3_malfunction │ │ └── en.md └── 20201127_cb_thunderlink_architecture.pptx ├── README.md ├── cb_thunderlink.json ├── .gitignore ├── cb_build.sh ├── cb_build.bat ├── cb_thunderlink.spec ├── CHANGELOG.MD ├── cb_put_licenses_on_files.py ├── cb_thunderlink.py └── LICENSE /.CHANGELOG.MD.un~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/.CHANGELOG.MD.un~ -------------------------------------------------------------------------------- /mail_extension/files_in_xpi.lst: -------------------------------------------------------------------------------- 1 | images/* 2 | modules/* 3 | *.js 4 | *.html 5 | *.json 6 | *.css 7 | -------------------------------------------------------------------------------- /documentation/Slide1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/documentation/Slide1.JPG -------------------------------------------------------------------------------- /mail_extension/images/cb-128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/mail_extension/images/cb-128px.png -------------------------------------------------------------------------------- /mail_extension/images/cb-16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/mail_extension/images/cb-16px.png -------------------------------------------------------------------------------- /mail_extension/images/cb-256px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/mail_extension/images/cb-256px.png -------------------------------------------------------------------------------- /mail_extension/images/cb-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/mail_extension/images/cb-32px.png -------------------------------------------------------------------------------- /mail_extension/images/cb-48px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/mail_extension/images/cb-48px.png -------------------------------------------------------------------------------- /mail_extension/images/cb-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/mail_extension/images/cb-64px.png -------------------------------------------------------------------------------- /documentation/20201127_cb_thunderlink_architecture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/documentation/20201127_cb_thunderlink_architecture.gif -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/thumbnail_1200x800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/documentation/50_cb_thunderlink/thumbnail_1200x800.jpg -------------------------------------------------------------------------------- /documentation/20201127_cb_thunderlink_architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/documentation/20201127_cb_thunderlink_architecture.pptx -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/5_architecture/arch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/documentation/50_cb_thunderlink/5_architecture/arch.jpg -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/jj-ying-PDxYfXVlK2M-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/documentation/50_cb_thunderlink/jj-ying-PDxYfXVlK2M-unsplash.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | See [my website](https://camiel.bouchier.be/en/cb_thunderlink) 3 | where I am going to maintain some documentation. 4 | 5 | 8 | -------------------------------------------------------------------------------- /cb_thunderlink.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cb_thunderlink", 3 | "description": "cb_thunderlink is a thunderlink replacement.", 4 | "path": "cb_thunderlink.exe", 5 | "type": "stdio", 6 | "allowed_extensions": [ "cb_thunderlink@bouchier.be" ] 7 | } 8 | -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/5_architecture/20201127_cb_thunderlink_architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CamielBouchier/cb_thunderlink/HEAD/documentation/50_cb_thunderlink/5_architecture/20201127_cb_thunderlink_architecture.pptx -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | references/* 2 | tiddly_wiki* 3 | build*/* 4 | dist*/* 5 | logs/* 6 | Lib/* 7 | Scripts/* 8 | __pycache__/* 9 | *.xpi 10 | .*.swp 11 | *.zip 12 | *.*~ 13 | pyvenv.cfg 14 | cb_thunderlink.html 15 | mail_extension/dot_vim/*.* 16 | -------------------------------------------------------------------------------- /cb_build.sh: -------------------------------------------------------------------------------- 1 | # 2 | # $BeginLicense$ 3 | # 4 | # (C) 2020 by Camiel Bouchier (camiel@bouchier.be) 5 | # 6 | # This file is part of cb_thunderlink. 7 | # All rights reserved. 8 | # 9 | # $EndLicense$ 10 | # 11 | # ***** 12 | # 13 | # This bat file builds the cb_thunderlink distribution. 14 | # 15 | 16 | rm -rf build_linux 17 | rm -rf dist_linux 18 | mkdir build_linux 19 | mkdir dist_linux 20 | cd mail_extension 21 | 7z a cb_thunderlink.xpi @files_in_xpi.lst 22 | cd .. 23 | pyinstaller --noconfirm --workpath build_linux --distpath dist_linux cb_thunderlink.spec 24 | cd dist_linux 25 | 7z a cb_thunderlink_linux.zip cb_thunderlink 26 | cd .. 27 | 28 | # 29 | # vim: ts=4 sw=4 sts=4 sr et columns=100 30 | # 31 | -------------------------------------------------------------------------------- /mail_extension/cb_options.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | $BeginLicense$ 4 | 5 | (C) 2020-2021 by Camiel Bouchier (camiel@bouchier.be) 6 | 7 | This file is part of cb_thunderlink. 8 | 9 | License: Mozilla Public License Version 2.0 10 | (https://github.com/CamielBouchier/cb_thunderlink/blob/main/LICENSE) 11 | 12 | $EndLicense$ 13 | 14 | */ 15 | 16 | body { 17 | /* 18 | width: 100%; 19 | font-family: "Open Sans Light", sans-serif; 20 | */ 21 | } 22 | 23 | #avoid_folders, 24 | #prefer_folders { 25 | width : 95%; 26 | } 27 | 28 | .name { 29 | width: 10em; 30 | margin: 0.5em; 31 | height: 1.8em; 32 | } 33 | 34 | .value { 35 | width: 20em; 36 | margin: 0.5em; 37 | height: 1.8em; 38 | } 39 | 40 | /* 41 | vim: syntax=css ts=4 sw=4 sts=4 sr et columns=80 42 | */ 43 | -------------------------------------------------------------------------------- /cb_build.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM 4 | REM $BeginLicense$ 5 | REM 6 | REM (C) 2020-2021 by Camiel Bouchier (camiel@bouchier.be) 7 | REM 8 | REM This file is part of cb_thunderlink. 9 | REM 10 | REM License: Mozilla Public License Version 2.0 11 | REM (https://github.com/CamielBouchier/cb_thunderlink/blob/main/LICENSE) 12 | REM 13 | REM $EndLicense$ 14 | REM 15 | REM ***** 16 | REM 17 | REM This bat file builds the cb_thunderlink distribution. 18 | REM 19 | 20 | cd mail_extension 21 | "c:\Program Files\7-Zip\7z.exe" a cb_thunderlink.xpi @files_in_xpi.lst 22 | cd .. 23 | pyinstaller --noconfirm --workpath build_windows --distpath dist_windows cb_thunderlink.spec 24 | cd dist_windows 25 | "c:\Program Files\7-Zip\7z.exe" a cb_thunderlink_windows.zip cb_thunderlink 26 | cd .. 27 | 28 | REM 29 | REM vim: ts=4 sw=4 sts=4 sr et columns=100 30 | REM 31 | -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/2_installation/en.md: -------------------------------------------------------------------------------- 1 | type : "page" 2 | title : "cb_thunderlink installation" 3 | menutitle : "Installation" 4 | slug : "cb_thunderlink/installation" 5 | date : "2020-11-26 00:00:00" 6 | 7 | visible : true 8 | comment : true 9 | 10 | grep : true 11 | align : "Justify" 12 | 13 | === 14 | 15 | Head to the [latest release](https://github.com/CamielBouchier/cb_thunderlink/releases) and download the `cb_thunderlink.xpi` from the assets. You can install the `cb_thunderlink.xpi` in Thunderbird using the `install add-on from file` feature in Thunderbird. 16 | 17 | When not using the OS-integration and the `clickable` feature, that is all you need. 18 | You can start using `(cb)thunderlink`'s with the approach of copying the links and pasting it using the cb_thunderlink button. 19 | 20 | If you need the full power of OS integration and the possibility to click links, read further. 21 | The instructions will be different per OS and currently only Windows is supported. 22 | 23 | * [Windows]({{slug}}/windows) 24 | * [Linux]({{slug}}/linux) 25 | 26 | 29 | -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/2_installation/1_windows/en.md: -------------------------------------------------------------------------------- 1 | type : "page" 2 | title : "cb_thunderlink installation under Windows" 3 | menutitle : "Windows" 4 | slug : "cb_thunderlink/installation/windows" 5 | date : "2020-11-26 00:00:00" 6 | 7 | visible : true 8 | comment : true 9 | 10 | grep : true 11 | align : "Justify" 12 | 13 | === 14 | 15 | Head again to the [latest release](https://github.com/CamielBouchier/cb_thunderlink/releases) and download the `cb_thunderlink_windows.zip` from the assets. 16 | 17 | Unzip the `cb_thunderlink_windows.zip` e.g. to `C:\FooBar\cb_thunderlink`. 18 | This location must be a fixed location (no network location!) and permanently available. 19 | 20 | Open a `Command prompt` **as administrator** and issue : 21 | 22 | ``` 23 | > cd C:\FooBar\cb_thunderlink 24 | > cb_thunderlink.exe register 25 | ``` 26 | 27 | It registers the executable to the add-on and it registers the protocols `thunderlink://` and `cbthunderlink://` to the operating system. 28 | 29 | Remove and reinstall the `cb_thunderlink.xpi` into Thunderbird and that's it. 30 | From now on you should have again clickable `(cb)thunderlink`'s! 31 | 32 | 35 | -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/2_installation/2_linux/en.md: -------------------------------------------------------------------------------- 1 | type : "page" 2 | title : "cb_thunderlink installation under Linux" 3 | menutitle : "Linux" 4 | slug : "cb_thunderlink/installation/linux" 5 | date : "2020-11-26 00:00:00" 6 | 7 | visible : true 8 | comment : true 9 | 10 | grep : true 11 | align : "Justify" 12 | 13 | === 14 | 15 | (Currently only tested for XUbuntu 18.04, all feedback on other systems welcome!) 16 | 17 | Head again to the [latest release](https://github.com/CamielBouchier/cb_thunderlink/releases) and download the `cb_thunderlink_linux.zip` from the assets. 18 | 19 | Unzip the `cb_thunderlink_linux.zip` e.g. to `~/cb_thunderlink`. 20 | This location must be a fixed location (no network location!) and permanently available. 21 | 22 | Open a `shell` and issue : 23 | 24 | ``` 25 | > cd ~/cb_thunderlink 26 | > ./cb_thunderlink register 27 | ``` 28 | 29 | It registers the executable to the add-on and it registers the protocols `thunderlink://` and `cbthunderlink://` to the operating system. 30 | 31 | Remove and reinstall the `cb_thunderlink.xpi` into Thunderbird and that's it. 32 | From now on you should have again clickable `(cb)thunderlink`'s! 33 | 34 | 37 | -------------------------------------------------------------------------------- /mail_extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "version": "1.9.1", 4 | "name": "cb_thunderlink", 5 | "description": "cb_thunderlink is a thunderlink replacement.", 6 | "author": "camiel@bouchier.be", 7 | "applications": { 8 | "gecko": { 9 | "id": "cb_thunderlink@bouchier.be", 10 | "strict_min_version": "128.0" 11 | } 12 | }, 13 | "browser_action": { 14 | "default_icon": "images/cb-32px.png" 15 | }, 16 | "commands": { 17 | "drop_link": { 18 | "suggested_key": { 19 | "default": "Ctrl+Alt+0" 20 | }, 21 | "description": "Paste configurable link from the clipboard" 22 | }, 23 | "copy_link_1": { 24 | "suggested_key": { 25 | "default": "Ctrl+Alt+1" 26 | }, 27 | "description": "Copy configurable link 1 to the clipboard" 28 | }, 29 | "copy_link_2": { 30 | "suggested_key": { 31 | "default": "Ctrl+Alt+2" 32 | }, 33 | "description": "Copy configurable link 2 to the clipboard" 34 | }, 35 | "copy_link_3": { 36 | "suggested_key": { 37 | "default": "Ctrl+Alt+3" 38 | }, 39 | "description": "Copy configurable link 3 to the clipboard" 40 | }, 41 | "copy_link_4": { 42 | "suggested_key": { 43 | "default": "Ctrl+Alt+4" 44 | }, 45 | "description": "Copy configurable link 4 to the clipboard" 46 | }, 47 | "copy_link_5": { 48 | "suggested_key": { 49 | "default": "Ctrl+Alt+5" 50 | }, 51 | "description": "Copy configurable link 5 to the clipboard" 52 | } 53 | }, 54 | "permissions": [ 55 | "accountsRead", 56 | "clipboardRead", 57 | "clipboardWrite", 58 | "menus", 59 | "messagesRead", 60 | "nativeMessaging", 61 | "storage" 62 | ], 63 | "icons": { 64 | "64": "images/cb-64px.png", 65 | "32": "images/cb-32px.png", 66 | "16": "images/cb-16px.png" 67 | }, 68 | "background": { 69 | "scripts": [ 70 | "cb_background.js" 71 | ], 72 | "type": "module" 73 | }, 74 | "options_ui": { 75 | "page": "cb_options.html" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cb_thunderlink.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | # vim: syntax=python ts=4 sw=4 sts=4 sr et columns=100 lines=45 3 | 4 | # 5 | # $BeginLicense$ 6 | # 7 | # (C) 2020-2021 by Camiel Bouchier (camiel@bouchier.be) 8 | # 9 | # This file is part of cb_thunderlink. 10 | # 11 | # License: Mozilla Public License Version 2.0 12 | # (https://github.com/CamielBouchier/cb_thunderlink/blob/main/LICENSE) 13 | # 14 | # $EndLicense$ 15 | # 16 | 17 | # 18 | # u option for unbuffered stdio. Needed (cf. python -u in the *.bat file approach) 19 | # 20 | 21 | options = [('u', None, 'OPTION')] 22 | 23 | ##### 24 | 25 | block_cipher = None 26 | 27 | ##### 28 | 29 | a = Analysis( 30 | ['cb_thunderlink.py'], 31 | binaries=[], 32 | datas=[ 33 | ('cb_thunderlink.json', '.'), 34 | ('mail_extension/cb_thunderlink.xpi', '.'), 35 | # Those solve at once our source distribution obligations. 36 | ('cb_thunderlink.*', 'src'), 37 | ('mail_extension/*.*', 'src/mail_extension'), 38 | ('mail_extension/images/*.*', 'src/mail_extension/images'), 39 | ], 40 | hiddenimports=[], 41 | hookspath=[], 42 | runtime_hooks=[], 43 | excludes=[], 44 | win_no_prefer_redirects=False, 45 | win_private_assemblies=False, 46 | cipher=block_cipher, 47 | noarchive=False 48 | ) 49 | 50 | ##### 51 | 52 | pyz = PYZ( 53 | a.pure, 54 | a.zipped_data, 55 | cipher=block_cipher 56 | ) 57 | 58 | ##### 59 | 60 | exe = EXE( 61 | pyz, 62 | a.scripts, 63 | options, 64 | [], 65 | exclude_binaries=True, 66 | name='cb_thunderlink', 67 | debug=False, 68 | bootloader_ignore_signals=False, 69 | strip=False, 70 | upx=True, 71 | console=True 72 | ) 73 | 74 | ##### 75 | 76 | coll = COLLECT( 77 | exe, 78 | a.binaries, 79 | a.zipfiles, 80 | a.datas, 81 | strip=False, 82 | upx=True, 83 | upx_exclude=[], 84 | name='cb_thunderlink' 85 | ) 86 | 87 | ##### 88 | -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | Release_1_9_1 2 | ============= 3 | 4 | * Synced with https://github.com/pauleve/cb_thunderlink/releases/tag/v1.9.0 5 | * Thereby removing experiments etc. 6 | * Some functionality *might* be lost or go slower. Feedback over time will learn. 7 | * At least works with my TB 128 ESR and TB 140. Chances all between OK as well. 8 | 9 | Release_1_7_4 10 | ============= 11 | 12 | * Support for ESR128 13 | 14 | Release_1_7_3 15 | ============= 16 | 17 | * Implemented https://github.com/CamielBouchier/cb_thunderlink/issues/62 18 | 19 | Release_1_7_2 20 | ============= 21 | 22 | * Support for ESR115 23 | * Implemented https://github.com/CamielBouchier/cb_thunderlink/issues/59 24 | * Implemented https://github.com/CamielBouchier/cb_thunderlink/issues/58 25 | 26 | Release_1_7_1 27 | ============= 28 | 29 | * Implemented https://github.com/CamielBouchier/cb_thunderlink/pull/57 30 | 31 | Release_1_7_0 32 | ============= 33 | 34 | * Implemented https://github.com/CamielBouchier/cb_thunderlink/pull/56 35 | 36 | Release_1_6_0 37 | ============= 38 | 39 | * Implemented https://github.com/CamielBouchier/cb_thunderlink/issues/47 40 | 41 | Release_1_5_0 42 | ============= 43 | 44 | * Implemented https://github.com/CamielBouchier/cb_thunderlink/issues/32 45 | 46 | Release_1_4_0 47 | ============= 48 | 49 | * Implemented https://github.com/CamielBouchier/cb_thunderlink/issues/43 50 | 51 | Release_1_3_0 52 | ============= 53 | 54 | * Merged https://github.com/CamielBouchier/cb_thunderlink/pull/38 55 | 56 | Release_1_2_0 57 | ============= 58 | 59 | * Added drop_link command shortcut. 60 | 61 | Release_1_1_0 62 | ============= 63 | 64 | * Introduced Mozilla Public License Version 2.0 65 | 66 | Release_1_0_0 67 | ============= 68 | 69 | * Messed up release numbering and version numbering in the past. Going to 1.0.0 after having introduced the suggestions from John Bieling, bringing it in the official thunderbird add-ons. 70 | 71 | Release_0_8_0 72 | ============= 73 | 74 | * Issue [#16]((https://github.com/CamielBouchier/cb_thunderlink/issues/16) adressed : quotes in Windows registry. 75 | 76 | * hashbang added for linux. See also [#18](https://github.com/CamielBouchier/cb_thunderlink/issues/18). 77 | 78 | Release_0_7_0 79 | ============= 80 | 81 | * Addressed issue [#10](https://github.com/CamielBouchier/cb_thunderlink/issues/10) 82 | 83 | * On Linux : Created directory for manifest.json if it does not exist 84 | 85 | As from Release_0_6_0 (not included) on, I will keep here a changelog. 86 | -------------------------------------------------------------------------------- /mail_extension/cb_api_schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "cb_api", 4 | "functions": 5 | [ 6 | { 7 | "name": "cb_show_message_from_api_id", 8 | "type": "function", 9 | "description": "Shows a message defined by API id.", 10 | "async": true, 11 | "parameters": 12 | [ 13 | { 14 | "name": "tb_version", 15 | "type": "integer", 16 | "description": "The tb_version." 17 | }, 18 | { 19 | "name": "api_id", 20 | "type": "integer", 21 | "description": "The api_id of the message to show." 22 | }, 23 | { 24 | "name": "open_mode", 25 | "type": "string", 26 | "description": "new_tab | new_window | three_pane" 27 | } 28 | ] 29 | }, 30 | { 31 | "name": "cb_show_message_from_msg_id", 32 | "type": "function", 33 | "description": "Shows a message defined by msg-id.", 34 | "async": true, 35 | "parameters": 36 | [ 37 | { 38 | "name": "tb_version", 39 | "type": "integer", 40 | "description": "The tb_version." 41 | }, 42 | { 43 | "name": "msg_id", 44 | "type": "string", 45 | "description": "The msg_id of the message to show." 46 | }, 47 | { 48 | "name": "open_mode", 49 | "type": "string", 50 | "description": "new_tab | new_window | three_pane" 51 | }, 52 | { 53 | "name": "prefer_folders", 54 | "type": "array", 55 | "items": { 56 | "type" : "string" 57 | }, 58 | "description": "array of preferred folders" 59 | }, 60 | { 61 | "name": "avoid_folders", 62 | "type": "array", 63 | "items": { 64 | "type" : "string" 65 | }, 66 | "description": "array of avoided folders" 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /mail_extension/cb_options.js: -------------------------------------------------------------------------------- 1 | // 2 | // $BeginLicense$ 3 | // 4 | // (C) 2020-2021 by Camiel Bouchier (camiel@bouchier.be) 5 | // 6 | // This file is part of cb_thunderlink. 7 | // 8 | // License: Mozilla Public License Version 2.0 9 | // (https://github.com/CamielBouchier/cb_thunderlink/blob/main/LICENSE) 10 | // 11 | // $EndLicense$ 12 | // 13 | 14 | const inputs = document.getElementsByTagName('input') 15 | 16 | // 17 | // GUI => local storage 18 | // 19 | 20 | function store_settings() { 21 | let conf_links = {} 22 | for (input of inputs) { 23 | if (!input.id.startsWith('s_')) continue 24 | let idx = input.id.replace(/^s_[^\d]*/, '') 25 | if (!conf_links[idx]) { 26 | conf_links[idx] = {} 27 | } 28 | if (input.id.startsWith('s_enable')) { 29 | conf_links[idx].enable = input.checked 30 | } 31 | if (input.id.startsWith('s_name')) { 32 | conf_links[idx].name = input.value 33 | } 34 | if (input.id.startsWith('s_value')) { 35 | conf_links[idx].value = input.value 36 | } 37 | } 38 | avoid_folders = document.getElementById('avoid_folders').value.split(',') 39 | for (let i=0; i GUI (at opening settings) 66 | // 67 | 68 | browser.storage.local.get('cb_thunderlink').then((settings) => { 69 | if (settings.cb_thunderlink) { 70 | let open_mode = settings.cb_thunderlink.open_mode 71 | let now_strftime_format = settings.cb_thunderlink.now_strftime_format 72 | let avoid_folders = settings.cb_thunderlink.avoid_folders 73 | let prefer_folders = settings.cb_thunderlink.prefer_folders 74 | let conf_links = settings.cb_thunderlink.conf_links 75 | document.getElementById(open_mode).checked = true 76 | document.getElementById('now_strftime_format').value = now_strftime_format 77 | document.getElementById('avoid_folders').value = avoid_folders.join(', ') 78 | document.getElementById('prefer_folders').value = prefer_folders.join(', ') 79 | for (const key in conf_links) { 80 | let conf_link = conf_links[key] 81 | document.getElementById('s_enable_' + key).checked = conf_link.enable 82 | document.getElementById('s_name_' + key).value = conf_link.name 83 | document.getElementById('s_value_' + key).value = conf_link.value 84 | } 85 | } 86 | }) 87 | 88 | // vim: syntax=javascript ts=4 sw=4 sts=4 sr et columns=120 89 | -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/en.md: -------------------------------------------------------------------------------- 1 | type : "page" 2 | title : "cb_thunderlink" 3 | slug : "cb_thunderlink" 4 | date : "2020-10-10 21:00:00" 5 | 6 | visible : true 7 | comment : true 8 | opencomment : true 9 | 10 | grep : true 11 | thumbnail : "thumbnail_1200x800.jpg" 12 | thumbnailtitle : "Photo by JJ Ying (https://unsplash.com/@jjying)" 13 | align : "Justify" 14 | 15 | === 16 | 17 | #### Intro - Thanks 18 | 19 | I am a long time user of **thunderlink** as can be found on 20 | [addons.thunderbird.net](https://addons.thunderbird.net/nl/thunderbird/addon/thunderlink/) or [github](https://github.com/mikehardy/thunderlink). 21 | I can't imagine my workflow and electronic archive without! 22 | 23 | It's functionality is as simple as useful : click (or paste) a link from your personal wiki into thunderbird and it opens the associated message. 24 | 25 | However Thunderbird changed quite drastically the underlying APIs and thefore thunderlink stopped working on recent versions. 26 | See e.g. [this announcement.](https://www.thunderbird.net/en-US/thunderbird/78.0/releasenotes/) 27 | A complete re-think and re-write was needed and the original author did not have the time. So I took the task doing so. 28 | 29 | Big thanks to [Mike Hardy](https://github.com/mikehardy) and team for having thunderlink supported all this time! 30 | 31 | #### Important remarks on cb_thunderlink vs thunderlink 32 | 33 | ##### `thunderlink://` versus `cbthunderlink://` 34 | 35 | The original thunderlink identified messages in an unique way by using message-id of the email. Links would read like `thunderlink://messageid=somestuff@foobar`. cb_thunderlink still fully supports this. 36 | 37 | However, there is a remote risk that searching on the message-id gets lost in the further future reduction of Thunderbird API. Therefore a second mechanism was introduced named `cbthunderlink`. This identifies messages in an unique way using the message-date and message-author. Links would read like `cbthunderlink://SomeBase64String`. It might be slightly more future-proof to start using those. 38 | 39 | Features work for both and where needed I will distinguish them as `cbthunderlink`, `thunderlink` or `(cb)thunderlink` when referring to both. 40 | 41 | ##### Clickable - OS-integration 42 | 43 | Thunderlinks clickable-feature depended on being able calling the `thunderbird -thunderlink xyz` command. However Thunderbird has also dropped support therefore... 44 | 45 | To keep that feature available, cb_thunderlink comes with an accompanying program, started automatically by the add-on, to emulate that feature and make links clickable again. Note that currently this is only available yet for Windows and for some Linuxes. 46 | 47 | However, cb_thunderlink can be used perfectly well without that accompanying program. Links need to be cut then from your source and pasted (using the cb_thunderlink button) into Thunderbird. 48 | 49 | 50 | #### Description - usage 51 | 52 | `(cb)thunderlink`'s are durable hyperlinks to specific email messages. 53 | 54 | You can use them anywhere you want immediate access to the original message contents in full. 55 | For example, wikis, task trackers, etc. 56 | 57 | Click on `(cb)thunderlink` later to open that specific message in Thunderbird. 58 | (when not installing the accompanying program, copy the link and paste it using cb_thunderlink button) 59 | 60 | You may customize `(cb)thunderlink` formats to fit your needs. 61 | 62 | `(cb)thunderlink`'s are durable even if you file the message. This enables the Thunderbird email client to quickly and reliably find and select any email that exists in your Thunderbird mail store. 63 | 64 | 67 | -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/3_malfunction/en.md: -------------------------------------------------------------------------------- 1 | type : "page" 2 | title : "cb_thunderlink malfunction checklist" 3 | menutitle : "Checklist" 4 | slug : "cb_thunderlink/checklist" 5 | date : "2020-11-27 00:00:00" 6 | 7 | visible : true 8 | comment : true 9 | 10 | grep : true 11 | align : "Justify" 12 | 13 | === 14 | 15 | If something does not work as anticipated, feel free to submit an [issue on github.](https://github.com/CamielBouchier/cb_thunderlink/issues) 16 | However, before doing so, I would strongly suggest to run through the checklist hereafter. It probably will address your issue and if not it 17 | will provide me the info I need anyways to help you. 18 | 19 | 20 | ### Basic functionality 21 | 22 | #### Did you install correctly? 23 | 24 | See [installation instructions.](installation) 25 | 26 | #### Does the basic functionality work? 27 | 28 | * Select a message in the message-list, create a `thunderlink` using the context-menu. 29 | * Select a different message and click the `cb_thunderlink` button. Does it revert to the message you created a link for? 30 | * Repeat a few times for different messages. 31 | * Do now the same cycle but create a `cbthunderlink` using the context-menu. 32 | * Repeat a few times for different messages. 33 | 34 | If all this works reliably, the basic functionality is OK and you are facing an [OS-integration](#os_integration) issue. 35 | Otherwise continue : 36 | 37 | #### Is global indexing enabled? 38 | 39 | * Can you actually find the message through the 'search messages' option of thunderbird itself? Try locating it with author and date. 40 | * One of the options of thunderbird is 'global indexing'. Go to the thunderbird generic options (location might be different with different versions) 41 | and make sure the 'global indexing' checkbox is checked. If not, searching probably won't work at all. 42 | * Restart thunderbird and try again. 43 | 44 | #### Rebuild `global-messages-db.sqlite` 45 | 46 | If above did not work : 47 | * Close thunderbird, go to the profile directory and remove `global-messages-db.sqlite` 48 | * Reopen thunderbird and wait until `global-messages-db.sqlite` is rebuild (i.e. you do not see changing it anymore). Depending on your setup that can take quite some time. 49 | * Try again. 50 | 51 | #### Check error console 52 | 53 | If at this point it still not works, I will need to have some logging results. 54 | 55 | * Open the error-console `Ctrl+Shift+J`. 56 | * Do a cycle of generating a link and pasting it using the button as described earlier. 57 | * Do you see any relevant output on the error console? Can you share? 58 | 59 | Remark w.r.t. privacy: the `cbthunderlink://SomeHexString` is actually a 'base64-encoding' of the emails author and time of sending! 60 | 61 | 62 | 63 | ### OS-integration 64 | 65 | #### Are you on Linux and is thunderbird 'snap' based installed? 66 | 67 | If so, the OS-integration won't work! 68 | 69 | [Native messaging](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging) is known not to work on 'snap' based thunderbirds. 70 | Until somebody builds a proof of concept based on [this link about Native messaging](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging) there is nothing I can do. 71 | 72 | Note however, it will and should work on a package (rpm, deb, ...) based installation of thunderbird. 73 | 74 | 75 | #### Does the extension receive the link? 76 | 77 | * Open the error-console `Ctrl+Shift+J`. 78 | * Paste the failing link (`(cb)thunderlink://...`) manually using the `cb_thunderlink`-button. 79 | * You should see in the error-console a message along the lines of `handle_incoming_link (cb)thunderlink://...`. 80 | * Now try to do the same by clicking the link. 81 | * If the OS-integration were working, you would see the same message now. But probably you don't? 82 | 83 | If you would see that same message now nevertheless reach out to me. It is totally unexpected that the basic functionality works and this one not. 84 | 85 | Keep the error-console open during the remainder of your investigation. 86 | 87 | #### Run `cb_thunderlink (cb)thunderlink://...` manually 88 | 89 | * Head to the directory where you installed cb_thunderlink. E.g. `C:\FooBar\cb_thunderlink` under Windows or `~/cb_thunderlink` under Linux. 90 | * You will find there find a `logs` directory containing `cb_thunderlink.log`. You might want to inspect this for error messages or other anomalies. 91 | * Run from a terminal at the installation directory: `cb_thunderlink (cb)thunderlink://your_link_with_issue`. 92 | * Does that work? Do you see an incoming message notification on the thunderbird error-console? Do you see anything in the `cb_thunderlink.log`? 93 | 94 | If this does not work, please contact me with the information you obtained. It is unclear what is not working ... 95 | 96 | If this does work, yet the clicking of a link does not work, something went wrong in associating in your OS a link with an executable. 97 | You are probably on Linux and we might want to review the specifics. 98 | 99 | 100 | 103 | -------------------------------------------------------------------------------- /documentation/50_cb_thunderlink/5_architecture/en.md: -------------------------------------------------------------------------------- 1 | type : "page" 2 | title : "cb_thunderlink architecture" 3 | menutitle : "Architecture" 4 | slug : "cb_thunderlink/architecture" 5 | date : "2020-11-27 00:00:00" 6 | 7 | 8 | visible : true 9 | comment : true 10 | opencomment : true 11 | 12 | grep : true 13 | align : "Justify" 14 | 15 | === 16 | 17 | #### Files in distribution 18 | 19 | 20 | ``` 21 | | api-ms-win-core-console-l1-1-0.dll | 22 | | : | 23 | | libssl-1_1-x64.dll | 24 | | python37.dll | 25 | | ucrtbase.dll | 26 | | VCRUNTIME140.dll | 27 | | : | 28 | | pyexpat.pyd | 29 | | select.pyd | 30 | | unicodedata.pyd | 31 | | _bz2.pyd | => (1) 32 | | _ctypes.pyd | 33 | | _hashlib.pyd | 34 | | _lzma.pyd | 35 | | _socket.pyd | 36 | | _ssl.pyd | 37 | | | 38 | | base_library.zip | 39 | | 40 | | cb_thunderlink.exe | => (2) 41 | | cb_thunderlink.exe.manifest | 42 | | 43 | | cb_thunderlink.json | => (3) 44 | | 45 | | cb_thunderlink.xpi | => (4) 46 | | 47 | +---logs 48 | | cb_thunderlink.log 49 | | 50 | \---src | => (5) 51 | | cb_thunderlink.json 52 | | cb_thunderlink.py | => (6) 53 | | cb_thunderlink.spec | => (7) 54 | | 55 | \---mail_extension | => (8) 56 | | cb_api.js | 57 | | cb_api_schema.json | 58 | | cb_background.js | 59 | | cb_options.css | 60 | | cb_options.html | 61 | | cb_options.js | 62 | | files_in_xpi.lst | 63 | | manifest.json | 64 | | 65 | | cb_thunderlink.xpi | => (9) 66 | | 67 | \---images 68 | cb-128px.png 69 | cb-16px.png 70 | cb-256px.png 71 | cb-32px.png 72 | cb-48px.png 73 | cb-64px.png 74 | ``` 75 | 76 | 1. All files that implement and support the Python interpreter that 'pyinstaller' packaged into `cb_thunderlink.exe`. 77 | 1. The Python script `cb_thunderlink.py` (see 'src') packaged along with the appropriate Python interpreter. 78 | 1. Used under Linux for describing the 'native messaging' interface. 79 | 1. `cb_thunderlink.xpi` is the Thunderbird add-on. 80 | 1. The 'src' is unused but delivered as documentation. 81 | 1. `cb_thunderlink.py` is the main and only script for os-integration. 82 | 1. Configuration script for 'pyinstaller'. 83 | 1. The Thunderbird add-on sources. 84 | 1. `cb_thunderlink.xpi` is the Thunderbird add-on, basically a zip of the sources above. 85 | 86 | #### Drawing 87 | 88 |

89 | arch 90 |

91 | 92 | #### Explanation 93 | 94 | ##### Main function 95 | 96 | The main functionality is obtained by loading `cb_thunderlink.xpi` as a thunderbird add-on. It runs in the OS 'Process 1'. 97 | 98 | Through the context-menu on the message-list, or a shortcut, a `(cb)thunderlink` is generated and stored in the OS clipboard. 99 | 100 | The `cb_thunderlink`-button takes the link that is in the OS clipboard and lets thunderbird navigate to the linked message. 101 | 102 | Actually, one can run run perfectly cb_thunderlink this way : copying around `(cb)thunderlink`'s and paste it using the `cb_thunderlink`-button 103 | 104 | #### OS-integration 105 | 106 | The OS-integration is about the capability to click a `(cb)thunderlink` in a web-page, a wiki etc. and having thunderbird jumping to that message. 107 | 108 | Earlier implementations could rely on the capability of thunderbird to be called as `thunderbird -thunderlink link`. 109 | Recent versions of thunderbird do not support anymore. 110 | Moreover the capability of thunderbird to communicate with the external world is very restricted. 111 | Hence a quite convoluted alternative approach was needed. 112 | 113 | The first step is that `cb_thunderlink.xpi` ('Process 1') tries to start `cb_thunderlink.py` ('Process 2') through the only mechanism that 114 | thunderbird allows, i.e. [native messaging.](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging) 115 | This process does nothing else than listen on port 1302 for incoming commands and communicate it through to 'Process 1' where it will 116 | be handled similar as pasting a `(cb)thunderlink`. 117 | 118 | The other step is `cb_thunderlink.py (cb)thunderlink://...` is started as 'Process 3' passing the argument over port 1302 to 'Process 2'. 119 | This allows to associate in the OS a link with an executable (equivalent to the earlier `thunderbird -thunderlink link`) that jumps to a message. 120 | 121 | ##### Register 122 | 123 | As well the [native messaging](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging), as the linking 124 | of `(cb)thunderlink://...` with 'Process 3' require OS specific setups. The `cb_thunderlink.py register` handles this part of the story. 125 | 126 | In Windows all connections are fixed in the registry. 127 | Under Linux it is a combination of setting up a json file on a specific location and setting up gio. 128 | 129 | For more details, look into the source code of `cb_thunderlink.py` under the 'register' branch. 130 | 131 | 134 | -------------------------------------------------------------------------------- /cb_put_licenses_on_files.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: syntax=python ts=4 sw=4 sts=4 sr et columns=100 lines=45 3 | 4 | 5 | """ 6 | 7 | $BeginLicense$ 8 | 9 | (C) 2020-2021 by Camiel Bouchier (camiel@bouchier.be) 10 | 11 | This file is part of cb_thunderlink. 12 | 13 | License: Mozilla Public License Version 2.0 14 | (https://github.com/CamielBouchier/cb_thunderlink/blob/main/LICENSE) 15 | 16 | $EndLicense$ 17 | 18 | """ 19 | 20 | #################################################################################################### 21 | 22 | # Want to be asap sure we are running Python 3.6 23 | import sys 24 | assert sys.version_info >= (3,6) 25 | 26 | import logging 27 | logger = logging.getLogger(__name__) 28 | 29 | import datetime 30 | import os 31 | import re 32 | 33 | #################################################################################################### 34 | 35 | program_name = "cbPutLicenseOnFiles" 36 | log_dir = "logs" 37 | 38 | license_txt = [ 39 | '', 40 | '(C) 2020-2021 by Camiel Bouchier (camiel@bouchier.be)', 41 | '', 42 | 'This file is part of cb_thunderlink.', 43 | '', 44 | 'License: Mozilla Public License Version 2.0', 45 | '(https://github.com/CamielBouchier/cb_thunderlink/blob/main/LICENSE)', 46 | '' 47 | ] 48 | 49 | #################################################################################################### 50 | 51 | def generate_files(root, extensions_to_include=[], regexes_to_exclude=[]) : 52 | 53 | for path, dirs, files in os.walk(root) : 54 | for the_file in files : 55 | 56 | relative_name = os.path.join(path, the_file).replace('\\', '/') 57 | 58 | if extensions_to_include : 59 | (filename, extension) = os.path.splitext(the_file) 60 | if extension not in extensions_to_include : 61 | continue 62 | 63 | # logger.info(f"including \'{relative_name}\'") 64 | 65 | excluded_due_to_regex = False 66 | for regex in regexes_to_exclude : 67 | if re.match(regex, relative_name) : 68 | # logger.info(f"excluding \'{relative_name}\' due to \'{regex}\'") 69 | excluded_due_to_regex = True 70 | break 71 | if excluded_due_to_regex : 72 | continue 73 | 74 | yield os.path.abspath(relative_name).replace('\\', '/') 75 | 76 | #################################################################################################### 77 | 78 | def handle_file(filename) : 79 | 80 | logger.info(f"Handling file {filename}") 81 | 82 | with open(filename, encoding="utf-8") as f : 83 | file_lines = f.readlines() 84 | 85 | has_begin_license = False 86 | has_end_license = False 87 | 88 | for line in file_lines : 89 | if not has_begin_license and re.match(r'.{0,5}\$BeginLicense\$', line) : 90 | has_begin_license = True 91 | begin_license_line = line 92 | if has_begin_license and re.match(r'.{0,5}\$EndLicense\$', line) : 93 | has_end_license = True 94 | end_license_line = line 95 | break 96 | 97 | if not has_begin_license : 98 | logger.info("No 'BeginLicense' found in '{}'".format(filename)) 99 | return 100 | 101 | if has_begin_license and not has_end_license : 102 | logger.info("No 'EndLicense' found in '{}'".format(filename)) 103 | return 104 | 105 | with open(filename, "w", encoding="utf-8") as f : 106 | skipping = False 107 | for line in file_lines : 108 | if not skipping : 109 | f.write(line.rstrip()+'\n') # Implicit trailing blanks removal. 110 | if line == end_license_line : 111 | f.write(line) 112 | skipping = False 113 | if line == begin_license_line : 114 | skipping = True 115 | for X in license_txt : 116 | f.write(begin_license_line.replace('$BeginLicense$', X).rstrip()+'\n') 117 | 118 | #################################################################################################### 119 | 120 | def install_logger() : 121 | 122 | now_string = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") 123 | log_filename = "{}/{}_{}.log".format(log_dir, program_name, now_string) 124 | 125 | root_logger = logging.getLogger() 126 | root_logger.setLevel(logging.DEBUG) 127 | 128 | file_handler = logging.FileHandler(log_filename, mode='a', encoding="utf8") 129 | file_handler.setLevel(logging.DEBUG) 130 | file_format = "%(asctime)s - %(levelname)10s - %(filename)32s:%(lineno)5s : %(message)s" 131 | file_formatter = logging.Formatter(file_format) 132 | file_handler.setFormatter(file_formatter) 133 | 134 | console_handler = logging.StreamHandler() 135 | console_handler.setLevel(logging.INFO) 136 | console_format = "%(filename)32s:%(lineno)5s : %(message)s" 137 | console_formatter = logging.Formatter(console_format) 138 | console_handler.setFormatter(console_formatter) 139 | 140 | root_logger.addHandler(file_handler) 141 | root_logger.addHandler(console_handler) 142 | 143 | logger.info("Logging in '{}'".format(log_filename)) 144 | 145 | #################################################################################################### 146 | 147 | if __name__ == '__main__': 148 | 149 | try : 150 | os.makedirs(log_dir) 151 | except FileExistsError : 152 | pass 153 | 154 | install_logger() 155 | logger.info("Starting {}".format(program_name)) 156 | 157 | dir_todo = '.' 158 | extensions_todo = ['', '.py', '.js', '.spec', '.txt', '.bat', '.css'] 159 | regex_exclude = [ 160 | r'.*/.hg/.*', 161 | r'.*/.git/.*', 162 | r'.*/__pycache__/.*', 163 | r'.*/build_linux/.*', 164 | r'.*/build_windows/.*', 165 | r'.*/dist_linux/.*', 166 | r'.*/dist_windows/.*', 167 | r'.*/logs/.*', 168 | r'.*/images/.*', 169 | r'.*/references/.*', 170 | ] 171 | 172 | for the_file in generate_files(dir_todo, extensions_todo, regex_exclude) : 173 | handle_file(f"{the_file}") 174 | 175 | logger.info("Ending {}".format(program_name)) 176 | 177 | #################################################################################################### 178 | -------------------------------------------------------------------------------- /mail_extension/cb_options.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |

Open-mode

19 | 20 | 21 | 22 |
23 | 24 | 25 |
26 | 27 | 28 |
29 | 30 |

Folders to avoid

31 | 32 |

If message in multiple folders, following list of comma separated folders 33 | will be preferred.

34 | 35 | 37 | 38 |

If message in multiple folders, following list of comma separated folders 39 | will be avoided.

40 | 41 | 43 | 44 |

Time format for '$now$'

45 | 46 |

See supported specifiers

47 | 48 | 49 | 50 |

Configurable links

51 | 52 |

Here you can configure the links to be generated.

53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | 82 | 100 |

Following substitutions are made and give the flexibility:

101 | 131 |

Typical default entries are:

132 | 136 | 137 |
138 | 139 | 140 | 141 | 142 | 143 | 146 | -------------------------------------------------------------------------------- /mail_extension/cb_api.js: -------------------------------------------------------------------------------- 1 | // 2 | // $BeginLicense$ 3 | // 4 | // (C) 2020-2021 by Camiel Bouchier (camiel@bouchier.be) 5 | // 6 | // This file is part of cb_thunderlink. 7 | // 8 | // License: Mozilla Public License Version 2.0 9 | // (https://github.com/CamielBouchier/cb_thunderlink/blob/main/LICENSE) 10 | // 11 | // $EndLicense$ 12 | // 13 | 14 | console.log("cb_api started") 15 | 16 | var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm") 17 | var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm") 18 | 19 | const Services = globalThis.Services || ChromeUtils.import("resource://gre/modules/Services.jsm").Services 20 | 21 | try { 22 | var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm") 23 | } catch(e) { 24 | try { 25 | var { Gloda } = ChromeUtils.import("resource:///modules/gloda/public.js") 26 | } catch (e) { 27 | console.log(e) 28 | } 29 | } 30 | 31 | // Somewhere around version 115 it seems NOUN_MESSAGE moved into GlodaConstants instead of GlodaPublic 32 | try { 33 | var { GlodaConstants } = ChromeUtils.import("resource:///modules/gloda/GlodaConstants.jsm") 34 | var CB_NOUN_MESSAGE = GlodaConstants.NOUN_MESSAGE 35 | } catch(e) { 36 | var CB_NOUN_MESSAGE = Gloda.NOUN_MESSAGE 37 | } 38 | 39 | // 40 | // Implementation of an experimental API. 41 | // - cb_show_message_from_api_id(api_id, open_mode) will 'goto' the message identified by api_id. 42 | // open_mode : three_pane | new_window | new_tab 43 | // - cb_show_message_from_msg_id(msg_id, open_mode) will 'goto' the message identified by msg_id. 44 | // open_mode : three_pane | new_window | new_tab 45 | // 46 | 47 | var cb_api = class extends ExtensionCommon.ExtensionAPI { 48 | getAPI(context) { 49 | return { 50 | cb_api: { 51 | cb_show_message_from_api_id(tb_version, api_id, open_mode) { 52 | console.log("cb_show_message_from_api_id", tb_version, api_id, open_mode) 53 | // See : https://thunderbird-webextensions.readthedocs.io/en/latest/how-to/experiments.html 54 | // #using-folder-and-message-types 55 | let the_message = context.extension.messageManager.get(api_id) 56 | // Following few lines are +/- from original thunderlink. 57 | let win = Services.wm.getMostRecentWindow("mail:3pane") 58 | if (open_mode == "new_window") { 59 | MailUtils.openMessagesInNewWindows([the_message]) 60 | } else if (open_mode == "new_tab") { 61 | MailUtils.displayMessage(the_message) 62 | } else { 63 | if (open_mode != "three_pane") { 64 | console.error("open_mode should be one of new_window|new_tab|three_pane:", open_mode) 65 | } 66 | if (win) { 67 | let tabmail = win.document.getElementById("tabmail") 68 | tabmail.switchToTab(0) //will always be the mail tab 69 | win.focus() 70 | if (tb_version <= 102) { 71 | win.gFolderTreeView.selectFolder(the_message.folder) 72 | win.gFolderDisplay.selectMessage(the_message) 73 | } else { 74 | win.gTabmail.tabInfo[0].folder = the_message.folder 75 | win.gTabmail.currentAbout3Pane.selectMessage(the_message) 76 | } 77 | } else { 78 | MailUtils.displayMessage(the_message) 79 | } 80 | } 81 | }, 82 | cb_show_message_from_msg_id(tb_version, msg_id, open_mode, prefer_folders, avoid_folders) { 83 | console.log("cb_show_message_from_msg_id", 84 | tb_version, msg_id, open_mode, prefer_folders, avoid_folders) 85 | let query = Gloda.newQuery(CB_NOUN_MESSAGE) 86 | query.headerMessageID(msg_id) 87 | query.getCollection({ 88 | onItemsAdded : function () {}, 89 | onItemsModified : function () {}, 90 | onItemsRemoved : function () {}, 91 | onQueryCompleted: function (collection) { 92 | let the_messages = [] 93 | for (let i=0; i 1 && isLeapYear()) ? 1 : 0), 3), 48 | '%k': nHour, 49 | '%l': (nHour + 11) % 12 + 1, 50 | '%m': zeroPad(nMonth + 1, 2), 51 | '%n': nMonth + 1, 52 | '%M': zeroPad(date.getMinutes(), 2), 53 | '%p': (nHour < 12) ? 'AM' : 'PM', 54 | '%P': (nHour < 12) ? 'am' : 'pm', 55 | '%s': Math.round(date.getTime() / 1000), 56 | '%S': zeroPad(date.getSeconds(), 2), 57 | '%u': nDay || 7, 58 | '%V': (function () { 59 | var target = getThursday(), 60 | n1stThu = target.valueOf(); 61 | target.setMonth(0, 1); 62 | var nJan1 = target.getDay(); 63 | if (nJan1 !== 4) target.setMonth(0, 1 + ((4 - nJan1) + 7) % 7); 64 | return zeroPad(1 + Math.ceil((n1stThu - target) / 604800000), 2); 65 | })(), 66 | '%w': nDay, 67 | '%x': date.toLocaleDateString(), 68 | '%X': date.toLocaleTimeString(), 69 | '%y': (nYear + '').slice(2), 70 | '%Y': nYear, 71 | '%z': date.toTimeString().replace(/.+GMT([+-]\d+).+/, '$1'), 72 | '%Z': date.toTimeString().replace(/.+\((.+?)\)$/, '$1') 73 | }[sMatch] || '') + '') || sMatch; 74 | }); 75 | } 76 | 77 | // 78 | // https://base64.guru/developers/javascript/examples/unicode-strings 79 | // 80 | // ASCII to Unicode (decode Base64 to original data) 81 | // @param {string} b64 82 | // @return {string} 83 | // 84 | 85 | export function atou(b64) { 86 | return decodeURIComponent(escape(atob(b64))); 87 | } 88 | 89 | // 90 | // https://base64.guru/developers/javascript/examples/unicode-strings 91 | // 92 | // Unicode to ASCII (encode data to Base64) 93 | // @param {string} data 94 | // @return {string} 95 | // 96 | 97 | export function utoa(data) { 98 | return btoa(unescape(encodeURIComponent(data))); 99 | } 100 | 101 | 102 | async function getFoldersWithNames(names) { 103 | if (names && names.length > 0) { 104 | return await browser.folders.query({ 105 | name:{regexp:`(${names.map(e => `^${e}$`).join("|")})`} 106 | }); 107 | } 108 | return []; 109 | } 110 | 111 | async function open_message({headerMessageId, messageId, open_mode}) { 112 | // open_mode: 113 | // - new_tab | new_window -> browser.messageDisplay.open() 114 | // - three_pane -> mailTabs.setSelectedMessage() 115 | 116 | switch (open_mode) { 117 | case "three_pane": 118 | if (headerMessageId) { 119 | throw new Error("browser.mailTabs.setSelectedMessages() does not support setting a headerMessageId"); 120 | } 121 | await browser.mailTabs.setSelectedMessages([messageId]); 122 | break; 123 | case "new_tab": 124 | await browser.messageDisplay.open({ 125 | location: "tab", 126 | messageId, 127 | headerMessageId, 128 | }); 129 | break; 130 | case "new_window": 131 | await browser.messageDisplay.open({ 132 | location: "window", 133 | messageId, 134 | headerMessageId 135 | }); 136 | default: 137 | throw new Error(`Unknown open_mode: ${open_mode}`) 138 | } 139 | } 140 | 141 | export async function cb_show_message_from_thunderlink(link, open_mode, settings) { 142 | const headerMessageId = link.replace('messageid=', '') 143 | const prefer_folders = await getFoldersWithNames(settings.prefer_folders); 144 | const avoid_folders = await getFoldersWithNames(settings.avoid_folders); 145 | // If we do not have prefer/avoid folders and open_mode is not three_pane, 146 | // use open_message() with the headerMessageId directly. This opens the last 147 | // known message with that headerMessageId. Throws if messageId is not known. 148 | if ( 149 | prefer_folders.length == 0 && 150 | avoid_folders.length == 0 && 151 | // API does not yet support opening a message via its headerMessageId in the 152 | // three pane. 153 | open_mode != "three_pane" 154 | ) { 155 | try { 156 | return await open_message({headerMessageId, open_mode}); 157 | } catch (ex) { 158 | console.info(`Message with id <${headerMessageId}> not found in cache, performing a full search.`); 159 | } 160 | } 161 | 162 | // To speed up search, we first search the preferred folders, if specified. 163 | let prefer_folder_ids = prefer_folders.map(f => f.id) 164 | if (prefer_folder_ids.length > 0) { 165 | let page = await browser.messages.query({ 166 | headerMessageId, 167 | folderId: prefer_folder_ids, 168 | includeSubFolders: false, 169 | messagesPerPage: 1 170 | }); 171 | if (page.id) { 172 | await browser.messages.abortList(page.id) 173 | } 174 | if (page.messages.length) { 175 | return open_message({messageId: page.messages[0].id, open_mode}); 176 | } 177 | } 178 | 179 | // We either did not have preferred folders, or the message was not found in 180 | // the preferred folders. Search again in all folders except in the folders 181 | // we already searched and also not in the avoided folders. 182 | // We also skip subfolders of avoided folders. 183 | let search_folders = (await browser.folders.query({isVirtual: false, isRoot: false })).filter(f => 184 | !prefer_folder_ids.includes[f.id] && 185 | !avoid_folders.some(f => f.path.startsWith(f.path)) 186 | ); 187 | let search_folder_ids = search_folders.map(f => f.id); 188 | let page = await browser.messages.query({ 189 | headerMessageId, 190 | folderId: search_folder_ids, 191 | includeSubFolders: false, 192 | messagesPerPage: 1 193 | }); 194 | 195 | if (page.id) { 196 | await browser.messages.abortList(page.id) 197 | } 198 | if (page.messages.length) { 199 | return open_message({messageId: page.messages[0].id, open_mode}); 200 | } 201 | 202 | // No message was found. 203 | console.error(`Message with id <${headerMessageId}> not found. Collection:`, search_folders); 204 | } 205 | 206 | export async function cb_show_message_from_cbthunderlink(link, open_mode, settings) { 207 | let decoded_link = atou(link) 208 | let date_auth = decoded_link.split(";") 209 | let the_date = new Date(date_auth[0]) 210 | let the_author = date_auth[1] 211 | let the_query = { 212 | author: the_author, 213 | fromDate: the_date, 214 | toDate: the_date 215 | } 216 | let ml = await messenger.messages.query(the_query) 217 | let the_message = null 218 | for (let idx = 0; idx < ml.messages.length; idx++) { 219 | let folder = ml.messages[idx].folder 220 | if (settings.prefer_folders.includes(folder.name)) { 221 | the_message = ml.messages[idx] 222 | break 223 | } 224 | } 225 | if (!the_message) { 226 | for (let idx = 0; idx < ml.messages.length; idx++) { 227 | let folder = ml.messages[idx].folder 228 | if (!settings.avoid_folders.includes(folder.name)) { 229 | the_message = ml.messages[idx] 230 | break 231 | } 232 | } 233 | } 234 | if (!the_message && ml.messages.length) { 235 | the_message = ml.messages[0] 236 | } 237 | if (!the_message) { 238 | console.error("Investigate me. the_message == null. ml:", ml) 239 | return 240 | } 241 | 242 | return open_message({messageId: the_message.id, open_mode}); 243 | } -------------------------------------------------------------------------------- /cb_thunderlink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 6 | # $BeginLicense$ 7 | # 8 | # (C) 2020-2021 by Camiel Bouchier (camiel@bouchier.be) 9 | # 10 | # This file is part of cb_thunderlink. 11 | # 12 | # License: Mozilla Public License Version 2.0 13 | # (https://github.com/CamielBouchier/cb_thunderlink/blob/main/LICENSE) 14 | # 15 | # $EndLicense$ 16 | # 17 | # 18 | 19 | # 20 | # ATTENTION for 2 important remarks : 21 | # 1. Do NOT use 'print', console loggers or anything else that writes to stdout. 22 | # It will jeopardize the communication to cb_background.js! 23 | # 2. Note that running python with the `-u` flag is required on Windows, 24 | # in order to ensure that stdin and stdout are opened in binary, rather than text, mode. 25 | # 26 | 27 | ##### 28 | 29 | import json 30 | import logging 31 | import os 32 | import socket 33 | import stat 34 | import struct 35 | import subprocess 36 | import sys 37 | import time 38 | import urllib.parse 39 | 40 | if sys.platform == "win32" : 41 | import ctypes 42 | import winreg 43 | 44 | ##### 45 | 46 | program_name = 'cb_thunderlink' 47 | author_mail = 'camiel@bouchier.be' 48 | server_address = ('127.0.0.1', 1302) 49 | protocols = ['cbthunderlink', 'thunderlink'] # Protocol can not have _ ! 50 | 51 | this_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 52 | log_dir = f"{this_dir}/logs" 53 | 54 | if not os.path.exists(log_dir) : 55 | os.makedirs(log_dir) 56 | 57 | logger = logging.getLogger(__name__) 58 | 59 | ##### 60 | 61 | def get_log_filename() : 62 | 63 | log_filename = os.path.join(log_dir, "%s.log" % (program_name)) 64 | return log_filename 65 | 66 | ##### 67 | 68 | def install_logger() : 69 | 70 | root_logger = logging.getLogger() 71 | root_logger.setLevel(logging.DEBUG) 72 | 73 | file_handler = logging.FileHandler(get_log_filename(), mode='a') 74 | file_handler.setLevel(logging.DEBUG) 75 | file_formatter = logging.Formatter( 76 | '%(asctime)s - %(process)5d - %(levelname)s - %(lineno)4d : %(message)s') 77 | file_handler.setFormatter(file_formatter) 78 | 79 | root_logger.addHandler(file_handler) 80 | 81 | ##### 82 | 83 | if __name__ == '__main__' : 84 | 85 | install_logger() 86 | logger.info("="*100) 87 | logger.info(f"Starting {program_name} ({sys.argv})") 88 | 89 | script_function = "background" 90 | if len(sys.argv) > 1 : 91 | if sys.argv[1] == "register" : 92 | script_function = "register" 93 | elif sys.argv[1] == "unregister" : 94 | script_function = "unregister" 95 | else : 96 | for protocol in protocols : 97 | if sys.argv[1].startswith(protocol + '://') : 98 | script_function = "command" 99 | break 100 | 101 | 102 | if script_function in ["register"] : 103 | 104 | # Registering (currently windows specific only) 105 | # (in this branch it is OK to use stdout => interactive) 106 | 107 | if sys.platform == "win32" : 108 | 109 | if not ctypes.windll.shell32.IsUserAnAdmin() : 110 | print("This must be run as administrator. You are just a mortal.") 111 | sys.exit() 112 | 113 | # We are setting up ourselves in the registry. 114 | print("Setting up the registry") 115 | 116 | # This are the keys for the native messaging registration. 117 | key = r'SOFTWARE\Mozilla\NativeMessagingHosts\cb_thunderlink' 118 | for K in [winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE] : 119 | try : 120 | reg_key = winreg.OpenKey(K, key, 0, winreg.KEY_ALL_ACCESS) 121 | except FileNotFoundError : 122 | reg_key = winreg.CreateKey(K, key) 123 | val = os.path.join(this_dir, "cb_thunderlink.json") 124 | winreg.SetValueEx(reg_key, None, 0, winreg.REG_SZ, val) 125 | winreg.CloseKey(reg_key) 126 | 127 | # And here for the cbthunderlink:// OS integration 128 | K = winreg.HKEY_CLASSES_ROOT 129 | for protocol in protocols: 130 | key = protocol 131 | try : 132 | reg_key = winreg.OpenKey(K, key, 0, winreg.KEY_ALL_ACCESS) 133 | except FileNotFoundError : 134 | reg_key = winreg.CreateKey(K, key) 135 | winreg.SetValueEx(reg_key, None, 0, winreg.REG_SZ, f"URL:{protocol} Protocol") 136 | winreg.SetValueEx(reg_key, "URL Protocol", 0, winreg.REG_SZ, "") 137 | winreg.CloseKey(reg_key) 138 | 139 | key = protocol + r'\shell\open\command' 140 | try : 141 | reg_key = winreg.OpenKey(K, key, 0, winreg.KEY_ALL_ACCESS) 142 | except FileNotFoundError : 143 | reg_key = winreg.CreateKey(K, key) 144 | val = os.path.join(this_dir, "cb_thunderlink.exe") 145 | winreg.SetValueEx(reg_key, None, 0, winreg.REG_SZ, f"\"{val}\" \"%1\"") 146 | winreg.CloseKey(reg_key) 147 | 148 | print("Registry setup finished") 149 | 150 | if sys.platform == "linux" : 151 | 152 | manifest_location = \ 153 | os.path.expanduser("~/.mozilla/native-messaging-hosts/cb_thunderlink.json") 154 | try : 155 | os.makedirs(os.path.dirname(manifest_location)) 156 | except FileExistsError : 157 | pass 158 | script_full_name = os.path.normpath(os.path.join(this_dir, sys.argv[0])) 159 | 160 | print(f"Registering {script_full_name} to Thunderbird ({manifest_location})") 161 | 162 | with open ("cb_thunderlink.json", "r", encoding='utf-8') as f : 163 | d = json.load(f) 164 | d['path'] = script_full_name 165 | with open (manifest_location, "w", encoding='utf-8') as f : 166 | json.dump(d, f) 167 | 168 | # First shot for gnome/gio based systems. 169 | # Likely I will need here a bunch of variations according to 170 | # the distribution/desktop. 171 | 172 | for protocol in protocols : 173 | desktop_file_dir = os.path.expanduser("~/.local/share/applications") 174 | desktop_file_name = f"cb_thunderlink_{protocol}.desktop" 175 | desktop_file_fullname = os.path.join(desktop_file_dir, desktop_file_name) 176 | print(f"Writing {desktop_file_fullname}") 177 | desktop_file = ( 178 | "[Desktop Entry]\n" 179 | "Encoding=UTF-8\n" 180 | f"Name=cb_thunderlink_{protocol}\n" 181 | f"Exec={script_full_name} %u\n" 182 | "Terminal=false\n" 183 | "X-MultipleArgs=false\n" 184 | "Type=Application\n" 185 | "Icon=thunderbird\n" 186 | "Categories=Application;Network;Email;\n" 187 | f"MimeType=x-scheme-handler/{protocol};\n" 188 | "StartupNotify=true\n" 189 | "Actions=Compose;Contacts\n" 190 | "NoDisplay=true\n") 191 | try : 192 | os.makedirs(desktop_file_dir) 193 | except FileExistsError : 194 | pass 195 | with open (desktop_file_fullname, "w", encoding='utf-8') as f: 196 | f.write(desktop_file) 197 | st = os.stat(desktop_file_fullname) 198 | os.chmod(desktop_file_fullname, st.st_mode|stat.S_IEXEC) 199 | 200 | gio_command = ["gio", "mime", f"x-scheme-handler/{protocol}", desktop_file_name] 201 | #f"gio mime x-scheme-handler/{protocol} {desktop_file_name}" 202 | print(f"Executing {gio_command}") 203 | gio_output = subprocess.Popen(gio_command, stdout=subprocess.PIPE).communicate() 204 | print(gio_output) 205 | 206 | elif script_function in "command" : 207 | 208 | # We are the command line interface. 209 | # Get the argument and send the identifier over the socket to our listening instance. 210 | logger.info("Executing cb_thunderlink") 211 | send_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 212 | send_socket.connect(server_address) 213 | # Hmz. 2025-03-25: not well understood, issue (messageid%3d) is only there on some machines 214 | decoded_input = urllib.parse.unquote(sys.argv[1]) 215 | message = decoded_input.strip('/') 216 | logger.info(f"Message sent: {message}") 217 | encoded_content = json.dumps(message).encode("utf-8") 218 | encoded_length = struct.pack('=I', len(encoded_content)) 219 | try : 220 | send_socket.send(encoded_length) 221 | send_socket.send(struct.pack(str(len(encoded_content))+"s",encoded_content)) 222 | finally : 223 | send_socket.close() 224 | 225 | else : 226 | # We are the instance interfacing with background.js 227 | # Just listen on our socket and if something would be received, 228 | # pipe it through stdout to the web-extension. 229 | logger.info("Interfacing with background.js") 230 | receive_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 231 | bind_tries = 0 232 | while bind_tries < 10 : 233 | try : 234 | logger.info(f"Trying to bind to {server_address}") 235 | receive_sock.bind(server_address) 236 | break 237 | except OSError : 238 | bind_tries += 1 239 | time.sleep(2) 240 | logger.info(f"Bound to {server_address}") 241 | receive_sock.listen(1) 242 | logger.info(f"Start listening on {server_address}") 243 | while True : 244 | connection, client_address = receive_sock.accept() 245 | logger.info(f"Connection from {client_address}") 246 | try : 247 | raw_length = connection.recv(4) 248 | if not raw_length: 249 | sys.exit(0) 250 | message_length = struct.unpack('=I', raw_length)[0] 251 | message = connection.recv(message_length) 252 | sys.stdout.buffer.write(raw_length) 253 | sys.stdout.buffer.write(message) 254 | sys.stdout.buffer.flush() 255 | finally : 256 | connection.close() 257 | 258 | logger.info("Finishing {}".format(program_name)) 259 | logger.info("="*100) 260 | 261 | # vim: syntax=python ts=4 sw=4 sts=4 sr et columns=100 262 | -------------------------------------------------------------------------------- /mail_extension/cb_background.js: -------------------------------------------------------------------------------- 1 | // 2 | // $BeginLicense$ 3 | // 4 | // (C) 2020-2021 by Camiel Bouchier (camiel@bouchier.be) 5 | // 6 | // This file is part of cb_thunderlink. 7 | // 8 | // License: Mozilla Public License Version 2.0 9 | // (https://github.com/CamielBouchier/cb_thunderlink/blob/main/LICENSE) 10 | // 11 | // $EndLicense$ 12 | // 13 | 14 | console.log("cb_background started") 15 | 16 | import { 17 | strftime, 18 | utoa, 19 | cb_show_message_from_cbthunderlink, 20 | cb_show_message_from_thunderlink 21 | } from "./modules/cb_tools.mjs" 22 | 23 | const default_settings = { 24 | open_mode: "three_pane", 25 | avoid_folders: [], 26 | prefer_folders: [], 27 | conf_links: { 28 | 0: { enable: true, name: "cbthunderlink", value: "cbthunderlink://$cblink$" }, 29 | 1: { enable: true, name: "thunderlink", value: "thunderlink://messageid=$msgid$" }, 30 | }, 31 | now_strftime_format: "%d-%m-%Y %H:%M:%S" 32 | } 33 | 34 | var settings = default_settings 35 | 36 | // 37 | // This will get settings from the local storage. 38 | // If no local storage would exist for cb_thunderbird, or it is incomplete, it will be completed. 39 | // Finally, as each change of settings could result in a new context_menu, that is called as well. 40 | // 41 | 42 | async function get_settings() { 43 | let config = await browser.storage.local.get('cb_thunderlink') 44 | if (!config.cb_thunderlink) { 45 | await browser.storage.local.set({ cb_thunderlink: settings }) 46 | } else { 47 | settings = { 48 | open_mode: config.cb_thunderlink.open_mode, 49 | avoid_folders: config.cb_thunderlink.avoid_folders, 50 | prefer_folders: config.cb_thunderlink.prefer_folders, 51 | conf_links: config.cb_thunderlink.conf_links, 52 | now_strftime_format: config.cb_thunderlink.now_strftime_format 53 | } 54 | if (!settings.open_mode) { 55 | settings.open_mode = default_settings.open_mode 56 | await browser.storage.local.set({ cb_thunderlink: settings }) 57 | } 58 | if (!settings.avoid_folders) { 59 | settings.avoid_folders = default_settings.avoid_folders 60 | await browser.storage.local.set({ cb_thunderlink: settings }) 61 | } 62 | if (!settings.prefer_folders) { 63 | settings.prefer_folders = default_settings.prefer_folders 64 | await browser.storage.local.set({ cb_thunderlink: settings }) 65 | } 66 | if (!settings.conf_links) { 67 | settings.conf_links = default_settings.conf_links 68 | await browser.storage.local.set({ cb_thunderlink: settings }) 69 | } 70 | if (!settings.now_strftime_format) { 71 | settings.now_strftime_format = default_settings.now_strftime_format 72 | await browser.storage.local.set({ cb_thunderlink: settings }) 73 | } 74 | } 75 | // A change could require a new context menu setting. 76 | create_context_menu() 77 | } 78 | 79 | browser.storage.onChanged.addListener((what, area) => { 80 | if (area == 'local' && what.cb_thunderlink) { 81 | get_settings() 82 | } 83 | }) 84 | 85 | await get_settings() // Actually will kick us of, generating the context menus. 86 | 87 | 88 | 89 | // 90 | // Link generator 91 | // 92 | 93 | async function link_to_clipboard(idx, the_message, selected_text) { 94 | let link = settings.conf_links[idx].value 95 | 96 | // Following few lines are +/- from original thunderlink. 97 | // replacing double quotes so they are escaped for JSON.parse 98 | link = link.replace(/["]/g, "\\\"") 99 | // convert escape characters like \t to tabs 100 | link = JSON.parse("\"" + link + "\"") 101 | 102 | let full = await messenger.messages.getFull(the_message.id) 103 | 104 | let author = the_message.author 105 | let date = the_message.date 106 | let subject = the_message.subject 107 | let cblink = utoa(date.toJSON() + ";" + author) 108 | let msgid = full.headers['message-id'][0].replace(/^$/, '') 109 | 110 | // Extract author name and email. This recognizes "foo@bar" or "Foo bar " 111 | // after removing all double quotes. Perhaps there is a more robust way. 112 | let author_name = '' 113 | let author_email = '' 114 | let author_match = author.replace(/["]/g, '').match(/^((.*)\s+<)?([^@<\s]+@[^@\s>]+)>?$/) 115 | if (author_match) { 116 | author_name = author_match[2] 117 | author_email = author_match[3] 118 | } 119 | 120 | // Following few lines are +/- from original thunderlink. 121 | let date_time = new Date(date) 122 | let date_time_iso = date_time.toISOString() 123 | let date_time_iso_match = date_time_iso.match(/^(.*)T(.*)\.\d{3}Z$/) 124 | let date_iso = date_time_iso_match[1] 125 | let time_iso = date_time_iso_match[2] 126 | let date_locale = date_time.toLocaleDateString() 127 | let time_locale = date_time.toLocaleTimeString() 128 | 129 | // https://github.com/CamielBouchier/cb_thunderlink/issues/43 : Adding now time 130 | let now_date_time = strftime(settings.now_strftime_format) 131 | 132 | // Following few lines are +/- from original thunderlink. 133 | // replace a few characters that frequently cause trouble 134 | // with a focus on org-mode, provided as filtered_subject 135 | let subject_filtered = subject.split("[").join("(") 136 | subject_filtered = subject_filtered.split("]").join(")") 137 | subject_filtered = subject_filtered.replace(/[<>'"`´]/g, "") 138 | 139 | link = link.replace(/\$msgid\$/ig, msgid) 140 | link = link.replace(/\$cblink\$/ig, cblink) 141 | link = link.replace(/\$author\$/ig, author) 142 | link = link.replace(/\$author_name\$/ig, author_name) 143 | link = link.replace(/\$author_email\$/ig, author_email) 144 | link = link.replace(/\$date\$/ig, date_time) 145 | link = link.replace(/\$date_iso\$/ig, date_iso) 146 | link = link.replace(/\$date_locale\$/ig, date_locale) 147 | link = link.replace(/\$time_iso\$/ig, time_iso) 148 | link = link.replace(/\$time_locale\$/ig, time_locale) 149 | link = link.replace(/\$subject\$/ig, subject) 150 | link = link.replace(/\$subject_filtered\$/ig, subject_filtered) 151 | link = link.replace(/\$selected_text\$/ig, selected_text ?? "") 152 | link = link.replace(/\$now\$/ig, now_date_time) 153 | await navigator.clipboard.writeText(link); 154 | } 155 | 156 | // 157 | // Build the context menus for generating a link. 158 | // 159 | 160 | async function create_context_menu() { 161 | 162 | browser.menus.removeAll() // We might be called several times (get_settings!) 163 | 164 | let main_context_menu = { 165 | contexts: ['message_list'], 166 | title: 'cb_thunderlink', 167 | id: 'main_context_menu' 168 | } 169 | let main_context_id = await browser.menus.create(main_context_menu) 170 | 171 | let selection_context_menu = { 172 | contexts: ['selection', 'tab', 'page'], 173 | title: 'cb_thunderlink', 174 | id: 'selection_context_menu' 175 | }; 176 | let selection_context_id = await browser.menus.create(selection_context_menu); 177 | 178 | // gather enabled configured links 179 | let conf_links = settings.conf_links; 180 | 181 | for (let [i, conf_link] of Object.entries(conf_links)) { 182 | 183 | // ignore disabled links 184 | if (!conf_link.enable) { 185 | continue; 186 | } 187 | 188 | // add sub menu entry to the message_list menu 189 | browser.menus.create({ 190 | contexts: ['message_list'], 191 | title: conf_link.name, 192 | parentId: main_context_id, 193 | id: 'sub_context_menu_' + i 194 | }); 195 | 196 | // add sub menu entry to the selection menu 197 | browser.menus.create({ 198 | contexts: ['selection', 'tab', 'page'], 199 | title: conf_link.name, 200 | parentId: selection_context_id, 201 | id: 'other_menu_' + i 202 | }); 203 | } 204 | 205 | browser.menus.onClicked.addListener((info, tab) => { 206 | if (info.menuItemId.startsWith('sub_context_menu_')) { 207 | on_context_menu(info) 208 | } 209 | if (info.menuItemId.startsWith('other_menu_')) { 210 | on_context_menu_selection(info, tab) 211 | } 212 | }) 213 | } 214 | 215 | // 216 | // Do our link generating action according to the submenu clicked. 217 | // Get it to the clipboard. 218 | // 219 | 220 | async function on_context_menu(e) { 221 | let the_message = e.selectedMessages.messages[0] 222 | let idx = e.menuItemId.replace('sub_context_menu_', '') 223 | await link_to_clipboard(idx, the_message) 224 | } 225 | 226 | // 227 | // Handle menu entries from the right click menu in a (message) tab 228 | // 229 | async function on_context_menu_selection(info, tab) { 230 | let idx = info.menuItemId.replace('other_menu_', '') 231 | let message = await messenger.messageDisplay.getDisplayedMessage(tab.id); 232 | 233 | if (!message) { 234 | console.error(`Could not find message in tab #${tab.id} (probably not a message tab?)`); 235 | return false; 236 | } 237 | 238 | let selected_text = info.selectionText; 239 | await link_to_clipboard(idx, message, selected_text); 240 | } 241 | 242 | // 243 | // Also trigger the link generating action from keyboard shortcuts. 244 | // 245 | 246 | browser.commands.onCommand.addListener(async (command) => { 247 | let copy_link_match = command.match(/^copy_link_(\d+)$/) 248 | if (copy_link_match) { 249 | let idx = copy_link_match[1] - 1 250 | if (settings.conf_links[idx].enable) { 251 | let tab_query = { active: true, currentWindow: true } 252 | messenger.tabs.query(tab_query).then(tabs => { 253 | messenger.messageDisplay.getDisplayedMessage(tabs[0].id).then((message) => { 254 | link_to_clipboard(idx, message) 255 | }) 256 | }) 257 | } 258 | } 259 | let drop_link_match = command.match(/^drop_link$/) 260 | if (drop_link_match) { 261 | let incoming_link = await navigator.clipboard.readText() 262 | handle_incoming_link(incoming_link) 263 | } 264 | }) 265 | 266 | // 267 | // Handle incoming link (be it by button, be it by script) 268 | // 269 | 270 | async function handle_incoming_link(incoming_link) { 271 | 272 | console.log("handle_incoming_link", incoming_link) 273 | 274 | let open_mode = settings.open_mode 275 | let match = /((cb)?thunderlink):\/\/([^\s\]]+)/.exec(incoming_link) 276 | let link_type = match[1] 277 | let link = match[3] 278 | 279 | switch (link_type) { 280 | case "cbthunderlink": 281 | cb_show_message_from_cbthunderlink(link, open_mode, settings); 282 | break; 283 | case "thunderlink": 284 | cb_show_message_from_thunderlink(link, open_mode, settings) 285 | break; 286 | } 287 | } 288 | 289 | // 290 | // Listen to incoming links on the browserAction. 291 | // 292 | 293 | messenger.browserAction.onClicked.addListener(async () => { 294 | let incoming_link = await navigator.clipboard.readText() 295 | handle_incoming_link(incoming_link) 296 | }) 297 | 298 | // 299 | // Start our accompanying python script. 300 | // See : https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging 301 | // 302 | 303 | var port = browser.runtime.connectNative("cb_thunderlink") 304 | 305 | port.onMessage.addListener((encoded_link) => { 306 | handle_incoming_link(encoded_link) 307 | }) 308 | 309 | // vim: syntax=javascript ts=4 sw=4 sts=4 sr et columns=100 310 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------