wizards 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/README.md +170 -0
- package/apps/runtime/dist/agents/architect.js +453 -0
- package/apps/runtime/dist/agents/project-assistant.js +354 -0
- package/apps/runtime/dist/agents/subscription.js +206 -0
- package/apps/runtime/dist/agents/thought.js +21 -0
- package/apps/runtime/dist/agents/website.js +197 -0
- package/apps/runtime/dist/app.js +153 -0
- package/apps/runtime/dist/auth/account.js +121 -0
- package/apps/runtime/dist/auth/index.js +329 -0
- package/apps/runtime/dist/auth/keys.js +55 -0
- package/apps/runtime/dist/auth/local-ticket.js +23 -0
- package/apps/runtime/dist/authoring/example.js +73 -0
- package/apps/runtime/dist/authoring/guide.js +259 -0
- package/apps/runtime/dist/authoring/ops.js +117 -0
- package/apps/runtime/dist/cli/autostart.js +134 -0
- package/apps/runtime/dist/cli/clients.js +44 -0
- package/apps/runtime/dist/cli/environment.js +81 -0
- package/apps/runtime/dist/cli/exec.js +45 -0
- package/apps/runtime/dist/cli/home.js +74 -0
- package/apps/runtime/dist/cli/machine.js +145 -0
- package/apps/runtime/dist/cli/main.js +125 -0
- package/apps/runtime/dist/cli/setup.js +273 -0
- package/apps/runtime/dist/cli/start.js +190 -0
- package/apps/runtime/dist/cli/status.js +120 -0
- package/apps/runtime/dist/cli/ui.js +12 -0
- package/apps/runtime/dist/cli/update.js +58 -0
- package/apps/runtime/dist/connectors/builtin.js +70 -0
- package/apps/runtime/dist/connectors/external.js +400 -0
- package/apps/runtime/dist/connectors/gmail.js +107 -0
- package/apps/runtime/dist/connectors/imap.js +217 -0
- package/apps/runtime/dist/connectors/index.js +266 -0
- package/apps/runtime/dist/connectors/mail.js +72 -0
- package/apps/runtime/dist/connectors/outlook.js +90 -0
- package/apps/runtime/dist/credits/credits.js +103 -0
- package/apps/runtime/dist/credits/estimate.js +132 -0
- package/apps/runtime/dist/db/adopt.js +149 -0
- package/apps/runtime/dist/db/client.js +146 -0
- package/apps/runtime/dist/db/control-schema.js +70 -0
- package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
- package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
- package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
- package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
- package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
- package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
- package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
- package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
- package/apps/runtime/dist/db/migrations/control/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
- package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
- package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/schema.js +278 -0
- package/apps/runtime/dist/documents/parse.js +230 -0
- package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
- package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
- package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
- package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
- package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
- package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
- package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
- package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
- package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
- package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +359 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
- package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
- package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
- package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
- package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
- package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
- package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +464 -0
- package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
- package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
- package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
- package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
- package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
- package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
- package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
- package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
- package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
- package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
- package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
- package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
- package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
- package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
- package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
- package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
- package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
- package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
- package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
- package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
- package/apps/runtime/dist/engenty/shims/web-ingest.js +3 -0
- package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
- package/apps/runtime/dist/engine/asks.js +60 -0
- package/apps/runtime/dist/engine/events.js +46 -0
- package/apps/runtime/dist/engine/input.js +170 -0
- package/apps/runtime/dist/engine/prepare.js +51 -0
- package/apps/runtime/dist/engine/requirements.js +124 -0
- package/apps/runtime/dist/engine/resources.js +89 -0
- package/apps/runtime/dist/engine/runner.js +560 -0
- package/apps/runtime/dist/engine/steps.js +598 -0
- package/apps/runtime/dist/engine/template.js +183 -0
- package/apps/runtime/dist/engine/types.js +3 -0
- package/apps/runtime/dist/env.js +224 -0
- package/apps/runtime/dist/files/blobs.js +22 -0
- package/apps/runtime/dist/files/objects.js +86 -0
- package/apps/runtime/dist/files/storage.js +95 -0
- package/apps/runtime/dist/geocode.js +58 -0
- package/apps/runtime/dist/harness/claude.js +138 -0
- package/apps/runtime/dist/harness/codex.js +228 -0
- package/apps/runtime/dist/harness/cursor.js +93 -0
- package/apps/runtime/dist/harness/env.js +124 -0
- package/apps/runtime/dist/harness/gemini.js +126 -0
- package/apps/runtime/dist/harness/index.js +99 -0
- package/apps/runtime/dist/harness/model.js +267 -0
- package/apps/runtime/dist/harness/prompt.js +121 -0
- package/apps/runtime/dist/harness/terminal.js +193 -0
- package/apps/runtime/dist/harness/types.js +7 -0
- package/apps/runtime/dist/index.js +76 -0
- package/apps/runtime/dist/limits.js +57 -0
- package/apps/runtime/dist/link-preview.js +59 -0
- package/apps/runtime/dist/manage.js +151 -0
- package/apps/runtime/dist/mcp/auth.js +102 -0
- package/apps/runtime/dist/mcp/bridge.js +59 -0
- package/apps/runtime/dist/mcp/handler.js +42 -0
- package/apps/runtime/dist/mcp/scopes.js +5 -0
- package/apps/runtime/dist/mcp/tools.js +401 -0
- package/apps/runtime/dist/media/ffmpeg.js +121 -0
- package/apps/runtime/dist/media/generate.js +120 -0
- package/apps/runtime/dist/media/marking.js +121 -0
- package/apps/runtime/dist/media/transcribe.js +30 -0
- package/apps/runtime/dist/model-errors.js +3 -0
- package/apps/runtime/dist/models.js +489 -0
- package/apps/runtime/dist/plugin.js +61 -0
- package/apps/runtime/dist/render/chromium.js +85 -0
- package/apps/runtime/dist/render/convert.js +161 -0
- package/apps/runtime/dist/render/downloads.js +277 -0
- package/apps/runtime/dist/render/guard.js +25 -0
- package/apps/runtime/dist/routes/api.js +74 -0
- package/apps/runtime/dist/routes/delivery.js +52 -0
- package/apps/runtime/dist/routes/internal.js +49 -0
- package/apps/runtime/dist/routes/marketplace.js +36 -0
- package/apps/runtime/dist/routes/runs.js +805 -0
- package/apps/runtime/dist/routes/studio.js +702 -0
- package/apps/runtime/dist/routes/wizard-stream.js +30 -0
- package/apps/runtime/dist/running.js +39 -0
- package/apps/runtime/dist/sandbox/agentos.js +191 -0
- package/apps/runtime/dist/sandbox/docker.js +77 -0
- package/apps/runtime/dist/sandbox/index.js +30 -0
- package/apps/runtime/dist/sandbox/types.js +1 -0
- package/apps/runtime/dist/secrets/crypto.js +28 -0
- package/apps/runtime/dist/secrets/signing.js +34 -0
- package/apps/runtime/dist/secrets/vault.js +59 -0
- package/apps/runtime/dist/services/architect.js +92 -0
- package/apps/runtime/dist/services/brand.js +20 -0
- package/apps/runtime/dist/services/cloud.js +52 -0
- package/apps/runtime/dist/services/draft-events.js +27 -0
- package/apps/runtime/dist/services/errors.js +24 -0
- package/apps/runtime/dist/services/files.js +129 -0
- package/apps/runtime/dist/services/links.js +16 -0
- package/apps/runtime/dist/services/marketplace.js +292 -0
- package/apps/runtime/dist/services/package.js +202 -0
- package/apps/runtime/dist/services/profile.js +23 -0
- package/apps/runtime/dist/services/project-files.js +266 -0
- package/apps/runtime/dist/services/project-index.js +109 -0
- package/apps/runtime/dist/services/projects.js +181 -0
- package/apps/runtime/dist/services/runs.js +224 -0
- package/apps/runtime/dist/services/shares.js +129 -0
- package/apps/runtime/dist/services/widgets.js +34 -0
- package/apps/runtime/dist/services/wizards.js +342 -0
- package/apps/runtime/dist/settings.js +16 -0
- package/apps/runtime/dist/store/index.js +279 -0
- package/apps/runtime/dist/tenants/control.js +106 -0
- package/apps/runtime/dist/tenants/tenant.js +26 -0
- package/apps/runtime/dist/tools/browser.js +380 -0
- package/apps/runtime/dist/tools/connector.js +204 -0
- package/apps/runtime/dist/tools/index.js +234 -0
- package/apps/runtime/dist/tools/mail.js +104 -0
- package/apps/runtime/dist/tools/net-guard.js +59 -0
- package/apps/runtime/dist/tools/project.js +53 -0
- package/apps/runtime/dist/tools/shared.js +42 -0
- package/apps/runtime/dist/tools/store.js +238 -0
- package/apps/runtime/dist/update.js +118 -0
- package/apps/runtime/dist/widgets/bundle.js +99 -0
- package/apps/runtime/dist/widgets/render.js +312 -0
- package/apps/runtime/dist/widgets/runtime.js +84 -0
- package/apps/runtime/dist/widgets/script.js +30 -0
- package/apps/runtime/dist/widgets/step.js +192 -0
- package/apps/web/dist/ai-labels/ai-black.svg +20 -0
- package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
- package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
- package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
- package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
- package/apps/web/dist/ai-labels/ai-white.svg +21 -0
- package/apps/web/dist/assets/ChangelogDialog-O00vEnbm.js +1 -0
- package/apps/web/dist/assets/ChatPanel-CH9ry0Hp.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-DRzecwUw.js +1 -0
- package/apps/web/dist/assets/EditorPage-BRnBf02Q.js +2 -0
- package/apps/web/dist/assets/FlowPreview-DDOj8IAH.js +1 -0
- package/apps/web/dist/assets/HomePage-CAGTTG3N.js +1 -0
- package/apps/web/dist/assets/ImportWizard-0RRrAnX3.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-BrP-ENHg.css +1 -0
- package/apps/web/dist/assets/LocalRuntime-C-TBYrzs.js +16 -0
- package/apps/web/dist/assets/NewWizardPage-kI90h3_9.js +2 -0
- package/apps/web/dist/assets/PublicRunner-CKRDeIVd.js +1 -0
- package/apps/web/dist/assets/RunnerView-Rc4H8wYH.js +2 -0
- package/apps/web/dist/assets/SettingsPage-CESMuZxq.js +2 -0
- package/apps/web/dist/assets/SetupPage-D1rrYU14.js +1 -0
- package/apps/web/dist/assets/SharePage-COTAmOgh.js +1 -0
- package/apps/web/dist/assets/api-BY28cjGY.js +10 -0
- package/apps/web/dist/assets/arrow-left-UpjOBfQp.js +1 -0
- package/apps/web/dist/assets/arrow-right-CucQycEy.js +1 -0
- package/apps/web/dist/assets/changelog-p3wsaDDS.js +1 -0
- package/apps/web/dist/assets/check-Bjx41xbB.js +1 -0
- package/apps/web/dist/assets/createLucideIcon-DBu2Ovxj.js +1 -0
- package/apps/web/dist/assets/definition-cy7uvY5L.js +55 -0
- package/apps/web/dist/assets/engenty-CWIFpgKq.js +1 -0
- package/apps/web/dist/assets/esm-D1OEhjTK.js +7 -0
- package/apps/web/dist/assets/external-link-CRg6DVdO.js +1 -0
- package/apps/web/dist/assets/fluffy-engenty-CJ3wCQgT.js +456 -0
- package/apps/web/dist/assets/index-CQ_q9iMO.css +1 -0
- package/apps/web/dist/assets/index-D0gxh90t.js +10 -0
- package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +1 -0
- package/apps/web/dist/assets/meta-BbSdtTUa.js +1 -0
- package/apps/web/dist/assets/mic-CeqQ89Dj.js +1 -0
- package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +1 -0
- package/apps/web/dist/assets/outputs-D6pxH6ve.js +69 -0
- package/apps/web/dist/assets/plus-HA210wXA.js +1 -0
- package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
- package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
- package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
- package/apps/web/dist/assets/type-BzYgVWKL.js +1 -0
- package/apps/web/dist/assets/ui-Ddwl35A6.js +1 -0
- package/apps/web/dist/assets/upload-DsgLfQ3U.js +1 -0
- package/apps/web/dist/assets/video-Dk1fuTDl.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-Bf97fs8T.js +1 -0
- package/apps/web/dist/embed.js +102 -0
- package/apps/web/dist/favicon.svg +1 -0
- package/apps/web/dist/icons/apple-touch-icon.png +0 -0
- package/apps/web/dist/icons/icon-192.png +0 -0
- package/apps/web/dist/icons/icon-512.png +0 -0
- package/apps/web/dist/icons/icon-maskable-512.png +0 -0
- package/apps/web/dist/index.html +46 -0
- package/apps/web/dist/install.sh +532 -0
- package/apps/web/dist/manifest.webmanifest +20 -0
- package/apps/web/dist/sw.js +15 -0
- package/bin/wizards.mjs +13 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1415 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +762 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +216 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +86 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +133 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +298 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.js +211 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +74 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.js +96 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +219 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +1 -0
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +100 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +44 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
- package/node_modules/@engenty-wizards/shared/package.json +10 -0
- package/package.json +62 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/commands/wizard.md +8 -0
- package/plugin/skills/build-wizard/SKILL.md +27 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { ImapFlow } from "imapflow";
|
|
2
|
+
import { htmlToMarkdown } from "../render/convert.js";
|
|
3
|
+
import { assertPublicUrl } from "../tools/net-guard.js";
|
|
4
|
+
import { dayAfter, mailActions, snippetOf, } from "./mail.js";
|
|
5
|
+
const KNOWN_HOSTS = {
|
|
6
|
+
"gmail.com": "imap.gmail.com",
|
|
7
|
+
"googlemail.com": "imap.gmail.com",
|
|
8
|
+
"outlook.com": "outlook.office365.com",
|
|
9
|
+
"hotmail.com": "outlook.office365.com",
|
|
10
|
+
"live.com": "outlook.office365.com",
|
|
11
|
+
"icloud.com": "imap.mail.me.com",
|
|
12
|
+
"me.com": "imap.mail.me.com",
|
|
13
|
+
"yahoo.com": "imap.mail.yahoo.com",
|
|
14
|
+
"gmx.at": "imap.gmx.net",
|
|
15
|
+
"gmx.de": "imap.gmx.net",
|
|
16
|
+
"gmx.net": "imap.gmx.net",
|
|
17
|
+
"web.de": "imap.web.de",
|
|
18
|
+
"a1.net": "securemail.a1.net",
|
|
19
|
+
"fastmail.com": "imap.fastmail.com",
|
|
20
|
+
};
|
|
21
|
+
const ALLOW_PRIVATE = process.env.ALLOW_PRIVATE_FETCH === "1";
|
|
22
|
+
async function open(credentials) {
|
|
23
|
+
const email = credentials.email.trim();
|
|
24
|
+
const domain = email.split("@")[1]?.toLowerCase() ?? "";
|
|
25
|
+
const host = credentials.host?.trim() || KNOWN_HOSTS[domain] || `imap.${domain}`;
|
|
26
|
+
const port = Number(credentials.port) || 993;
|
|
27
|
+
if (!ALLOW_PRIVATE && port !== 993 && port !== 143) {
|
|
28
|
+
throw new Error("IMAP runs on port 993 (or 143).");
|
|
29
|
+
}
|
|
30
|
+
// The host is typed by the person: it must be a public mail server, never this server's network.
|
|
31
|
+
await assertPublicUrl(`https://${host}`);
|
|
32
|
+
const client = new ImapFlow({
|
|
33
|
+
host,
|
|
34
|
+
port,
|
|
35
|
+
secure: port === 993,
|
|
36
|
+
auth: { user: email, pass: credentials.password },
|
|
37
|
+
logger: false,
|
|
38
|
+
socketTimeout: 60_000,
|
|
39
|
+
});
|
|
40
|
+
// A dropped connection must not take the process down.
|
|
41
|
+
client.on("error", () => undefined);
|
|
42
|
+
await client.connect();
|
|
43
|
+
return client;
|
|
44
|
+
}
|
|
45
|
+
/** Runs `use` on the mailbox that holds everything ("All Mail" where the server has one). */
|
|
46
|
+
async function withMailbox(ctx, use) {
|
|
47
|
+
const client = await open(JSON.parse(ctx.accessToken));
|
|
48
|
+
try {
|
|
49
|
+
const boxes = await client.list();
|
|
50
|
+
const all = boxes.find((b) => b.specialUse === "\\All");
|
|
51
|
+
const lock = await client.getMailboxLock(all?.path ?? "INBOX", { readOnly: true });
|
|
52
|
+
try {
|
|
53
|
+
return await use(client);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
lock.release();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await client.logout().catch(() => undefined);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function parts(node, out) {
|
|
64
|
+
if (!node) {
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
const name = node.dispositionParameters?.filename ?? node.parameters?.name;
|
|
68
|
+
const part = node.part ?? "1";
|
|
69
|
+
if (node.childNodes?.length) {
|
|
70
|
+
for (const child of node.childNodes) {
|
|
71
|
+
parts(child, out);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (node.disposition === "attachment" || (name && !node.type.startsWith("text/"))) {
|
|
75
|
+
out.attachments.push({
|
|
76
|
+
id: part,
|
|
77
|
+
name: name ?? `attachment-${part}`,
|
|
78
|
+
mime: node.type,
|
|
79
|
+
size: node.size ?? null,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (node.type === "text/plain" && !out.plain) {
|
|
83
|
+
out.plain = part;
|
|
84
|
+
}
|
|
85
|
+
else if (node.type === "text/html" && !out.html) {
|
|
86
|
+
out.html = part;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
const emptyParts = () => ({ attachments: [], plain: null, html: null });
|
|
91
|
+
function sender(from) {
|
|
92
|
+
const f = from?.[0];
|
|
93
|
+
return f?.address ? (f.name ? `${f.name} <${f.address}>` : f.address) : null;
|
|
94
|
+
}
|
|
95
|
+
async function bytes(client, uid, part) {
|
|
96
|
+
const { content } = await client.download(uid, part, { uid: true });
|
|
97
|
+
if (!content) {
|
|
98
|
+
throw new Error("The mail is no longer there.");
|
|
99
|
+
}
|
|
100
|
+
const chunks = [];
|
|
101
|
+
for await (const chunk of content) {
|
|
102
|
+
chunks.push(Buffer.from(chunk));
|
|
103
|
+
}
|
|
104
|
+
return Buffer.concat(chunks);
|
|
105
|
+
}
|
|
106
|
+
const backend = {
|
|
107
|
+
search: (input, ctx) => withMailbox(ctx, async (client) => {
|
|
108
|
+
const query = {};
|
|
109
|
+
if (input.since) {
|
|
110
|
+
query.since = input.since;
|
|
111
|
+
}
|
|
112
|
+
if (input.until) {
|
|
113
|
+
query.before = dayAfter(input.until);
|
|
114
|
+
}
|
|
115
|
+
if (input.from) {
|
|
116
|
+
query.from = input.from;
|
|
117
|
+
}
|
|
118
|
+
const terms = input.terms ?? [];
|
|
119
|
+
if (terms.length === 1) {
|
|
120
|
+
query.text = terms[0];
|
|
121
|
+
}
|
|
122
|
+
else if (terms.length > 1) {
|
|
123
|
+
query.or = terms.map((text) => ({ text }));
|
|
124
|
+
}
|
|
125
|
+
if (!Object.keys(query).length) {
|
|
126
|
+
query.all = true;
|
|
127
|
+
}
|
|
128
|
+
const found = (await client.search(query, { uid: true })) || [];
|
|
129
|
+
const limit = input.limit ?? 40;
|
|
130
|
+
// Without the attachment filter the newest `limit` are enough; with it, look further back.
|
|
131
|
+
const uids = found.slice(-(input.has_attachment ? limit * 4 : limit));
|
|
132
|
+
if (!uids.length) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const messages = await client.fetchAll(uids, { envelope: true, bodyStructure: true, internalDate: true }, { uid: true });
|
|
136
|
+
return messages
|
|
137
|
+
.map((m) => ({
|
|
138
|
+
id: String(m.uid),
|
|
139
|
+
date: m.internalDate ? new Date(m.internalDate).toISOString() : null,
|
|
140
|
+
from: sender(m.envelope?.from),
|
|
141
|
+
subject: m.envelope?.subject ?? null,
|
|
142
|
+
snippet: "",
|
|
143
|
+
attachments: parts(m.bodyStructure, emptyParts()).attachments,
|
|
144
|
+
}))
|
|
145
|
+
.filter((m) => !input.has_attachment || m.attachments.length > 0)
|
|
146
|
+
.reverse()
|
|
147
|
+
.slice(0, limit);
|
|
148
|
+
}),
|
|
149
|
+
read: (id, ctx) => withMailbox(ctx, async (client) => {
|
|
150
|
+
const m = await client.fetchOne(id, { envelope: true, bodyStructure: true, internalDate: true }, { uid: true });
|
|
151
|
+
if (!m) {
|
|
152
|
+
throw new Error("The mail is no longer there.");
|
|
153
|
+
}
|
|
154
|
+
const found = parts(m.bodyStructure, emptyParts());
|
|
155
|
+
const html = found.html ? (await bytes(client, id, found.html)).toString("utf8") : null;
|
|
156
|
+
const text = found.plain
|
|
157
|
+
? (await bytes(client, id, found.plain)).toString("utf8")
|
|
158
|
+
: html
|
|
159
|
+
? htmlToMarkdown(html)
|
|
160
|
+
: "";
|
|
161
|
+
return {
|
|
162
|
+
id,
|
|
163
|
+
date: m.internalDate ? new Date(m.internalDate).toISOString() : null,
|
|
164
|
+
from: sender(m.envelope?.from),
|
|
165
|
+
to: (m.envelope?.to ?? []).map((t) => t.address ?? "").filter(Boolean),
|
|
166
|
+
subject: m.envelope?.subject ?? null,
|
|
167
|
+
snippet: snippetOf(text),
|
|
168
|
+
attachments: found.attachments,
|
|
169
|
+
text,
|
|
170
|
+
html,
|
|
171
|
+
};
|
|
172
|
+
}),
|
|
173
|
+
attachment: (id, attachmentId, ctx) => withMailbox(ctx, async (client) => {
|
|
174
|
+
const m = await client.fetchOne(id, { bodyStructure: true }, { uid: true });
|
|
175
|
+
const info = m
|
|
176
|
+
? parts(m.bodyStructure, emptyParts()).attachments.find((a) => a.id === attachmentId)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (!info) {
|
|
179
|
+
throw new Error("The mail has no such attachment.");
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
name: info.name,
|
|
183
|
+
mime: info.mime,
|
|
184
|
+
data_base64: (await bytes(client, id, attachmentId)).toString("base64"),
|
|
185
|
+
};
|
|
186
|
+
}),
|
|
187
|
+
};
|
|
188
|
+
export const imapConnector = {
|
|
189
|
+
id: "imap",
|
|
190
|
+
moduleId: "wizards-mail",
|
|
191
|
+
name: "IMAP",
|
|
192
|
+
description: "Read mail in any mailbox over IMAP (GMX, iCloud, your own domain …).",
|
|
193
|
+
icon: "mail",
|
|
194
|
+
toolPrefix: "mail",
|
|
195
|
+
auth: {
|
|
196
|
+
kind: "api_key",
|
|
197
|
+
apiKey: {
|
|
198
|
+
fields: [
|
|
199
|
+
{ key: "email", label: "E-Mail-Adresse", placeholder: "name@firma.at" },
|
|
200
|
+
{ key: "password", label: "Passwort oder App-Passwort", secret: true },
|
|
201
|
+
{
|
|
202
|
+
key: "host",
|
|
203
|
+
label: "IMAP-Server",
|
|
204
|
+
placeholder: "leer lassen bei Gmail, GMX, iCloud …",
|
|
205
|
+
required: false,
|
|
206
|
+
},
|
|
207
|
+
{ key: "port", label: "Port", placeholder: "993", required: false },
|
|
208
|
+
],
|
|
209
|
+
verify: async (credentials) => {
|
|
210
|
+
const client = await open(credentials);
|
|
211
|
+
await client.logout().catch(() => undefined);
|
|
212
|
+
return { label: credentials.email.trim(), externalId: credentials.email.trim() };
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
actions: mailActions(backend, []),
|
|
217
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "../db/client.js";
|
|
3
|
+
import { buildAuthorizationUrl, createOAuth2Pkce, exchangeAuthorizationCode, refreshAccessToken, } from "../engenty/connections-sdk/oauth2.js";
|
|
4
|
+
import { scopesForGroups, } from "../engenty/connections-sdk/types.js";
|
|
5
|
+
import { env } from "../env.js";
|
|
6
|
+
import { seal, unseal } from "../secrets/crypto.js";
|
|
7
|
+
import { deleteSecret, getSecret, putSecret } from "../store/index.js";
|
|
8
|
+
import { connectorFetch, MAIL_CONNECTORS, resolveEnv, usable } from "./builtin.js";
|
|
9
|
+
import { resolveConnector } from "./external.js";
|
|
10
|
+
/** The connectors a person can pick for each kind of connection. */
|
|
11
|
+
const CONNECTORS = { mail: MAIL_CONNECTORS };
|
|
12
|
+
const HINTS = {
|
|
13
|
+
imap: "Bei Gmail, iCloud, GMX und web.de brauchst du ein App-Passwort (in den Sicherheitseinstellungen deines Kontos), nicht dein normales Passwort.",
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* What a connection asks the provider for: what reading needs, plus what each action the wizard
|
|
17
|
+
* may use needs. A wizard that only reads never asks for the right to send or delete.
|
|
18
|
+
*/
|
|
19
|
+
function scopesFor(connection, connector) {
|
|
20
|
+
const scopes = new Set(scopesForGroups(connector, new Set(["read"])));
|
|
21
|
+
for (const action of connector.actions) {
|
|
22
|
+
if (connection.actions?.includes(action.id)) {
|
|
23
|
+
for (const scope of action.providerScopes ?? []) {
|
|
24
|
+
scopes.add(scope);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return [...scopes];
|
|
29
|
+
}
|
|
30
|
+
/** Where the provider sends the person back; must be registered with the OAuth client. */
|
|
31
|
+
export function connectRedirectUri() {
|
|
32
|
+
return env.connectRedirectUrl || `${env.appUrl}/api/connect/callback`;
|
|
33
|
+
}
|
|
34
|
+
const slot = (connectionId) => `connection:${connectionId}`;
|
|
35
|
+
/** A connector that needs no credential: there is nothing for the person to connect. */
|
|
36
|
+
const open = (connector) => connector.auth.kind === "api_key" && connector.auth.apiKey.fields.length === 0;
|
|
37
|
+
/** The connectors behind a connection: the built-in ones of its kind, or the one it names. */
|
|
38
|
+
async function candidates(connection, projectId) {
|
|
39
|
+
if (connection.connector) {
|
|
40
|
+
const found = await resolveConnector(projectId, connection.connector);
|
|
41
|
+
return found ? [found.connector] : [];
|
|
42
|
+
}
|
|
43
|
+
return connection.kind ? CONNECTORS[connection.kind] : [];
|
|
44
|
+
}
|
|
45
|
+
export async function connectorOptions(connection, projectId) {
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const c of await candidates(connection, projectId)) {
|
|
48
|
+
if (!(await usable(c))) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
out.push(c.auth.kind === "api_key"
|
|
52
|
+
? {
|
|
53
|
+
id: c.id,
|
|
54
|
+
name: c.name,
|
|
55
|
+
auth: "api_key",
|
|
56
|
+
fields: c.auth.apiKey.fields,
|
|
57
|
+
hint: HINTS[c.id],
|
|
58
|
+
}
|
|
59
|
+
: { id: c.id, name: c.name, auth: "oauth2" });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
async function connectorFor(connection, connectorId, projectId) {
|
|
64
|
+
const connector = (await candidates(connection, projectId)).find((c) => c.id === connectorId);
|
|
65
|
+
if (!connector) {
|
|
66
|
+
throw new Error(`Unknown connector "${connectorId}".`);
|
|
67
|
+
}
|
|
68
|
+
return connector;
|
|
69
|
+
}
|
|
70
|
+
export async function connectionViews(connections, scope, projectId) {
|
|
71
|
+
return Promise.all(connections.map(async (c) => {
|
|
72
|
+
const stored = await getSecret(scope, slot(c.id));
|
|
73
|
+
const only = c.connector ? (await candidates(c, projectId))[0] : undefined;
|
|
74
|
+
return {
|
|
75
|
+
id: c.id,
|
|
76
|
+
kind: c.kind ?? null,
|
|
77
|
+
connector: c.connector ?? null,
|
|
78
|
+
title: c.title ?? only?.name ?? null,
|
|
79
|
+
description: c.description ?? null,
|
|
80
|
+
account: stored
|
|
81
|
+
? {
|
|
82
|
+
connector: stored.provider,
|
|
83
|
+
label: stored.label,
|
|
84
|
+
connectedAt: stored.createdAt.toISOString(),
|
|
85
|
+
}
|
|
86
|
+
: only && open(only)
|
|
87
|
+
? { connector: only.id, label: only.name, connectedAt: new Date(0).toISOString() }
|
|
88
|
+
: null,
|
|
89
|
+
connectors: await connectorOptions(c, projectId),
|
|
90
|
+
};
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
export async function startOAuth(scope, projectId, runId, connection, connectorId) {
|
|
94
|
+
const connector = await connectorFor(connection, connectorId, projectId);
|
|
95
|
+
if (connector.auth.kind !== "oauth2") {
|
|
96
|
+
throw new Error(`${connector.name} is not connected by signing in.`);
|
|
97
|
+
}
|
|
98
|
+
// Servers that hand out OAuth clients on request (most MCP servers): get one on first use.
|
|
99
|
+
await connector.auth.oauth2.registerClient?.();
|
|
100
|
+
const pkce = createOAuth2Pkce();
|
|
101
|
+
const state = {
|
|
102
|
+
scope,
|
|
103
|
+
projectId,
|
|
104
|
+
runId,
|
|
105
|
+
connectionId: connection.id,
|
|
106
|
+
connectorId,
|
|
107
|
+
verifier: pkce.codeVerifier,
|
|
108
|
+
exp: Date.now() + 15 * 60_000,
|
|
109
|
+
};
|
|
110
|
+
return buildAuthorizationUrl({
|
|
111
|
+
connector,
|
|
112
|
+
pkce,
|
|
113
|
+
redirectUri: connectRedirectUri(),
|
|
114
|
+
scopes: scopesFor(connection, connector),
|
|
115
|
+
state: seal(state),
|
|
116
|
+
resolveEnv,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** The run an OAuth callback belongs to; its tenant is looked up before the state is used. */
|
|
120
|
+
export function oauthStateRun(rawState) {
|
|
121
|
+
return unseal(rawState)?.runId ?? null;
|
|
122
|
+
}
|
|
123
|
+
export async function finishOAuth(code, rawState) {
|
|
124
|
+
const state = unseal(rawState);
|
|
125
|
+
if (!state || state.exp < Date.now()) {
|
|
126
|
+
throw new Error("Die Anmeldung ist abgelaufen. Bitte noch einmal verbinden.");
|
|
127
|
+
}
|
|
128
|
+
const run = await db.query.run.findFirst({ where: eq(schema.run.id, state.runId) });
|
|
129
|
+
const connection = run?.definition.connections?.find((c) => c.id === state.connectionId);
|
|
130
|
+
if (!connection) {
|
|
131
|
+
throw new Error("The run this sign-in belongs to is gone.");
|
|
132
|
+
}
|
|
133
|
+
const connector = await connectorFor(connection, state.connectorId, state.projectId);
|
|
134
|
+
if (connector.auth.kind !== "oauth2") {
|
|
135
|
+
throw new Error("not an oauth connector");
|
|
136
|
+
}
|
|
137
|
+
const tokens = await exchangeAuthorizationCode({
|
|
138
|
+
code,
|
|
139
|
+
codeVerifier: state.verifier,
|
|
140
|
+
config: connector.auth.oauth2,
|
|
141
|
+
redirectUri: connectRedirectUri(),
|
|
142
|
+
resolveEnv,
|
|
143
|
+
});
|
|
144
|
+
// A provider that will not name the account still connected it.
|
|
145
|
+
const account = (await connector.auth.oauth2
|
|
146
|
+
.resolveAccount?.(tokens.accessToken, fetch)
|
|
147
|
+
.catch(() => null)) ?? { label: connector.name };
|
|
148
|
+
const stored = {
|
|
149
|
+
accessToken: tokens.accessToken,
|
|
150
|
+
refreshToken: tokens.refreshToken,
|
|
151
|
+
expiresAt: tokens.expiresAt?.toISOString() ?? null,
|
|
152
|
+
scopes: tokens.grantedScopes,
|
|
153
|
+
};
|
|
154
|
+
await putSecret(state.scope, slot(connection.id), connector.id, account.label, stored);
|
|
155
|
+
return state;
|
|
156
|
+
}
|
|
157
|
+
/** Connects an `api_key` connector with what the person typed; the connector checks it first. */
|
|
158
|
+
export async function connectWithCredentials(scope, projectId, connection, connectorId, values) {
|
|
159
|
+
const connector = await connectorFor(connection, connectorId, projectId);
|
|
160
|
+
if (connector.auth.kind !== "api_key") {
|
|
161
|
+
throw new Error(`${connector.name} is connected by signing in.`);
|
|
162
|
+
}
|
|
163
|
+
const credentials = {};
|
|
164
|
+
for (const field of connector.auth.apiKey.fields) {
|
|
165
|
+
const value = String(values[field.key] ?? "").trim();
|
|
166
|
+
if (!value && field.required !== false) {
|
|
167
|
+
throw new ConnectError(`„${field.label}“ fehlt.`);
|
|
168
|
+
}
|
|
169
|
+
if (value) {
|
|
170
|
+
credentials[field.key] = value.slice(0, 500);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let account;
|
|
174
|
+
try {
|
|
175
|
+
account = await connector.auth.apiKey.verify(credentials, connectorFetch);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
throw new ConnectError(connectFailure(err));
|
|
179
|
+
}
|
|
180
|
+
const stored = {
|
|
181
|
+
accessToken: JSON.stringify(credentials),
|
|
182
|
+
refreshToken: null,
|
|
183
|
+
expiresAt: null,
|
|
184
|
+
scopes: [],
|
|
185
|
+
};
|
|
186
|
+
await putSecret(scope, slot(connection.id), connector.id, account.label, stored);
|
|
187
|
+
return account.label;
|
|
188
|
+
}
|
|
189
|
+
/** A connect attempt the person can fix (wrong password, unknown server). */
|
|
190
|
+
export class ConnectError extends Error {
|
|
191
|
+
}
|
|
192
|
+
function connectFailure(err) {
|
|
193
|
+
const e = err;
|
|
194
|
+
if (e.authenticationFailed) {
|
|
195
|
+
return "Anmeldung abgelehnt. Stimmen Adresse und (App-)Passwort?";
|
|
196
|
+
}
|
|
197
|
+
if (e.code === "ENOTFOUND" || e.code === "ECONNREFUSED" || e.code === "ETIMEDOUT") {
|
|
198
|
+
return "Der Mail-Server ist nicht erreichbar. Bitte den IMAP-Server prüfen.";
|
|
199
|
+
}
|
|
200
|
+
return `Verbinden hat nicht geklappt: ${String(e.message ?? err).slice(0, 200)}`;
|
|
201
|
+
}
|
|
202
|
+
export function disconnect(scope, connectionId) {
|
|
203
|
+
return deleteSecret(scope, slot(connectionId));
|
|
204
|
+
}
|
|
205
|
+
// --- using ---------------------------------------------------------------------
|
|
206
|
+
/**
|
|
207
|
+
* The connector and the context its actions run with, for a connection the person has
|
|
208
|
+
* connected — with a fresh access token. `null` when nothing is connected.
|
|
209
|
+
*/
|
|
210
|
+
export async function connectionContext(scope, connection, projectId) {
|
|
211
|
+
const stored = await getSecret(scope, slot(connection.id));
|
|
212
|
+
if (!stored) {
|
|
213
|
+
const only = connection.connector ? (await candidates(connection, projectId))[0] : undefined;
|
|
214
|
+
return only && open(only)
|
|
215
|
+
? { connector: only, label: only.name, ctx: actionContext(scope, connection, only, "{}", []) }
|
|
216
|
+
: null;
|
|
217
|
+
}
|
|
218
|
+
const connector = await connectorFor(connection, stored.provider, projectId);
|
|
219
|
+
let data = stored.data;
|
|
220
|
+
const expires = data.expiresAt ? Date.parse(data.expiresAt) : null;
|
|
221
|
+
if (connector.auth.kind === "oauth2" &&
|
|
222
|
+
data.refreshToken &&
|
|
223
|
+
expires !== null &&
|
|
224
|
+
expires - Date.now() < 5 * 60_000) {
|
|
225
|
+
const tokens = await refreshAccessToken({
|
|
226
|
+
config: connector.auth.oauth2,
|
|
227
|
+
refreshToken: data.refreshToken,
|
|
228
|
+
resolveEnv,
|
|
229
|
+
});
|
|
230
|
+
data = {
|
|
231
|
+
accessToken: tokens.accessToken,
|
|
232
|
+
refreshToken: tokens.refreshToken ?? data.refreshToken,
|
|
233
|
+
expiresAt: tokens.expiresAt?.toISOString() ?? null,
|
|
234
|
+
scopes: tokens.grantedScopes.length ? tokens.grantedScopes : data.scopes,
|
|
235
|
+
};
|
|
236
|
+
await putSecret(scope, slot(connection.id), connector.id, stored.label, data);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
connector,
|
|
240
|
+
label: stored.label,
|
|
241
|
+
ctx: actionContext(scope, connection, connector, data.accessToken, data.scopes, stored),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function actionContext(scope, connection, connector, accessToken, scopes, stored) {
|
|
245
|
+
return {
|
|
246
|
+
accessToken,
|
|
247
|
+
fetchImpl: connectorFetch,
|
|
248
|
+
log: () => undefined,
|
|
249
|
+
connection: {
|
|
250
|
+
id: connection.id,
|
|
251
|
+
connector_id: connector.id,
|
|
252
|
+
auth_kind: connector.auth.kind,
|
|
253
|
+
autonomous_mode: "read_only",
|
|
254
|
+
connected_by: null,
|
|
255
|
+
created_at: (stored?.createdAt ?? new Date()).toISOString(),
|
|
256
|
+
display_name: stored?.label ?? connector.name,
|
|
257
|
+
error_message: null,
|
|
258
|
+
external_account: stored?.label ?? null,
|
|
259
|
+
granted_scopes: scopes,
|
|
260
|
+
owner_user_id: null,
|
|
261
|
+
space_id: null,
|
|
262
|
+
status: "active",
|
|
263
|
+
tenant_id: scope.wizardId,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Mail as wizards use it: read-only, and the same three actions whichever provider the person
|
|
4
|
+
* connected. Every mail connector is an engenty `ConnectorDefinition` whose actions are these.
|
|
5
|
+
*/
|
|
6
|
+
export const mailSearchInput = z.object({
|
|
7
|
+
terms: z
|
|
8
|
+
.array(z.string().min(1))
|
|
9
|
+
.max(12)
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Words to look for in subject and body; a mail matches when ANY of them occurs."),
|
|
12
|
+
from: z.string().optional().describe("Sender address or domain, e.g. billing@notion.so"),
|
|
13
|
+
since: z.string().optional().describe("First day, YYYY-MM-DD (inclusive)."),
|
|
14
|
+
until: z.string().optional().describe("Last day, YYYY-MM-DD (inclusive)."),
|
|
15
|
+
has_attachment: z.boolean().optional(),
|
|
16
|
+
limit: z.number().int().min(1).max(100).optional().describe("Newest first; default 40."),
|
|
17
|
+
});
|
|
18
|
+
export const mailReadInput = z.object({ id: z.string() });
|
|
19
|
+
export const mailAttachmentInput = z.object({ id: z.string(), attachment_id: z.string() });
|
|
20
|
+
export function mailActions(backend, providerScopes) {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
id: "search",
|
|
24
|
+
group: "read",
|
|
25
|
+
summary: "Search the mailbox",
|
|
26
|
+
description: "Search the mailbox, newest first. Returns id, date, sender, subject, a snippet and the attachments of each mail.",
|
|
27
|
+
inputSchema: mailSearchInput,
|
|
28
|
+
providerScopes,
|
|
29
|
+
handler: async (input, ctx) => ({
|
|
30
|
+
mails: await backend.search(mailSearchInput.parse(input), ctx),
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "read",
|
|
35
|
+
group: "read",
|
|
36
|
+
summary: "Read one mail",
|
|
37
|
+
description: "Read one mail in full: sender, recipients, text and attachments.",
|
|
38
|
+
inputSchema: mailReadInput,
|
|
39
|
+
providerScopes,
|
|
40
|
+
handler: (input, ctx) => backend.read(mailReadInput.parse(input).id, ctx),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "get_attachment",
|
|
44
|
+
group: "read",
|
|
45
|
+
summary: "Fetch an attachment",
|
|
46
|
+
description: "Fetch one attachment's bytes (base64).",
|
|
47
|
+
inputSchema: mailAttachmentInput,
|
|
48
|
+
providerScopes,
|
|
49
|
+
handler: (input, ctx) => {
|
|
50
|
+
const { id, attachment_id } = mailAttachmentInput.parse(input);
|
|
51
|
+
return backend.attachment(id, attachment_id, ctx);
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
/** Calls one of a mail connector's actions. */
|
|
57
|
+
export async function mailAction(connector, actionId, input, ctx) {
|
|
58
|
+
const action = connector.actions.find((a) => a.id === actionId);
|
|
59
|
+
if (!action) {
|
|
60
|
+
throw new Error(`${connector.name} has no action "${actionId}".`);
|
|
61
|
+
}
|
|
62
|
+
return (await action.handler(input, ctx));
|
|
63
|
+
}
|
|
64
|
+
export function snippetOf(text, length = 240) {
|
|
65
|
+
return text.replace(/\s+/g, " ").trim().slice(0, length);
|
|
66
|
+
}
|
|
67
|
+
/** The day after a YYYY-MM-DD date — for providers whose "before" is exclusive. */
|
|
68
|
+
export function dayAfter(day) {
|
|
69
|
+
const d = new Date(`${day}T00:00:00Z`);
|
|
70
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
71
|
+
return d.toISOString().slice(0, 10);
|
|
72
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { bodyToText, graphJson, MICROSOFT_OAUTH2 } from "../engenty/connections-microsoft/graph.js";
|
|
2
|
+
import { mailActions, snippetOf, } from "./mail.js";
|
|
3
|
+
const SCOPE_READ = "Mail.Read";
|
|
4
|
+
const address = (a) => {
|
|
5
|
+
const e = a?.emailAddress;
|
|
6
|
+
return e?.address ? (e.name ? `${e.name} <${e.address}>` : e.address) : null;
|
|
7
|
+
};
|
|
8
|
+
/** KQL, the language of Graph's `$search` on messages. */
|
|
9
|
+
function kql(input) {
|
|
10
|
+
const quote = (t) => `"${t.replace(/["\\]/g, "")}"`;
|
|
11
|
+
const parts = [
|
|
12
|
+
input.terms?.length ? `(${input.terms.map(quote).join(" OR ")})` : "",
|
|
13
|
+
input.from ? `from:${quote(input.from)}` : "",
|
|
14
|
+
input.since || input.until
|
|
15
|
+
? `received:${input.since ?? "1990-01-01"}..${input.until ?? "2999-12-31"}`
|
|
16
|
+
: "",
|
|
17
|
+
input.has_attachment ? "hasAttachments:true" : "",
|
|
18
|
+
].filter(Boolean);
|
|
19
|
+
return parts.join(" AND ");
|
|
20
|
+
}
|
|
21
|
+
async function attachmentsOf(ctx, id) {
|
|
22
|
+
const data = await graphJson(ctx, `/me/messages/${encodeURIComponent(id)}/attachments?$select=id,name,contentType,size,isInline`);
|
|
23
|
+
return (data.value ?? [])
|
|
24
|
+
.filter((a) => !a.isInline)
|
|
25
|
+
.map((a) => ({
|
|
26
|
+
id: a.id,
|
|
27
|
+
name: a.name ?? "attachment",
|
|
28
|
+
mime: a.contentType ?? "application/octet-stream",
|
|
29
|
+
size: a.size ?? null,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
const SELECT = "id,subject,bodyPreview,receivedDateTime,hasAttachments,from";
|
|
33
|
+
const backend = {
|
|
34
|
+
async search(input, ctx) {
|
|
35
|
+
const params = new URLSearchParams({ $select: SELECT, $top: String(input.limit ?? 40) });
|
|
36
|
+
const search = kql(input);
|
|
37
|
+
if (search) {
|
|
38
|
+
params.set("$search", `"${search.replace(/"/g, '\\"')}"`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
params.set("$orderby", "receivedDateTime desc");
|
|
42
|
+
}
|
|
43
|
+
const data = await graphJson(ctx, `/me/messages?${params}`);
|
|
44
|
+
const messages = data.value ?? [];
|
|
45
|
+
return Promise.all(messages.map(async (m) => ({
|
|
46
|
+
id: m.id,
|
|
47
|
+
date: m.receivedDateTime ?? null,
|
|
48
|
+
from: address(m.from),
|
|
49
|
+
subject: m.subject ?? null,
|
|
50
|
+
snippet: snippetOf(m.bodyPreview ?? ""),
|
|
51
|
+
attachments: m.hasAttachments ? await attachmentsOf(ctx, m.id) : [],
|
|
52
|
+
})));
|
|
53
|
+
},
|
|
54
|
+
async read(id, ctx) {
|
|
55
|
+
const m = await graphJson(ctx, `/me/messages/${encodeURIComponent(id)}?$select=${SELECT},toRecipients,body`);
|
|
56
|
+
const text = bodyToText(m.body);
|
|
57
|
+
return {
|
|
58
|
+
id: m.id,
|
|
59
|
+
date: m.receivedDateTime ?? null,
|
|
60
|
+
from: address(m.from),
|
|
61
|
+
to: (m.toRecipients ?? []).map(address).filter((a) => Boolean(a)),
|
|
62
|
+
subject: m.subject ?? null,
|
|
63
|
+
snippet: snippetOf(m.bodyPreview ?? text),
|
|
64
|
+
attachments: m.hasAttachments ? await attachmentsOf(ctx, m.id) : [],
|
|
65
|
+
text,
|
|
66
|
+
html: m.body?.contentType?.toLowerCase() === "html" ? (m.body.content ?? null) : null,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
async attachment(id, attachmentId, ctx) {
|
|
70
|
+
const a = await graphJson(ctx, `/me/messages/${encodeURIComponent(id)}/attachments/${encodeURIComponent(attachmentId)}`);
|
|
71
|
+
if (!a.contentBytes) {
|
|
72
|
+
throw new Error("This attachment is not a file.");
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
name: a.name ?? "attachment",
|
|
76
|
+
mime: a.contentType ?? "application/octet-stream",
|
|
77
|
+
data_base64: a.contentBytes,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
export const outlookConnector = {
|
|
82
|
+
id: "mail-outlook",
|
|
83
|
+
moduleId: "wizards-mail",
|
|
84
|
+
name: "Outlook",
|
|
85
|
+
description: "Read mail in an Outlook or Microsoft 365 account.",
|
|
86
|
+
icon: "logo:outlook",
|
|
87
|
+
toolPrefix: "mail",
|
|
88
|
+
auth: { kind: "oauth2", oauth2: MICROSOFT_OAUTH2 },
|
|
89
|
+
actions: mailActions(backend, [SCOPE_READ]),
|
|
90
|
+
};
|