├── .env ├── static ├── favicon.ico ├── assets │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-32.png │ ├── icon-64.png │ ├── icon-80.png │ └── logo-filled.png ├── taskpane.js ├── commands.js └── taskpane.css ├── scripts ├── uninstall.sh ├── uninstall_linux.sh ├── install_linux.sh ├── verify.sh ├── verify_linux.sh ├── install.ps1 ├── uninstall.ps1 └── verify.ps1 ├── requirements.txt ├── templates ├── index.html ├── commands.html └── taskpane.html ├── devcerts ├── defaults.py ├── install.py ├── verify.py └── generate.py ├── .gitignore ├── app.py ├── README.md └── manifest_python.xml /.env: -------------------------------------------------------------------------------- 1 | APP_MODE=DEV -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Masterjx9/Outlook-Addin-TaskPane-python/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/assets/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Masterjx9/Outlook-Addin-TaskPane-python/HEAD/static/assets/icon-128.png -------------------------------------------------------------------------------- /static/assets/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Masterjx9/Outlook-Addin-TaskPane-python/HEAD/static/assets/icon-16.png -------------------------------------------------------------------------------- /static/assets/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Masterjx9/Outlook-Addin-TaskPane-python/HEAD/static/assets/icon-32.png -------------------------------------------------------------------------------- /static/assets/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Masterjx9/Outlook-Addin-TaskPane-python/HEAD/static/assets/icon-64.png -------------------------------------------------------------------------------- /static/assets/icon-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Masterjx9/Outlook-Addin-TaskPane-python/HEAD/static/assets/icon-80.png -------------------------------------------------------------------------------- /static/assets/logo-filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Masterjx9/Outlook-Addin-TaskPane-python/HEAD/static/assets/logo-filled.png -------------------------------------------------------------------------------- /scripts/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | hashes=$(security find-certificate -c "$1" -a -Z | grep SHA-1 | awk '{ print $NF }') 3 | for hash in $hashes; do 4 | security delete-certificate -Z $hash 5 | done -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.9.0 2 | cffi==1.17.1 3 | click==8.1.8 4 | colorama==0.4.6 5 | cryptography==44.0.0 6 | Flask==3.1.0 7 | itsdangerous==2.2.0 8 | Jinja2==3.1.5 9 | MarkupSafe==3.0.2 10 | pycparser==2.22 11 | python-dotenv==1.0.1 12 | Werkzeug==3.1.3 -------------------------------------------------------------------------------- /scripts/uninstall_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f "/usr/sbin/update-ca-certificates" ]; then 4 | sudo rm -r /usr/local/share/ca-certificates/office-addin-dev-certs/$1 && sudo /usr/sbin/update-ca-certificates --fresh 5 | elif [ -f "/usr/sbin/update-ca-trust" ]; then 6 | sudo rm -r /etc/ca-certificates/trust-source/anchors/office-addin-dev-certs-ca.crt && sudo /usr/sbin/update-ca-trust 7 | fi -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Index 9 | 10 | 11 | 12 |

Index

13 |

This is an HTML file served up by Flask

14 | 15 | 16 | -------------------------------------------------------------------------------- /scripts/install_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f "/usr/sbin/update-ca-certificates" ]; then 4 | sudo mkdir -p /usr/local/share/ca-certificates/office-addin-dev-certs && sudo cp $1 /usr/local/share/ca-certificates/office-addin-dev-certs && sudo /usr/sbin/update-ca-certificates 5 | elif [ -f "/usr/sbin/update-ca-trust" ]; then 6 | sudo cp $1 /etc/ca-certificates/trust-source/anchors/office-addin-dev-certs-ca.crt && sudo /usr/sbin/update-ca-trust 7 | fi -------------------------------------------------------------------------------- /scripts/verify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | certs=$(security find-certificate -a -c "$1" -p) 3 | while read line; do 4 | if [[ "$line" == *"--BEGIN"* ]]; then 5 | cert=$line 6 | else 7 | cert="$cert"$'\n'"$line" 8 | if [[ "$line" == *"--END"* ]]; then 9 | if [ 0 -lt $(echo "$cert" | openssl x509 -checkend 86400 | grep -c "will not expire") ]; then 10 | echo "$cert" 11 | fi 12 | fi 13 | fi 14 | done <<< "$certs" -------------------------------------------------------------------------------- /scripts/verify_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f "/usr/sbin/update-ca-certificates" ]; then 4 | echo [ -f /usr/local/share/ca-certificates/office-addin-dev-certs/$1 ] && openssl x509 -in /usr/local/share/ca-certificates/office-addin-dev-certs/$1 -checkend 86400 -noout 5 | elif [ -f "/usr/sbin/update-ca-trust" ]; then 6 | echo [ -f /etc/ca-certificates/trust-source/anchors/office-addin-dev-certs-ca.crt ] && openssl x509 -in /etc/ca-certificates/trust-source/anchors/office-addin-dev-certs-ca.crt -checkend 86400 -noout 7 | fi -------------------------------------------------------------------------------- /templates/commands.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /devcerts/defaults.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | 4 | # Default certificate names 5 | certificate_directory_name = ".office-addin-dev-certs" 6 | certificate_directory = os.path.join(pathlib.Path.home(), certificate_directory_name) 7 | ca_certificate_file_name = "ca.crt" 8 | ca_certificate_path = os.path.join(certificate_directory, ca_certificate_file_name) 9 | localhost_certificate_file_name = "localhost.crt" 10 | localhost_certificate_path = os.path.join(certificate_directory, localhost_certificate_file_name) 11 | localhost_key_file_name = "localhost.key" 12 | localhost_key_path = os.path.join(certificate_directory, localhost_key_file_name) 13 | 14 | # Default certificate details 15 | certificate_name = "Developer CA for Microsoft Office Add-ins" 16 | country_code = "US" 17 | days_until_certificate_expires = 30 18 | domain = ["127.0.0.1", "localhost"] 19 | locality = "Redmond" 20 | state = "WA" -------------------------------------------------------------------------------- /static/taskpane.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | /* global document, Office */ 7 | 8 | Office.onReady((info) => { 9 | if (info.host === Office.HostType.Outlook) { 10 | document.getElementById("sideload-msg").style.display = "none"; 11 | document.getElementById("app-body").style.display = "flex"; 12 | document.getElementById("run").onclick = run; 13 | } 14 | }); 15 | 16 | async function run() { 17 | const bootstrapAlertDiv = document.getElementById("alert"); 18 | bootstrapAlertDiv.innerHTML = ` 19 | 23 | `; 24 | 25 | await new Promise(resolve => setTimeout(resolve, 4000)); 26 | bootstrapAlertDiv.innerHTML = ""; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /scripts/install.ps1: -------------------------------------------------------------------------------- 1 | if($args.Count -ne 2){ 2 | throw "Usage: install.ps1 " 3 | } 4 | 5 | # Without this, the script always succeeds (exit code = 0) 6 | $ErrorActionPreference = 'Stop' 7 | 8 | $machine = $args[0] 9 | $caCertificatePath=$args[1] 10 | if(Get-Command -name Import-Certificate -ErrorAction SilentlyContinue){ 11 | if ($PSVersionTable.PSVersion.Major -le 5) { 12 | # The following line is required in case pwsh is one of the parent callers 13 | # because the changes it makes to PSModulePath are not backward compatible with Windows powershell. 14 | $env:PSModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine') 15 | } 16 | Import-Certificate -CertStoreLocation cert:\\$machine\\Root ${caCertificatePath} 17 | } 18 | else{ 19 | # Legacy system support 20 | $pfx = new-object System.Security.Cryptography.X509Certificates.X509Certificate2 21 | $pfx.import($caCertificatePath) 22 | 23 | $store = new-object System.Security.Cryptography.X509Certificates.X509Store("Root", $machine) 24 | $store.open("MaxAllowed") 25 | $store.add($pfx) 26 | $store.close() 27 | } -------------------------------------------------------------------------------- /scripts/uninstall.ps1: -------------------------------------------------------------------------------- 1 | if($args.Count -ne 2){ 2 | throw "Usage: uninstall.ps1 " 3 | } 4 | 5 | # Without this, the script always succeeds (exit code = 0) 6 | $ErrorActionPreference = 'Stop' 7 | 8 | $machine = $args[0] 9 | $caCertificateName=$args[1] 10 | if(Get-Command -name Import-Certificate -ErrorAction SilentlyContinue){ 11 | if ($PSVersionTable.PSVersion.Major -le 5) { 12 | # The following line is required in case pwsh is one of the parent callers 13 | # because the changes it makes to PSModulePath are not backward compatible with Windows powershell. 14 | $env:PSModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine') 15 | } 16 | Get-ChildItem cert:\\$machine\\Root | Where-Object { $_.IssuerName.Name -like "*CN=$caCertificateName*" } | Remove-Item 17 | } 18 | else{ 19 | # Legacy system support 20 | $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("root", $machine) 21 | $store.Open("MaxAllowed") 22 | $certs = $store.Certificates.Find("FindBySubjectName", $caCertificateName, $false) 23 | foreach ($cert in $certs){ 24 | $store.Remove($cert) 25 | } 26 | $store.close() 27 | } -------------------------------------------------------------------------------- /static/commands.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | /* global global, Office, self, window */ 7 | 8 | Office.onReady(() => { 9 | // If needed, Office.js is ready to be called 10 | }); 11 | 12 | /** 13 | * Shows a notification when the add-in command is executed. 14 | * @param event {Office.AddinCommands.Event} 15 | */ 16 | function action(event) { 17 | const message = { 18 | type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage, 19 | message: "Performed action.", 20 | icon: "Icon.80x80", 21 | persistent: true, 22 | }; 23 | 24 | // Show a notification message 25 | Office.context.mailbox.item.notificationMessages.replaceAsync("action", message); 26 | 27 | // Be sure to indicate when the add-in command function is complete 28 | event.completed(); 29 | } 30 | 31 | function getGlobal() { 32 | return typeof self !== "undefined" 33 | ? self 34 | : typeof window !== "undefined" 35 | ? window 36 | : typeof global !== "undefined" 37 | ? global 38 | : undefined; 39 | } 40 | 41 | const g = getGlobal(); 42 | 43 | // The add-in command functions need to be available in global scope 44 | g.action = action; 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | .venv/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # Visual Studio Code 93 | .vscode/ 94 | 95 | #azure information 96 | .azure 97 | 98 | #node modules 99 | static/node_modules 100 | 101 | #personal manifest files 102 | manifest_python_personal.xml 103 | manifest_python_SSO.xml -------------------------------------------------------------------------------- /static/taskpane.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 3 | * See LICENSE in the project root for license information. 4 | */ 5 | 6 | html, 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | ul { 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | .ms-welcome__header { 20 | padding: 20px; 21 | padding-bottom: 30px; 22 | padding-top: 100px; 23 | display: -webkit-flex; 24 | display: flex; 25 | -webkit-flex-direction: column; 26 | flex-direction: column; 27 | align-items: center; 28 | } 29 | 30 | .ms-welcome__main { 31 | display: -webkit-flex; 32 | display: flex; 33 | -webkit-flex-direction: column; 34 | flex-direction: column; 35 | -webkit-flex-wrap: nowrap; 36 | flex-wrap: nowrap; 37 | -webkit-align-items: center; 38 | align-items: center; 39 | -webkit-flex: 1 0 0; 40 | flex: 1 0 0; 41 | padding: 10px 20px; 42 | } 43 | 44 | .ms-welcome__main > h2 { 45 | width: 100%; 46 | text-align: center; 47 | } 48 | 49 | .ms-welcome__features { 50 | list-style-type: none; 51 | margin-top: 20px; 52 | } 53 | 54 | .ms-welcome__features.ms-List .ms-ListItem { 55 | padding-bottom: 20px; 56 | display: -webkit-flex; 57 | display: flex; 58 | } 59 | 60 | .ms-welcome__features.ms-List .ms-ListItem > .ms-Icon { 61 | margin-right: 10px; 62 | } 63 | 64 | .ms-welcome__action.ms-Button--hero { 65 | margin-top: 30px; 66 | } 67 | 68 | .ms-Button.ms-Button--hero .ms-Button-label { 69 | color: #0078d7; 70 | } 71 | 72 | .ms-Button.ms-Button--hero:hover .ms-Button-label, 73 | .ms-Button.ms-Button--hero:focus .ms-Button-label{ 74 | color: #005a9e; 75 | cursor: pointer; 76 | } 77 | 78 | b { 79 | font-weight: bold; 80 | } -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import render_template 3 | from flask.helpers import send_file 4 | import os 5 | import dotenv 6 | from devcerts.install import ensure_certificates_are_installed 7 | 8 | 9 | dotenv.load_dotenv() 10 | 11 | app = Flask(__name__, static_folder="static", template_folder="templates") 12 | 13 | @app.route("/") 14 | def index(): 15 | return render_template("index.html") 16 | 17 | @app.route("/taskpane.html") 18 | def taskpane(): 19 | return render_template("taskpane.html") 20 | 21 | @app.route("/commands.html") 22 | def commands(): 23 | return render_template("commands.html") 24 | 25 | @app.route("/assets/icon-16.png") 26 | def icon16(): 27 | return send_file("./static/assets/icon-16.png",mimetype='image/png') 28 | 29 | @app.route("/assets/icon-32.png") 30 | def icon32(): 31 | return send_file("./static/assets/icon-32.png",mimetype='image/png') 32 | 33 | @app.route("/assets/icon-64.png") 34 | def icon64(): 35 | return send_file("./static/assets/icon-64.png",mimetype='image/png') 36 | 37 | @app.route("/assets/icon-80.png") 38 | def icon128(): 39 | return send_file("./static/assets/icon-80.png",mimetype='image/png') 40 | 41 | @app.route("/assets/logo-filled.png") 42 | def iconlogofilled(): 43 | return send_file("./static/assets/logo-filled.png",mimetype='image/png') 44 | 45 | @app.route('/favicon.ico') 46 | def favicon(): 47 | return send_file('./static/favicon.ico', mimetype='image/vnd.microsoft.icon') 48 | 49 | if __name__ == "__main__": 50 | if os.environ.get("APP_MODE") == "DEV": 51 | print("Running in DEV mode") 52 | # Call the function to ensure certificates are installed and valid 53 | ensure_certificates_are_installed() 54 | 55 | # Assuming the ensure_certificates_are_installed function updates the default paths as needed 56 | from devcerts.defaults import localhost_certificate_path, localhost_key_path 57 | ssl_context = (localhost_certificate_path, localhost_key_path) 58 | 59 | app.run(debug=True, ssl_context=ssl_context) 60 | else: 61 | app.run(debug=True) 62 | -------------------------------------------------------------------------------- /devcerts/install.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | from pathlib import Path 5 | import devcerts.defaults as defaults 6 | from devcerts.verify import is_ca_certificate_installed, verify_certificates 7 | from devcerts.generate import generate_certificates 8 | 9 | def get_install_command(ca_certificate_path, machine=False): 10 | if sys.platform == "win32": 11 | script = Path(__file__).parent / "../scripts/install.ps1" 12 | store = "LocalMachine" if machine else "CurrentUser" 13 | return f"powershell -ExecutionPolicy Bypass -File \"{script}\" {store} \"{ca_certificate_path}\"" 14 | elif sys.platform == "darwin": 15 | prefix = "sudo " if machine else "" 16 | keychain_file = "/Library/Keychains/System.keychain" if machine else "~/Library/Keychains/login.keychain-db" 17 | return f"{prefix}security add-trusted-cert -d -r trustRoot -k {keychain_file} '{ca_certificate_path}'" 18 | elif sys.platform == "linux": 19 | script = Path(__file__).parent / "../scripts/install_linux.sh" 20 | return f"sudo sh '{script}' '{ca_certificate_path}'" 21 | else: 22 | raise Exception(f"Platform not supported: {sys.platform}") 23 | 24 | def install_ca_certificate(ca_certificate_path=defaults.ca_certificate_path, machine=False): 25 | command = get_install_command(ca_certificate_path, machine) 26 | print("Installing CA certificate \"Developer CA for Microsoft Office Add-ins\"...") 27 | 28 | # Check if the CA certificate is already installed (implementation depends on your setup) 29 | if not is_ca_certificate_installed(): 30 | subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 31 | print(f"You now have trusted access to https://localhost.\nCertificate: {defaults.localhost_certificate_path}\nKey: {defaults.localhost_key_path}") 32 | 33 | 34 | def ensure_certificates_are_installed(machine=False): 35 | are_certificates_valid = verify_certificates() 36 | 37 | if are_certificates_valid: 38 | print(f"You already have trusted access to https://localhost.\nCertificate: {defaults.localhost_certificate_path}\nKey: {defaults.localhost_key_path}") 39 | else: 40 | print("Certificates are not installed or are invalid. Generating and installing new certificates...") 41 | generate_certificates() 42 | install_ca_certificate(defaults.ca_certificate_path, machine) 43 | 44 | if __name__ == "__main__": 45 | ensure_certificates_are_installed(machine=False) 46 | -------------------------------------------------------------------------------- /scripts/verify.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [Parameter(Mandatory=$true)] 3 | [ValidateNotNullOrEmpty()] 4 | [string] 5 | $CaCertificateName, 6 | 7 | [Parameter(Mandatory=$true)] 8 | [ValidateNotNullOrEmpty()] 9 | [string] 10 | $CaCertificatePath, 11 | 12 | [Parameter(Mandatory=$true)] 13 | [ValidateNotNullOrEmpty()] 14 | [string] 15 | $LocalhostCertificatePath, 16 | 17 | [Parameter(Mandatory = $false)] 18 | [ValidateNotNullOrEmpty()] 19 | [string] 20 | $OutputMarker, 21 | 22 | [switch] 23 | $ReturnInvalidCertificate 24 | ) 25 | 26 | # An optional output marker that can be used to find the beginning of this script's output 27 | if ($OutputMarker) { 28 | Write-Output $OutputMarker 29 | } 30 | 31 | # Without this, the script always succeeds (exit code = 0) 32 | $ErrorActionPreference = 'Stop' 33 | 34 | if ($PSVersionTable.PSVersion.Major -le 5) { 35 | # The following line is required in case pwsh is one of the parent callers 36 | # because the changes it makes to PSModulePath are not backward compatible with Windows powershell. 37 | $env:PSModulePath = [Environment]::GetEnvironmentVariable('PSModulePath', 'Machine') 38 | } 39 | 40 | if(Get-Command -name Import-Certificate -ErrorAction SilentlyContinue){ 41 | $result = Get-ChildItem cert:\\CurrentUser\\Root | Where-Object Issuer -like "*CN=$CaCertificateName*" 42 | if (!$ReturnInvalidCertificate) { 43 | $result = $result | Where-Object { $_.NotAfter -gt (Get-Date).AddDays(1) } 44 | if ($result -and ($result.Length -eq 1) -and (Test-Path $CaCertificatePath) -and (Test-Path $LocalhostCertificatePath)) { 45 | # Check that CA certificate in store is the same as ca.crt 46 | $caCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($CaCertificatePath) 47 | $caThumbprint = $caCert.Thumbprint 48 | 49 | $result = $result | Where-Object Thumbprint -eq $caThumbprint 50 | 51 | if ($result) { 52 | # Check that it matches the issuer of localhost.crt 53 | $localhostCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($LocalhostCertificatePath) 54 | 55 | $localhostChain = [System.Security.Cryptography.X509Certificates.X509Chain]::new() 56 | $localhostChain.Build($localhostCert) | Out-Null 57 | $localhostTrustedIssuer = $localhostChain.ChainElements.Certificate | Where-Object { $_.Subject -eq $localhostCert.Issuer -and $_.Thumbprint -eq $caThumbprint } 58 | if (!$localhostTrustedIssuer) { 59 | $result = $null 60 | } 61 | } 62 | } 63 | else { 64 | $result = $null 65 | } 66 | } 67 | 68 | $result | Format-List 69 | } 70 | else{ 71 | # Legacy system support 72 | Get-ChildItem cert:\\CurrentUser\\Root | Where-Object { $_.Subject -like "*CN=$CaCertificateName*"} | Where-Object { $_.NotAfter -gt (Get-Date).AddDays(1) } | Format-List 73 | } -------------------------------------------------------------------------------- /devcerts/verify.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import sys 4 | from pathlib import Path 5 | from cryptography import x509 6 | from cryptography.hazmat.backends import default_backend 7 | from cryptography.hazmat.primitives import serialization 8 | from cryptography.hazmat.primitives.asymmetric import padding 9 | from cryptography.hazmat.primitives import hashes 10 | import devcerts.defaults # Assuming defaults.py is available 11 | 12 | def get_verify_command(return_invalid_certificate=False): 13 | if sys.platform == "win32": 14 | script = Path(__file__).parent / "../scripts/verify.ps1" 15 | default_command = f"powershell -ExecutionPolicy Bypass -File \"{script}\" -CaCertificateName \"{devcerts.defaults.certificate_name}\" -CaCertificatePath \"{devcerts.defaults.ca_certificate_path}\" -LocalhostCertificatePath \"{devcerts.defaults.localhost_certificate_path}\"" 16 | if return_invalid_certificate: 17 | default_command += " -ReturnInvalidCertificate" 18 | return default_command 19 | elif sys.platform == "darwin": 20 | script = Path(__file__).parent / "../scripts/verify.sh" 21 | return f"sh '{script}' '{defaults.certificate_name}'" 22 | elif sys.platform == "linux": 23 | script = Path(__file__).parent / "../scripts/verify_linux.sh" 24 | return f"sh '{script}' '{defaults.ca_certificate_file_name}'" 25 | else: 26 | raise Exception(f"Platform not supported: {sys.platform}") 27 | 28 | def is_ca_certificate_installed(return_invalid_certificate=False): 29 | command = get_verify_command(return_invalid_certificate) 30 | try: 31 | output = subprocess.check_output(command, shell=True, text=True) 32 | if sys.platform == "win32": 33 | print(output) 34 | return len(output.strip()) != 0 35 | if output: 36 | return True 37 | except subprocess.CalledProcessError: 38 | pass 39 | return False 40 | 41 | def validate_certificate_and_key(certificate_path, key_path): 42 | try: 43 | with open(certificate_path, "rb") as cert_file: 44 | cert = x509.load_pem_x509_certificate(cert_file.read(), default_backend()) 45 | 46 | with open(key_path, "rb") as key_file: 47 | key = serialization.load_pem_private_key(key_file.read(), password=None, backend=default_backend()) 48 | 49 | # Attempt to encrypt and decrypt data to validate the cert/key pair 50 | encrypted = key.public_key().encrypt(b"test", padding.PKCS1v15()) 51 | key.decrypt(encrypted, padding.PKCS1v15()) 52 | return True 53 | except Exception as e: 54 | print(f"Validation failed: {e}") 55 | return False 56 | 57 | def verify_certificates(certificate_path=devcerts.defaults.localhost_certificate_path, key_path=devcerts.defaults.localhost_key_path): 58 | is_certificate_valid = validate_certificate_and_key(certificate_path, key_path) 59 | is_ca_installed = is_ca_certificate_installed() 60 | return is_certificate_valid and is_ca_installed 61 | 62 | if __name__ == "__main__": 63 | # Example usage 64 | if verify_certificates(): 65 | print("Certificates are valid and CA is installed.") 66 | else: 67 | print("Certificate validation failed or CA is not installed.") 68 | -------------------------------------------------------------------------------- /templates/taskpane.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | PythonicIT Task Pane Add-in 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | Contoso 32 |

Welcome

33 |
34 |
35 |

Please sideload your add-in to see app body.

36 |
37 |
38 |

Discover what Office Add-ins can do for you today!

39 |
    40 |
  • 41 | 42 | Achieve more with Office integration 43 |
  • 44 |
  • 45 | 46 | Unlock features and functionality 47 |
  • 48 |
  • 49 | 50 | Create and visualize like a pro 51 |
  • 52 |
53 |

Modify the run function in taskpane.js, refresh this window, then click Run.

54 |
55 | Run 56 |
57 |
58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Outlook-Addin-TaskPane-python 2 | Template to get start started writing a TaskPane Outlook Add-in using python for the backend 3 | 4 | ## Video Discussion 5 | [![Video Discussion](https://img.youtube.com/vi/RDL2BWfq43Q/0.jpg)](https://youtu.be/RDL2BWfq43Q) 6 | 7 | This template is a modified version of the office Addin taskpane JS repository here: https://github.com/OfficeDev/Office-Addin-TaskPane-JS with a combination of the python webapp repository from here: https://github.com/Azure-Samples/python-docs-hello-world. 8 | The html files are placed in the `Templates` folder while the assests pictures, javascript files, and css files are placed in the `static` folder. This is to help flask know where html, css, and javascript files would be. 9 | 10 | ## Version Updates 11 | 12 | - 1.0.0.1 - Integrated https into development build using office-addin-dev-certs. We re-wrote the [office-addin-dev-certs](https://github.com/OfficeDev/Office-Addin-Scripts/tree/master/packages/office-addin-dev-certs) from typescript to python. 13 | - 1.0.0.0 - Initial release 14 | 15 | ## Test webapp before deployment 16 | You can run flask locally for development 17 | 1. [Download the zip](https://github.com/Masterjx9/Outlook-Addin-TaskPane-python/archive/refs/heads/master.zip) or use `git clone https://github.com/Masterjx9/Outlook-Addin-TaskPane-python.git` then go to the root of the folder and perform the following commands: 18 | 19 | ### For Windows 20 | ```powershell 21 | py -3 -m venv .venv 22 | .venv\scripts\activate 23 | pip install -r requirements.txt 24 | ``` 25 | 26 | ### For MacOS/Linux 27 | ```bash 28 | python3 -m venv .venv 29 | source .venv/bin/activate 30 | pip install -r requirements.txt 31 | ``` 32 | 33 | 2. In cmd, vscode, or powershell, go to the root of your folder and type `python app.py` 34 | 3. Go to OWA and add your maniest.xml. There are multiple methods dependings on your permissions and rights to your azure ad - 35 | - If you are an admin: https://docs.servicenow.com/bundle/quebec-employee-service-management/page/product/workplace-reservations-outlook-addin/task/upload-the-manifest-file-office365.html

36 | 37 | - If you are a normal user:
38 | - Go to this link: https://aka.ms/olksideload
39 | - From the “Custom Add-ins” pop-up click the “My Add-ins” tab
40 | ![My Add-ins](https://learn.microsoft.com/en-us/office/dev/add-ins/images/outlook-sideload-my-add-ins-owa.png "Image Title") 41 | - On the “My Add-ins” tab scroll down to bottom of the page and click the “+ Add a Custom Add-in link.
42 | ![+ Add a Custom Add-in](https://learn.microsoft.com/en-us/office/dev/add-ins/images/outlook-sideload-custom-add-in.png "Image Title") 43 | - In the older version of Outlook web you may have to go to Add-in management is available under “Settings” > “Manage Add-Ins” > “Custom Add-ins” > “My Add-ins” > “+ Add a Custom Add-in” 44 | - select `file` and chose your `manifest_python.xml` file, then choose `install` 45 | 46 | **Note** - You will have to change your manifest file to the hosting url later. So you will have to remove and readd the new manifest file later on. 47 | 48 | ## Deploy the sample 49 | Follow the same instructions given from the microsoft website: https://docs.microsoft.com/en-us/azure/app-service/quickstart-python?tabs=powershell&pivots=python-framework-flask#deploy-the-sample 50 | 51 | Once the sample is deployed, test your webapp's urls to make sure they work. 52 | After, you can modify the `manifest_python.xml` to route all `https://localhost:5000` urls to your own hosted url. 53 | -------------------------------------------------------------------------------- /devcerts/generate.py: -------------------------------------------------------------------------------- 1 | from cryptography import x509 2 | from cryptography.hazmat.primitives import hashes 3 | from cryptography.hazmat.primitives.asymmetric import rsa 4 | from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption 5 | from cryptography.x509.oid import NameOID 6 | import datetime 7 | import os 8 | from pathlib import Path 9 | import devcerts.defaults as defaults 10 | 11 | def generate_private_key(): 12 | return rsa.generate_private_key(public_exponent=65537, key_size=2048) 13 | 14 | def generate_ca_certificate(private_key): 15 | subject = issuer = x509.Name([ 16 | x509.NameAttribute(NameOID.COUNTRY_NAME, defaults.country_code), 17 | x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, defaults.state), 18 | x509.NameAttribute(NameOID.LOCALITY_NAME, defaults.locality), 19 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Developer CA for Microsoft Office Add-ins"), 20 | x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), 21 | ]) 22 | 23 | cert = ( 24 | x509.CertificateBuilder() 25 | .subject_name(subject) 26 | .issuer_name(issuer) 27 | .public_key(private_key.public_key()) 28 | .serial_number(x509.random_serial_number()) 29 | .not_valid_before(datetime.datetime.utcnow()) 30 | .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=defaults.days_until_certificate_expires)) 31 | .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) 32 | .sign(private_key, hashes.SHA256()) 33 | ) 34 | 35 | return cert 36 | 37 | def generate_localhost_certificate(private_key, ca_cert, ca_key): 38 | subject = x509.Name([ 39 | x509.NameAttribute(NameOID.COUNTRY_NAME, defaults.country_code), 40 | x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, defaults.state), 41 | x509.NameAttribute(NameOID.LOCALITY_NAME, defaults.locality), 42 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, "localhost"), 43 | x509.NameAttribute(NameOID.COMMON_NAME, "localhost"), 44 | ]) 45 | 46 | cert = ( 47 | x509.CertificateBuilder() 48 | .subject_name(subject) 49 | .issuer_name(ca_cert.subject) 50 | .public_key(private_key.public_key()) 51 | .serial_number(x509.random_serial_number()) 52 | .not_valid_before(datetime.datetime.utcnow()) 53 | .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=defaults.days_until_certificate_expires)) 54 | .add_extension(x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False) 55 | .sign(ca_key, hashes.SHA256()) 56 | ) 57 | 58 | return cert 59 | 60 | def save_certificate(cert, filename): 61 | with open(filename, "wb") as f: 62 | f.write(cert.public_bytes(Encoding.PEM)) 63 | 64 | def save_private_key(key, filename): 65 | with open(filename, "wb") as f: 66 | f.write(key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())) 67 | 68 | def generate_certificates(): 69 | ca_key = generate_private_key() 70 | ca_cert = generate_ca_certificate(ca_key) 71 | 72 | localhost_key = generate_private_key() 73 | localhost_cert = generate_localhost_certificate(localhost_key, ca_cert, ca_key) 74 | 75 | # Ensure the certificate directory exists 76 | Path(defaults.certificate_directory).mkdir(parents=True, exist_ok=True) 77 | 78 | # Save the CA certificate and key 79 | save_certificate(ca_cert, defaults.ca_certificate_path) 80 | save_private_key(ca_key, defaults.ca_certificate_path.replace(".crt", ".key")) 81 | 82 | # Save the localhost certificate and key 83 | save_certificate(localhost_cert, defaults.localhost_certificate_path) 84 | save_private_key(localhost_key, defaults.localhost_key_path) 85 | 86 | if __name__ == "__main__": 87 | generate_certificates() 88 | -------------------------------------------------------------------------------- /manifest_python.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 05c2e1c9-3e1d-406e-9a91-e9ac64854143 7 | 1.0.0.2 8 | PythonicIT 9 | en-US 10 | 11 | 12 | 13 | 14 | 15 | 16 | https://www.pythonicit.com 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 250 31 | 32 |
33 |
34 | ReadWriteItem 35 | 36 | 37 | 38 | false 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |