diff --git a/.gitignore b/.gitignore index 9566b97..ac5e3d3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ .env .DS_Store plugins/**/.DS_Store +.env.local diff --git a/mailcow_plugin_project b/mailcow_plugin_project new file mode 160000 index 0000000..c537d38 --- /dev/null +++ b/mailcow_plugin_project @@ -0,0 +1 @@ +Subproject commit c537d3867018fff087b259ec9bd9d0fb0aa51949 diff --git a/mailcow_plugin_runtime_v0_3_0_setup_v2.sh b/mailcow_plugin_runtime_v0_3_0_setup_v2.sh new file mode 100644 index 0000000..da99019 --- /dev/null +++ b/mailcow_plugin_runtime_v0_3_0_setup_v2.sh @@ -0,0 +1,226 @@ +#!/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 --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" '\n\n
\n \n \nPlaceholder UI for the ownCloud picker modal.
\n \n