From a27d3e6cfd1fdeaa8c7ed4c895a9308bd912bc7d Mon Sep 17 00:00:00 2001 From: vtsanev Date: Fri, 6 Mar 2026 16:50:06 +0200 Subject: [PATCH 01/17] outlook initial commit --- .editorconfig | 18 ++ .gitignore | 3 + .node-version | 1 + README.md | 2 - outlook/.env.example | 122 +++++++++++++ outlook/.gitignore | 2 + outlook/gateway-server.config.json | 38 ++++ outlook/package.json | 55 ++++++ outlook/src/App.css | 264 +++++++++++++++++++++++++++ outlook/src/App.tsx | 113 ++++++++++++ outlook/src/OutlookApp.tsx | 93 ++++++++++ outlook/src/components/EmailForm.tsx | 131 +++++++++++++ outlook/src/config.json | 7 + outlook/src/index.css | 5 + outlook/src/index.html | 13 ++ outlook/src/main.tsx | 10 + outlook/src/outlook-main.tsx | 14 ++ outlook/src/outlook.html | 13 ++ outlook/src/vite-env.d.ts | 12 ++ outlook/tsconfig.json | 36 ++++ outlook/vite.config.ts | 24 +++ readme.md | 53 ++++++ 22 files changed, 1027 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .node-version delete mode 100644 README.md create mode 100644 outlook/.env.example create mode 100644 outlook/.gitignore create mode 100644 outlook/gateway-server.config.json create mode 100644 outlook/package.json create mode 100644 outlook/src/App.css create mode 100644 outlook/src/App.tsx create mode 100644 outlook/src/OutlookApp.tsx create mode 100644 outlook/src/components/EmailForm.tsx create mode 100644 outlook/src/config.json create mode 100644 outlook/src/index.css create mode 100644 outlook/src/index.html create mode 100644 outlook/src/main.tsx create mode 100644 outlook/src/outlook-main.tsx create mode 100644 outlook/src/outlook.html create mode 100644 outlook/src/vite-env.d.ts create mode 100644 outlook/tsconfig.json create mode 100644 outlook/vite.config.ts create mode 100644 readme.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..54e4783 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.json] +indent_size = 2 + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..490bca4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +.env +.secrets/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/README.md b/README.md deleted file mode 100644 index 8fc2084..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# bridge-examples -io.Bridge Examples diff --git a/outlook/.env.example b/outlook/.env.example new file mode 100644 index 0000000..38d02a6 --- /dev/null +++ b/outlook/.env.example @@ -0,0 +1,122 @@ +## This is an example .env file for io.Bridge +## Copy this file as .env and fill in the required values + +############################################################################### +## License key for io.Bridge ## +############################################################################### + +## License key for io.Bridge (Required). Specify it as a string. +IO_BRIDGE_LICENSE_KEY= +## Alternatively, you can specify the path to a file containing the license key. +#IO_BRIDGE_LICENSE_KEY_FILE=./.secrets/io-bridge-license.key + +############################################################################### +## Server ## +############################################################################### + +## The PORT for io.Bridge server to bind on (Optional, default is 8084) +#IO_BRIDGE_SERVER_PORT=8385 + +## The host for io.Bridge server to bind on (Optional, default is '0.0.0.0' i.e. any local IP) +#IO_BRIDGE_SERVER_HOST=127.0.0.1 + +## The path to file where the resolved io.Bridge server is bound (Optional) +#IO_BRIDGE_PRINT_PORT=./io-bridge-address.json + +############################################################################### +## Auth ## +############################################################################### + +## Authentication type. For example 'none', 'basic' or 'oauth2' +#IO_BRIDGE_SERVER_AUTH_TYPE=oauth2 + +## Basic Authentication Realm +IO_BRIDGE_SERVER_AUTH_BASIC_REALM=interop.io + +## +#IO_BRIDGE_SERVER_AUTH_OAUTH2_JWT_ISSUERURI= + +## +IO_BRIDGE_SERVER_AUTH_OAUTH2_JWT_AUDIENCE= + +############################################################################### +## Mesh ## +############################################################################### + +## The ping interval for mesh (Optional, default is 30s) +#IO_BRIDGE_MESH_PING_INTERVAL=30s + +############################################################################### +## Gateway ## +############################################################################### + +## If you want to embed gateway (Optional, default is false) +#IO_BRIDGE_GATEWAY_ENABLED=true + +############################################################################### +## CORS ## +############################################################################### + +## Allow origins for CORS (Optional, default is *) +## The special value * allows all origins. +## You can also specify multiple origins separated by commas. +## You can use regular expressions to match origins, for example: +## - http:\/\/localhost(:d+)? matches http://localhost, http://localhost:8080, etc. +## - file:\/\/.* matches any file URL +## - null is a way to support specific 'null' origin set by the client, for example when using about:blank in a browser +#IO_BRIDGE_SERVER_CORS_ALLOW_ORIGIN=/http:\/\/localhost(:d+)?/,/file:\/\/.*/,null + +## Allow methods for CORS (Optional, default is GET,HEAD). The special value * allows all methods. +#IO_BRIDGE_SERVER_CORS_ALLOW_METHODS=GET,POST,DELETE + +## Which headers a pre-flight request can list as allowed for use during an actual request. (Optional, default is *) +#IO_BRIDGE_SERVER_CORS_ALLOW_HEADERS=* + +## Which headers that an actual response might have and can be exposed to the clients (Optional) +#IO_BRIDGE_SERVER_CORS_EXPOSE_HEADERS=X-Authorization + +## How long (in seconds) response from a pre-flight request can be cached by clients (Optional, default is 3600 i.e. 1 hour) +#IO_BRIDGE_SERVER_CORS_MAX_AGE=3600 + +## If you want to allow credentials in CORS (Optional, default is false), cannot be true if allow origins is set to * +## Setting this to true will impact how allow origins, methods and headers are processed +#IO_BRIDGE_SERVER_CORS_ALLOW_CREDENTIALS=true + +## If you want to allow private network access (PNA) in CORS (Optional, default is false), cannot be true if allow origins is set to * +#IO_BRIDGE_SERVER_CORS_ALLOW_PRIVATE_NETWORK=true + +## If you want to completely disable CORS handling (Optional, default is false) +#IO_BRIDGE_SERVER_CORS_DISABLED=true + +############################################################################### +## Logging +############################################################################### + +## This is the logging layout to use. The default is 'ecs' which is the Elastic Common Schema. +## Other options are 'logstash' +LOGGING_LAYOUT=ecs + +## The logging level to use. The default is 'info'. +## Supported levels are: trace, debug, info, warn, error and off +#LOGGING_LEVEL=debug + +#LOGGING_LEVEL_GATEWAY_BRIDGE=debug +#LOGGING_LEVEL_GATEWAY_SERVER=info +#LOGGING_LEVEL_GATEWAY_SERVER_HTTP=debug + +LOGGING_LEVEL_GATEWAY=info + +############################################################################### +## Web App Configuration (Vite) +## Variables prefixed with VITE_ are exposed to the browser (required by Vite) +############################################################################### + +## License key for io.Connect Browser Platform (Required for web app) +## Note: This is a DIFFERENT license than IO_BRIDGE_LICENSE_KEY above. +## - IO_BRIDGE_LICENSE_KEY is for the io.Bridge server (Node.js) +## - VITE_IO_CB_LICENSE_KEY is for the browser platform (client-side) +VITE_IO_CB_LICENSE_KEY= + +## io.Bridge URL for browser platform connection (Optional, default is https://localhost:8084) +#VITE_IO_CB_BRIDGE_URL=https://localhost:8084 + diff --git a/outlook/.gitignore b/outlook/.gitignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/outlook/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/outlook/gateway-server.config.json b/outlook/gateway-server.config.json new file mode 100644 index 0000000..8c91363 --- /dev/null +++ b/outlook/gateway-server.config.json @@ -0,0 +1,38 @@ +{ + "port": 8385, + "host": "127.0.0.1", + "cors": false, + "auth": { + "type": "none" + }, + "gateway": { + "methods": { + "visibility": [ + { + "identity": { + "application": "Outlook" + }, + "restrictions": "cluster" + }, + {"restrictions": "local"} + ] + }, + "peers": { + "visibility": [ + { + "domain": "agm", + "restrictions": "cluster" + } + ] + }, + "mesh": { + "auth": { + "user": null + }, + "cluster": { + "endpoint": "http://localhost:8084" + }, + "enabled": true + } + } +} diff --git a/outlook/package.json b/outlook/package.json new file mode 100644 index 0000000..1e7e4b5 --- /dev/null +++ b/outlook/package.json @@ -0,0 +1,55 @@ +{ + "name": "@interopio/bridge-example-outlook", + "private": true, + "description": "io.Bridge example demonstrating Outlook integration from a web app", + "type": "module", + "scripts": { + "build": "tsc && vite build", + "clean": "rimraf dist", + "start:bridge": "npx @interopio/bridge --server-port 8084", + "start:gateway": "npx @interopio/gateway-server run", + "start:web": "vite", + "start": "concurrently \"npm:start:bridge\" \"npm:start:gateway\" \"npm:start:web\"", + "start:no-gateway": "concurrently \"npm:start:bridge\" \"npm:start:web\"", + "dev": "npm run start:web", + "preview": "vite preview" + }, + "keywords": [ + "io.bridge", + "outlook", + "gateway", + "interop", + "example" + ], + "author": "InteropIO", + "license": "MIT", + "dependencies": { + "@interopio/browser-platform": "file:../../connect-js/packages/browser-platform", + "@interopio/workspaces-api": "file:../../connect-js/packages/workspaces-api", + "@interopio/browser": "file:../../connect-js/packages/browser", + "@interopio/search-api": "file:../../connect-js/packages/search-api", + "@interopio/home-ui-react": "file:../../connect-js/packages/home-ui-react", + "@interopio/workspaces-ui-react": "file:../../connect-js/packages/workspaces-ui-react", + "@interopio/react-hooks": "file:../../connect-js/packages/react-hooks", + "@interopio/core": "file:../../connect-js/packages/core", + "@interopio/desktop": "file:../../desktop/packages/desktop-client", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@interopio/bridge": "file:../../bridge/packages/bridge", + "@interopio/gateway-server": "file:../../gateway/packages/gateway-server", + "@interopio/gateway": "file:../../gateway/packages/gateway", + "@types/node": "^20.11.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@vitejs/plugin-react": "^4.2.0", + "concurrently": "^8.2.0", + "rimraf": "^5.0.0", + "typescript": "^5.3.0", + "vite": "^5.0.0" + }, + "engines": { + "node": ">=24.0.0" + } +} diff --git a/outlook/src/App.css b/outlook/src/App.css new file mode 100644 index 0000000..0ae7210 --- /dev/null +++ b/outlook/src/App.css @@ -0,0 +1,264 @@ +:root { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.5; + font-weight: 400; + color: #213547; + background-color: #f5f5f5; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + margin: 0; + display: flex; + min-width: 320px; + min-height: 100vh; +} + +#root { + width: 100%; +} + +.app { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +header { + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 2px solid #e0e0e0; +} + +header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #0078d4; +} + +.status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; + color: #666; +} + +.indicator { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; +} + +.indicator.connected { + background-color: #28a745; + box-shadow: 0 0 4px #28a745; +} + +.indicator.disconnected { + background-color: #dc3545; +} + +main { + flex: 1; +} + +footer { + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid #e0e0e0; + text-align: center; + color: #666; + font-size: 0.9rem; +} + +.app.loading, +.app.error { + display: flex; + justify-content: center; + align-items: center; + text-align: center; +} + +.app.error h1 { + color: #dc3545; + margin-bottom: 1rem; +} + +.app.error button { + margin-top: 1rem; + padding: 0.5rem 1rem; + background-color: #0078d4; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; +} + +.app.error button:hover { + background-color: #005a9e; +} + +button { + font-family: inherit; +} + +/* Outlook Child App Styles */ +.outlook-container { + max-width: 800px; + margin: 0 auto; + padding: 2rem; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.outlook-header { + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 2px solid #e0e0e0; +} + +.outlook-header h1 { + font-size: 1.75rem; + margin-bottom: 0.5rem; + color: #0078d4; +} + +.outlook-header p { + color: #666; + font-size: 0.95rem; +} + +.outlook-main { + flex: 1; +} + +.outlook-container .loading { + display: flex; + justify-content: center; + align-items: center; + height: 200px; + font-size: 1.1rem; + color: #666; +} + +.outlook-container .error { + background-color: #fff5f5; + border: 1px solid #dc3545; + border-radius: 8px; + padding: 1.5rem; + color: #dc3545; +} + +.outlook-container .error h2 { + margin-bottom: 0.5rem; +} + +/* Email Form Styles */ +.email-form { + background: white; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.form-group { + margin-bottom: 1.25rem; +} + +.form-group label { + display: block; + font-weight: 500; + margin-bottom: 0.5rem; + color: #333; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; + font-family: inherit; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: #0078d4; + box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.15); +} + +.form-group input:disabled, +.form-group textarea:disabled { + background-color: #f5f5f5; + cursor: not-allowed; +} + +.form-group textarea { + resize: vertical; + min-height: 150px; +} + +.form-actions { + margin-top: 1.5rem; +} + +.form-actions button { + width: 100%; + padding: 0.875rem 1.5rem; + background-color: #0078d4; + color: white; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.form-actions button:hover:not(:disabled) { + background-color: #005a9e; +} + +.form-actions button:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.form-actions button.success { + background-color: #28a745; +} + +.error-message { + margin-top: 1rem; + padding: 0.75rem 1rem; + background-color: #fff5f5; + border: 1px solid #dc3545; + border-radius: 4px; + color: #dc3545; + font-size: 0.9rem; +} + +.success-message { + margin-top: 1rem; + padding: 0.75rem 1rem; + background-color: #f0fff4; + border: 1px solid #28a745; + border-radius: 4px; + color: #28a745; + font-size: 0.9rem; +} diff --git a/outlook/src/App.tsx b/outlook/src/App.tsx new file mode 100644 index 0000000..57daf85 --- /dev/null +++ b/outlook/src/App.tsx @@ -0,0 +1,113 @@ +import { IOConnectBrowser } from "@interopio/browser"; +import IOBrowserPlatform from "@interopio/browser-platform"; +import { IOConnectHome, UserData } from "@interopio/home-ui-react"; +import { IOConnectInitSettings } from "@interopio/react-hooks"; +import IOWorkspaces from "@interopio/workspaces-api"; +import "@interopio/home-ui-react/index.css"; +import "@interopio/workspaces-ui-react/dist/styles/workspaces.css"; +import './App.css'; +import config from "./config.json"; + +const getConfig = (userData: UserData): IOConnectInitSettings => { + const bridgeUrl = import.meta.env.VITE_IO_CB_BRIDGE_URL || 'http://localhost:8084'; + const licenseKey = import.meta.env.VITE_IO_CB_LICENSE_KEY || ''; + + // Extract user details from login + const user = userData.user; + + // Platform configuration + return { + browserPlatform: { + factory: async (platformConfig) => { + const platformInit = await IOBrowserPlatform(platformConfig); + + (window as any).io = platformInit.io; + (window as any).platform = platformInit.platform; + + return platformInit; + }, + config: { + // License key from environment + licenseKey, + environment: config.environment, + applications: { + local: [ + { + name: "outlook-demo", + type: "window", + title: "Outlook Demo", + details: { + url: `${window.location.href}outlook.html`, + }, + customProperties: { + includeInWorkspaces: true + } + } + ] + }, + layouts: { + mode: "idb" + }, + channels: { + definitions: config.channels + }, + browser: { + libraries: [IOWorkspaces], + systemLogger: { + level: "debug" + }, + intentResolver: { + enable: true + } + }, + + // Gateway configuration - connect to local io.Bridge + gateway: { + + bridge: { + url: bridgeUrl, + + interop: { + enabled: true, + visibility: [ + { restrictions: 'cluster', method: /.*/ } + ] + } + } + }, + // User details required when connecting to io.Bridge + user: { + id: user?.id || 'anonymous', + username: user?.username || 'anonymous' + }, + // Workspaces App configuration + workspaces: { + src: "/", + isFrame: true + } + } + } + }; +}; + +// Configuration for the IOConnectHome component +const homeConfig = { + getIOConnectConfig: getConfig, + // Simple login - no authentication required for this example + login: { + type: "simple" as const, + onLogin: async (username: string, _password: string) => { + // For this example, accept any user + return { + id: username, + username + }; + } + } +}; + +function App() { + return ; +} + +export default App; diff --git a/outlook/src/OutlookApp.tsx b/outlook/src/OutlookApp.tsx new file mode 100644 index 0000000..af2b78c --- /dev/null +++ b/outlook/src/OutlookApp.tsx @@ -0,0 +1,93 @@ +import { useContext, useState, useEffect } from 'react'; +import { IOConnectContext } from '@interopio/react-hooks'; +import EmailForm from './components/EmailForm'; +import './App.css'; + +function getBrowserInfo(): string { + const ua = navigator.userAgent; + let browser = 'Unknown Browser'; + let version = ''; + + if (ua.includes('Firefox/')) { + browser = 'Firefox'; + version = ua.match(/Firefox\/([\d.]+)/)?.[1] || ''; + } else if (ua.includes('Edg/')) { + browser = 'Microsoft Edge'; + version = ua.match(/Edg\/([\d.]+)/)?.[1] || ''; + } else if (ua.includes('Chrome/')) { + browser = 'Chrome'; + version = ua.match(/Chrome\/([\d.]+)/)?.[1] || ''; + } else if (ua.includes('Safari/')) { + browser = 'Safari'; + version = ua.match(/Version\/([\d.]+)/)?.[1] || ''; + } + + return `${browser} ${version}`; +} + +function OutlookApp() { + // io is already initialized by the platform, get it from context + const io = useContext(IOConnectContext); + const [initialBody, setInitialBody] = useState(null); + + useEffect(() => { + if (!io) return; + + const loadProfileData = async () => { + const browserInfo = getBrowserInfo(); + + try { + // system.getProfileData is available in Browser platform + const system = (io as any).system; + if (system && typeof system.getProfileData === 'function') { + const profileData = await system.getProfileData(); + const platformVersion = profileData.productsInfo?.platform?.apiVersion || io.version; + + setInitialBody( + `This message is prepared from ${browserInfo} using io.Connect ${platformVersion} via Outlook.` + ); + } else { + // Fallback for Desktop or older versions + setInitialBody( + `This message is prepared from ${browserInfo} using io.Connect ${io.version} via Outlook.` + ); + } + } catch (err) { + // Fallback to io.version if getProfileData fails + setInitialBody( + `This message is prepared from ${browserInfo} using io.Connect ${io.version} via Outlook.` + ); + } + }; + + loadProfileData(); + }, [io]); + + if (!io || initialBody === null) { + return ( +
+
Connecting to io.Connect...
+
+ ); + } + + + return ( +
+
+

πŸ“§ Outlook Email Demo

+

Send emails via io.Bridge to Outlook

+
+
+ +
+
+ ); +} + +export default OutlookApp; diff --git a/outlook/src/components/EmailForm.tsx b/outlook/src/components/EmailForm.tsx new file mode 100644 index 0000000..c6c1c67 --- /dev/null +++ b/outlook/src/components/EmailForm.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; +import type { IOConnectBrowser } from '@interopio/browser'; +import type { IOConnectDesktop } from '@interopio/desktop'; + +interface EmailFormProps { + io: IOConnectBrowser.API | IOConnectDesktop.API; + initialSubject?: string; + initialBody?: string; +} + +interface EmailData { + to: string; + subject: string; + body: string; +} + +type SendStatus = 'idle' | 'sending' | 'success' | 'error'; + +export default function EmailForm({ io, initialSubject = '', initialBody = '' }: EmailFormProps) { + const [email, setEmail] = useState({ + to: '', + subject: initialSubject, + body: initialBody + }); + const [status, setStatus] = useState('idle'); + const [errorMessage, setErrorMessage] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setStatus('sending'); + setErrorMessage(''); + + try { + // Invoke the Outlook add-on's email method via interop + // T42.SendEmail accepts: To, Subject, Body, HTMLBody, Cc, Bcc, AttachFiles, SendFrom + await io.interop.invoke('T42.SendEmail', { + To: [email.to], + Subject: email.subject, + Body: email.body + }); + + setStatus('success'); + // Reset form after success + setTimeout(() => { + setEmail({ to: '', subject: initialSubject, body: initialBody }); + setStatus('idle'); + }, 3000); + } catch (err) { + setStatus('error'); + setErrorMessage(err instanceof Error ? err.message : 'Unknown error occurred'); + } + }; + + const handleChange = (field: keyof EmailData) => ( + e: React.ChangeEvent + ) => { + setEmail(prev => ({ ...prev, [field]: e.target.value })); + if (status === 'error') { + setStatus('idle'); + setErrorMessage(''); + } + }; + + return ( +
+
+ + +
+ +
+ + +
+ +
+ +