Compare commits

4 Commits

Author SHA1 Message Date
Thea Kindinger
cdb8e31c54 feat: scaffold v0.3.0 mailcow integration and plugin hook structure 2026-04-08 20:00:01 -04:00
Thea Kindinger
931c03ba93 chore: clean repo and prepare plugin runtime baseline 2026-04-08 19:57:59 -04:00
Thea Kindinger
4b7fa4565e chore: clean repo and prepare plugin runtime baseline 2026-04-08 19:54:58 -04:00
Thea Kindinger
79daa88311 chore: clean repo and prepare plugin runtime baseline 2026-04-08 19:49:55 -04:00
11 changed files with 240 additions and 0 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ dist/
.env .env
.DS_Store .DS_Store
plugins/**/.DS_Store plugins/**/.DS_Store
.env.local

View File

@@ -0,0 +1 @@
# Mailcow plugin runtime reverse proxy\n# Include this from the Mailcow nginx site or merge into the active server block.\n# Adjust upstream address if the runtime is not on localhost:4110.\n\nlocation /plugins-runtime/ {\n proxy_pass http://127.0.0.1:4110/;\n proxy_http_version 1.1;\n\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n\n proxy_request_buffering off;\n proxy_buffering off;\n client_max_body_size 100m;\n}\n

View File

@@ -0,0 +1 @@
# mailcow-plugin-runtime v0.3.0 integration plan\n\n## Scope\n- Proxy runtime behind Mailcow at \n- Load runtime bootstrap into Mailcow UI\n- Render compose toolbar and attachment hooks\n- Wire plugin end to end\n\n## Deployment notes\n1. Start runtime on port .\n2. Add to the Mailcow nginx server block.\n3. Ensure is reachable from Mailcow.\n4. Inject the bootstrap loader into the Mailcow compose page.\n\n## Expected follow-up\n- Replace placeholders in with actual selectors and injection logic.\n- Implement attachment token exchange and ownCloud file listing here.\n

View File

@@ -0,0 +1,230 @@
#!/bin/sh
# Re-exec under bash when invoked with sh.
if [ -z "${BASH_VERSION:-}" ]; then
if command -v bash >/dev/null 2>&1; then
exec bash "$0" "$@"
else
echo "This script requires bash." >&2
exit 1
fi
fi
set -Eeuo pipefail
# mailcow-plugin-runtime v0.3.0 bootstrap helper
#
# Purpose:
# - clean and stabilize the git repo
# - prepare a v0.2.0 baseline commit/tag
# - create a v0.3.0 feature branch
# - scaffold Mailcow nginx integration, runtime bootstrap wiring,
# hook renderer placeholders, and ownCloud attach plugin placeholders
#
# Default mode is DRY RUN. Nothing changes unless --apply is passed.
#
# Usage:
# bash mailcow_plugin_runtime_v0_3_0_setup.sh
# bash mailcow_plugin_runtime_v0_3_0_setup.sh --apply
# bash mailcow_plugin_runtime_v0_3_0_setup.sh --apply --skip-git
#
# Notes:
# - This script is intentionally conservative.
# - It creates scaffolding and placeholders, not a fully working Mailcow patch.
# - Review every generated file before committing and pushing.
APPLY=0
SKIP_GIT=0
SKIP_SCAFFOLD=0
FEATURE_BRANCH="feature/mailcow-plugin-runtime-v0.3.0"
BASELINE_TAG="v0.2.0"
BASELINE_COMMIT_MSG="chore: clean repo and prepare plugin runtime baseline"
SCAFFOLD_COMMIT_MSG="feat: scaffold v0.3.0 mailcow integration and plugin hook structure"
while [[ $# -gt 0 ]]; do
case "$1" in
--apply) APPLY=1 ;;
--skip-git) SKIP_GIT=1 ;;
--skip-scaffold) SKIP_SCAFFOLD=1 ;;
--feature-branch) FEATURE_BRANCH="${2:?missing value}"; shift ;;
--baseline-tag) BASELINE_TAG="${2:?missing value}"; shift ;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
shift
done
say() { printf '\n[%s] %s\n' "INFO" "$*"; }
warn() { printf '\n[%s] %s\n' "WARN" "$*"; }
run() {
if [[ "$APPLY" -eq 1 ]]; then
printf '+ %s\n' "$*"
eval "$*"
else
printf '[dry-run] %s\n' "$*"
fi
}
require_repo() {
git rev-parse --show-toplevel >/dev/null 2>&1 || {
echo "This script must be run inside a git repository." >&2
exit 1
}
}
repo_root() {
git rev-parse --show-toplevel
}
append_line_once() {
local file="$1"
local line="$2"
if [[ -f "$file" ]] && grep -Fqx "$line" "$file"; then
return 0
fi
if [[ "$APPLY" -eq 1 ]]; then
printf '%s\n' "$line" >> "$file"
else
printf '[dry-run] append to %s: %s\n' "$file" "$line"
fi
}
write_file_if_missing() {
local path="$1"
local content="$2"
if [[ -e "$path" ]]; then
say "Keeping existing file: $path"
return 0
fi
if [[ "$APPLY" -eq 1 ]]; then
mkdir -p "$(dirname "$path")"
python3 - <<'PY' "$path" "$content"
import sys
path, content = sys.argv[1], sys.argv[2]
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
PY
else
printf '[dry-run] create file: %s\n' "$path"
fi
}
ensure_gitignore() {
local file=".gitignore"
if [[ ! -f "$file" && "$APPLY" -eq 1 ]]; then
: > "$file"
fi
append_line_once "$file" "node_modules/"
append_line_once "$file" ".DS_Store"
append_line_once "$file" "dist/"
append_line_once "$file" ".env"
append_line_once "$file" ".env.local"
}
cleanup_index() {
if git ls-files --stage | awk '{print $1" "$4}' | grep -q '^160000 mailcow_plugin_project$'; then
run "git rm --cached mailcow_plugin_project"
fi
if git ls-files --error-unmatch node_modules >/dev/null 2>&1; then
run "git rm -r --cached node_modules"
else
say "node_modules is not tracked."
fi
local ds
while IFS= read -r ds; do
[[ -z "$ds" ]] && continue
run "git rm --cached '$ds'"
done < <(git ls-files | grep -E '(^|/)\.DS_Store$' || true)
}
maybe_commit() {
local message="$1"
if git diff --cached --quiet && git diff --quiet; then
say "No changes detected for commit: $message"
return 0
fi
run "git add -A"
run "git commit -m '$message'"
}
maybe_tag() {
local tag="$1"
if git rev-parse "$tag" >/dev/null 2>&1; then
say "Tag already exists: $tag"
else
run "git tag -a '$tag' -m 'mailcow plugin runtime $tag baseline'"
fi
}
ensure_branch() {
local branch="$1"
local current
current="$(git branch --show-current)"
if [[ "$current" == "$branch" ]]; then
say "Already on branch: $branch"
return 0
fi
if git show-ref --verify --quiet "refs/heads/$branch"; then
run "git checkout '$branch'"
else
run "git checkout -b '$branch'"
fi
}
scaffold_files() {
write_file_if_missing "deploy/nginx/mailcow-plugin-runtime.conf" "# Mailcow plugin runtime reverse proxy\n# Include this from the Mailcow nginx site or merge into the active server block.\n# Adjust upstream address if the runtime is not on localhost:4110.\n\nlocation /plugins-runtime/ {\n proxy_pass http://127.0.0.1:4110/;\n proxy_http_version 1.1;\n\n proxy_set_header Host \$host;\n proxy_set_header X-Real-IP \$remote_addr;\n proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto \$scheme;\n\n proxy_request_buffering off;\n proxy_buffering off;\n client_max_body_size 100m;\n}\n"
write_file_if_missing "docs/v0.3.0-mailcow-integration.md" "# mailcow-plugin-runtime v0.3.0 integration plan\n\n## Scope\n- Proxy runtime behind Mailcow at `/plugins-runtime/`\n- Load runtime bootstrap into Mailcow UI\n- Render compose toolbar and attachment hooks\n- Wire `owncloud-attach` plugin end to end\n\n## Deployment notes\n1. Start runtime on port `4110`.\n2. Add `deploy/nginx/mailcow-plugin-runtime.conf` to the Mailcow nginx server block.\n3. Ensure `/plugins-runtime/runtime/bootstrap.js` is reachable from Mailcow.\n4. Inject the bootstrap loader into the Mailcow compose page.\n\n## Expected follow-up\n- Replace placeholders in `src/integrations/mailcow` with actual selectors and injection logic.\n- Implement attachment token exchange and ownCloud file listing here.\n"
write_file_if_missing "src/integrations/mailcow/bootstrap-loader.ts" 'export interface MailcowBootstrapOptions {\n runtimeBaseUrl?: string;\n}\n\nexport function loadPluginRuntimeBootstrap(options: MailcowBootstrapOptions = {}): HTMLScriptElement {\n const runtimeBaseUrl = options.runtimeBaseUrl ?? "/plugins-runtime";\n const script = document.createElement("script");\n script.src = `\${runtimeBaseUrl}/runtime/bootstrap.js`;\n script.async = true;\n script.dataset.runtimeBaseUrl = runtimeBaseUrl;\n document.head.appendChild(script);\n return script;\n}\n'
write_file_if_missing "src/integrations/mailcow/dom-targets.ts" 'export const MAILCOW_SELECTORS = {\n composeToolbar: "[data-mailcow-compose-toolbar], .compose-toolbar, .toolbar",\n composeAttachments: "[data-mailcow-compose-attachments], .compose-attachments, .attachments",\n};\n'
write_file_if_missing "src/integrations/mailcow/install.ts" 'import { loadPluginRuntimeBootstrap } from "./bootstrap-loader";\nimport { mountComposeToolbarHook, mountComposeAttachmentsHook } from "../hooks/mailcow-hooks";\n\nexport function installMailcowPluginRuntime(): void {\n loadPluginRuntimeBootstrap();\n\n window.addEventListener("DOMContentLoaded", () => {\n mountComposeToolbarHook();\n mountComposeAttachmentsHook();\n });\n}\n'
write_file_if_missing "src/hooks/mailcow-hooks.ts" 'import { MAILCOW_SELECTORS } from "../integrations/mailcow/dom-targets";\n\nfunction ensureHookContainer(selector: string, id: string): HTMLElement | null {\n const host = document.querySelector(selector);\n if (!host) return null;\n\n let node = document.getElementById(id);\n if (!node) {\n node = document.createElement("div");\n node.id = id;\n node.dataset.pluginHookContainer = id;\n host.appendChild(node);\n }\n\n return node;\n}\n\nexport function mountComposeToolbarHook(): void {\n ensureHookContainer(MAILCOW_SELECTORS.composeToolbar, "mailcow-plugin-hook-compose-toolbar");\n}\n\nexport function mountComposeAttachmentsHook(): void {\n ensureHookContainer(MAILCOW_SELECTORS.composeAttachments, "mailcow-plugin-hook-compose-attachments");\n}\n'
write_file_if_missing "plugins/owncloud-attach/README.md" "# owncloud-attach plugin\n\n## Goal\nAdd an ownCloud file picker to the Mailcow compose flow.\n\n## Planned behavior\n- Toolbar button opens picker\n- Picker lists the authenticated user's ownCloud files\n- Selection returns attachment metadata to the runtime\n- Runtime injects attachment entries into Mailcow compose UI\n"
write_file_if_missing "plugins/owncloud-attach/plugin.json" "{\n \"id\": \"owncloud-attach\",\n \"version\": \"0.3.0\",\n \"name\": \"owncloud-attach\",\n \"hooks\": [\n \"compose.toolbar\",\n \"compose.attachments\"\n ]\n}\n"
write_file_if_missing "plugins/owncloud-attach/ui/index.html" '<!doctype html>\n<html lang="en">\n <head>\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1" />\n <title>owncloud-attach</title>\n <style>\n body { font-family: system-ui, sans-serif; margin: 24px; background: #0b0b0f; color: #f7f7f8; }\n .card { max-width: 720px; margin: 0 auto; padding: 24px; border-radius: 16px; background: #15151d; }\n button { padding: 10px 14px; border-radius: 10px; border: 0; cursor: pointer; }\n </style>\n </head>\n <body>\n <div class="card">\n <h1>owncloud-attach</h1>\n <p>Placeholder UI for the ownCloud picker modal.</p>\n <button id="connect-owncloud">Connect ownCloud</button>\n </div>\n </body>\n</html>\n'
write_file_if_missing "plugins/owncloud-attach/api/README.md" "# owncloud-attach API placeholders\n\nExpected endpoints:\n- GET /plugins/owncloud-attach/api/files\n- POST /plugins/owncloud-attach/api/attach\n- GET /plugins/owncloud-attach/api/health\n\nImplement the runtime-side token exchange and ownCloud file listing here.\n"
write_file_if_missing "scripts/next-steps.sh" "#!/usr/bin/env bash\nset -euo pipefail\n\necho 'Next manual steps:'\necho '1. Wire deploy/nginx/mailcow-plugin-runtime.conf into the Mailcow nginx server block.'\necho '2. Make Mailcow load src/integrations/mailcow/install.ts output on compose pages.'\necho '3. Replace placeholder selectors in src/integrations/mailcow/dom-targets.ts.'\necho '4. Build owncloud-attach picker API and file attachment flow.'\n"
if [[ "$APPLY" -eq 1 ]]; then
chmod +x scripts/next-steps.sh || true
fi
}
main() {
require_repo
local root
root="$(repo_root)"
say "Repository root: $root"
say "Mode: $([[ "$APPLY" -eq 1 ]] && echo APPLY || echo DRY RUN)"
if [[ "$SKIP_GIT" -eq 0 ]]; then
say "Stabilizing git baseline"
ensure_gitignore
cleanup_index
maybe_commit "$BASELINE_COMMIT_MSG"
maybe_tag "$BASELINE_TAG"
ensure_branch "$FEATURE_BRANCH"
fi
if [[ "$SKIP_SCAFFOLD" -eq 0 ]]; then
say "Scaffolding v0.3.0 integration files"
scaffold_files
maybe_commit "$SCAFFOLD_COMMIT_MSG"
fi
say "Done. Review changes with: git status && git diff --stat"
warn "This script does not push changes. Push manually after review."
}
main "$@"

View File

@@ -0,0 +1 @@
# owncloud-attach API placeholders\n\nExpected endpoints:\n- GET /plugins/owncloud-attach/api/files\n- POST /plugins/owncloud-attach/api/attach\n- GET /plugins/owncloud-attach/api/health\n\nImplement the runtime-side token exchange and ownCloud file listing here.\n

View File

@@ -0,0 +1 @@
<!doctype html>\n<html lang="en">\n <head>\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1" />\n <title>owncloud-attach</title>\n <style>\n body { font-family: system-ui, sans-serif; margin: 24px; background: #0b0b0f; color: #f7f7f8; }\n .card { max-width: 720px; margin: 0 auto; padding: 24px; border-radius: 16px; background: #15151d; }\n button { padding: 10px 14px; border-radius: 10px; border: 0; cursor: pointer; }\n </style>\n </head>\n <body>\n <div class="card">\n <h1>owncloud-attach</h1>\n <p>Placeholder UI for the ownCloud picker modal.</p>\n <button id="connect-owncloud">Connect ownCloud</button>\n </div>\n </body>\n</html>\n

1
scripts/next-steps.sh Executable file
View File

@@ -0,0 +1 @@
#!/usr/bin/env bash\nset -euo pipefail\n\necho 'Next manual steps:'\necho '1. Wire deploy/nginx/mailcow-plugin-runtime.conf into the Mailcow nginx server block.'\necho '2. Make Mailcow load src/integrations/mailcow/install.ts output on compose pages.'\necho '3. Replace placeholder selectors in src/integrations/mailcow/dom-targets.ts.'\necho '4. Build owncloud-attach picker API and file attachment flow.'\n

View File

@@ -0,0 +1 @@
import { MAILCOW_SELECTORS } from "../integrations/mailcow/dom-targets";\n\nfunction ensureHookContainer(selector: string, id: string): HTMLElement | null {\n const host = document.querySelector(selector);\n if (!host) return null;\n\n let node = document.getElementById(id);\n if (!node) {\n node = document.createElement("div");\n node.id = id;\n node.dataset.pluginHookContainer = id;\n host.appendChild(node);\n }\n\n return node;\n}\n\nexport function mountComposeToolbarHook(): void {\n ensureHookContainer(MAILCOW_SELECTORS.composeToolbar, "mailcow-plugin-hook-compose-toolbar");\n}\n\nexport function mountComposeAttachmentsHook(): void {\n ensureHookContainer(MAILCOW_SELECTORS.composeAttachments, "mailcow-plugin-hook-compose-attachments");\n}\n

View File

@@ -0,0 +1 @@
export interface MailcowBootstrapOptions {\n runtimeBaseUrl?: string;\n}\n\nexport function loadPluginRuntimeBootstrap(options: MailcowBootstrapOptions = {}): HTMLScriptElement {\n const runtimeBaseUrl = options.runtimeBaseUrl ?? "/plugins-runtime";\n const script = document.createElement("script");\n script.src = `\${runtimeBaseUrl}/runtime/bootstrap.js`;\n script.async = true;\n script.dataset.runtimeBaseUrl = runtimeBaseUrl;\n document.head.appendChild(script);\n return script;\n}\n

View File

@@ -0,0 +1 @@
export const MAILCOW_SELECTORS = {\n composeToolbar: "[data-mailcow-compose-toolbar], .compose-toolbar, .toolbar",\n composeAttachments: "[data-mailcow-compose-attachments], .compose-attachments, .attachments",\n};\n

View File

@@ -0,0 +1 @@
import { loadPluginRuntimeBootstrap } from "./bootstrap-loader";\nimport { mountComposeToolbarHook, mountComposeAttachmentsHook } from "../hooks/mailcow-hooks";\n\nexport function installMailcowPluginRuntime(): void {\n loadPluginRuntimeBootstrap();\n\n window.addEventListener("DOMContentLoaded", () => {\n mountComposeToolbarHook();\n mountComposeAttachmentsHook();\n });\n}\n