Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 164 additions & 2 deletions scripts/thunder/02-sample-resources.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,54 @@ set -e
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]:-$0}")"
source "${SCRIPT_DIR}/common.sh"

# Load .env values when available (useful for local execution).
ENV_FILE="${SCRIPT_DIR}/.env"
# Load values from the single services/.env file (useful for local execution).
ENV_FILE="${SCRIPT_DIR}/../../services/.env"
if [[ -f "$ENV_FILE" ]]; then
set -a
source "$ENV_FILE"
set +a
fi

# Resolve primary domain. Prefer MAIL_DOMAIN env (set by docker-compose) so the
# script works the same on the host and inside the thunder-setup container.
# Falls back to scanning silver.yaml at a few well-known paths.
MAIL_DOMAIN="${MAIL_DOMAIN:-}"
if [[ -z "${MAIL_DOMAIN}" ]]; then
SILVER_CONF_FILE=""
for candidate in \
"${SCRIPT_DIR}/silver.yaml" \
"${SCRIPT_DIR}/../../conf/silver.yaml" \
"/opt/thunder/bootstrap/silver.yaml"; do
if [[ -f "$candidate" ]]; then
SILVER_CONF_FILE="$candidate"
break
fi
done
if [[ -z "${SILVER_CONF_FILE}" ]]; then
echo "ERROR: Could not locate silver.yaml; set MAIL_DOMAIN or mount conf/silver.yaml" >&2
exit 1
fi
MAIL_DOMAIN=$(grep -m 1 '^\s*-\s*domain:' "${SILVER_CONF_FILE}" | sed 's/.*domain:\s*//' | xargs)
fi
if [[ -z "${MAIL_DOMAIN}" ]]; then
echo "ERROR: No domain configured (MAIL_DOMAIN env unset and silver.yaml domain empty)" >&2
exit 1
fi

# Derive a human-readable OU name from the domain (e.g. example.com -> "Example Com").
DOMAIN_OU_NAME=$(echo "${MAIL_DOMAIN}" | sed 's/\./ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1')
DOMAIN_OU_HANDLE="${MAIL_DOMAIN}"
DOMAIN_USER_SCHEMA_NAME="${THUNDER_DOMAIN_USER_SCHEMA_NAME:-Contact}"
CONTACT_USERNAME="${THUNDER_CONTACT_USERNAME:-contact}"
# Contact user password: take THUNDER_SMTP_PASSWORD from services/.env so the
# Thunder user and the SMTP credentials in deployment.yaml stay in sync.
# Falls back to a generated random value if the env var is empty.
CONTACT_PASSWORD="${THUNDER_SMTP_PASSWORD:-}"
if [[ -z "${CONTACT_PASSWORD}" ]]; then
CONTACT_PASSWORD=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 20)
log_warning "THUNDER_SMTP_PASSWORD not set in services/.env; generated a random password for '${CONTACT_USERNAME}'"
fi

# Default SPA parameters (can be overridden via env vars).
SPA_APP_NAME="${THUNDER_SPA_APP_NAME:-Email App}"
SPA_APP_DESCRIPTION="${THUNDER_SPA_APP_DESCRIPTION:-Application for email client to use OAuth2 authentication}"
Expand Down Expand Up @@ -208,6 +248,128 @@ JSON
fi
}

# ============================================================================
# Create Domain Organization Unit
# ============================================================================

log_info "Creating organization unit for domain '${MAIL_DOMAIN}'..."

OU_PAYLOAD=$(cat <<JSON
{
"handle": "${DOMAIN_OU_HANDLE}",
"name": "${DOMAIN_OU_NAME}",
"description": "Organization unit for ${MAIL_DOMAIN}",
"logoUrl": "emoji:βœ‰οΈ"
}
JSON
)

RESPONSE=$(thunder_api_call POST "/organization-units" "${OU_PAYLOAD}")
HTTP_CODE="${RESPONSE: -3}"
BODY="${RESPONSE%???}"

if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then
log_success "Organization unit '${DOMAIN_OU_NAME}' created"
DOMAIN_OU_ID=$(extract_json_value "$BODY" "id")
elif [[ "$HTTP_CODE" == "409" ]]; then
log_warning "Organization unit '${DOMAIN_OU_HANDLE}' already exists, retrieving ID..."
DOMAIN_OU_ID=$(get_ou_id_by_handle "${DOMAIN_OU_HANDLE}") || exit 1
else
log_error "Failed to create organization unit (HTTP $HTTP_CODE)"
echo "Response: $BODY"
exit 1
fi

if [[ -z "$DOMAIN_OU_ID" ]]; then
log_error "Could not resolve OU ID for handle '${DOMAIN_OU_HANDLE}'"
exit 1
fi
log_info "OU ID: $DOMAIN_OU_ID"
echo ""

# ============================================================================
# Create User Schema (username + password) under the Domain OU
# ============================================================================

log_info "Creating user schema '${DOMAIN_USER_SCHEMA_NAME}' under OU '${DOMAIN_OU_HANDLE}'..."

SCHEMA_PAYLOAD=$(cat <<JSON
{
"name": "${DOMAIN_USER_SCHEMA_NAME}",
"ouId": "${DOMAIN_OU_ID}",
"schema": {
"username": {
"type": "string",
"displayName": "Username",
"required": true,
"unique": true
},
"password": {
"type": "string",
"displayName": "Password",
"required": true,
"credential": true
}
},
"systemAttributes": {
"display": "username"
}
}
JSON
)

RESPONSE=$(thunder_api_call POST "/user-schemas" "${SCHEMA_PAYLOAD}")
HTTP_CODE="${RESPONSE: -3}"
BODY="${RESPONSE%???}"

if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then
log_success "User schema '${DOMAIN_USER_SCHEMA_NAME}' created"
elif [[ "$HTTP_CODE" == "409" ]]; then
log_warning "User schema '${DOMAIN_USER_SCHEMA_NAME}' already exists, skipping"
else
log_error "Failed to create user schema (HTTP $HTTP_CODE)"
echo "Response: $BODY"
exit 1
fi

echo ""

# ============================================================================
# Create Contact User in the Domain OU
# ============================================================================

log_info "Creating user '${CONTACT_USERNAME}' in OU '${DOMAIN_OU_HANDLE}'..."

USER_PAYLOAD=$(cat <<JSON
{
"type": "${DOMAIN_USER_SCHEMA_NAME}",
"ouId": "${DOMAIN_OU_ID}",
"attributes": {
"username": "${CONTACT_USERNAME}",
"password": "${CONTACT_PASSWORD}"
}
}
JSON
)

RESPONSE=$(thunder_api_call POST "/users" "${USER_PAYLOAD}")
HTTP_CODE="${RESPONSE: -3}"
BODY="${RESPONSE%???}"

if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then
log_success "User '${CONTACT_USERNAME}' created"
log_info "Username: ${CONTACT_USERNAME}"
log_info "Password: ${CONTACT_PASSWORD}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Logging the generated password to stdout is a security risk, as these logs may be persisted in container logs or monitoring systems. Since this script is intended for automated setup, consider providing the password via an environment variable (which is already supported via THUNDER_SMTP_PASSWORD) and only generating/printing it as a last resort. If it must be printed, ensure the environment is secure and logs are handled appropriately.

elif [[ "$HTTP_CODE" == "409" ]]; then
log_warning "User '${CONTACT_USERNAME}' already exists, password unchanged"
else
log_error "Failed to create user (HTTP $HTTP_CODE)"
echo "Response: $BODY"
exit 1
fi

echo ""

# ============================================================================
# Create Single SPA Application
# ============================================================================
Expand Down
12 changes: 12 additions & 0 deletions services/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ RSPAMD_PASSWORD=your_secure_password
THUNDER_ADMIN_USERNAME=admin
THUNDER_ADMIN_PASSWORD=admin

# Thunder SMTP password written into silver-config/thunder/deployment.yaml
# (email.smtp.password). Host, username, and from_address are auto-derived
# from the primary domain in conf/silver.yaml. Leave blank to keep the
# placeholder in deployment.yaml.
THUNDER_SMTP_PASSWORD=

# Thunder contact user (created by scripts/thunder/02-sample-resources.sh in
# the per-domain OU). Username is overridable; password is auto-generated at
# runtime and printed once to stdout.
THUNDER_CONTACT_USERNAME=contact
THUNDER_DOMAIN_USER_SCHEMA_NAME=Contact

# Public IP of this mail server
MAIL_SERVER_IP=

Expand Down
26 changes: 24 additions & 2 deletions services/config-scripts/gen-thunder.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ readonly THUNDER_CONSOLE_CONFIG="${ROOT_DIR}/silver-config/thunder/console-confi
readonly THUNDER_GATE_CONFIG="${ROOT_DIR}/silver-config/thunder/gate-config.js"
readonly THUNDER_PORT="8090"

# Load services/.env so SMTP credentials (and any other overrides) are available.
if [[ -f "${ROOT_DIR}/.env" ]]; then
set -a
source "${ROOT_DIR}/.env"
set +a
fi

# SMTP values derived from the primary domain (password from .env, optional).
readonly SMTP_HOST="mail.${MAIL_DOMAIN}"
readonly SMTP_USERNAME="contact@${MAIL_DOMAIN}"
readonly SMTP_FROM_ADDRESS="contact@${MAIL_DOMAIN}"
readonly SMTP_PASSWORD="${THUNDER_SMTP_PASSWORD:-}"

mkdir -p "${THUNDER_CERTS_PATH}"

cp "${LETSENCRYPT_PATH}/fullchain.pem" "${THUNDER_CERTS_PATH}/server.cert"
Expand Down Expand Up @@ -48,10 +61,19 @@ if [[ -f "${THUNDER_DEPLOYMENT_FILE}" ]]; then

# Update passkey.allowed_origins - replace any https://domain:port pattern
sed -i'' -e "/^passkey:/,/^[^ ]/ s|https://[^:\"]*:[0-9]*|https://${MAIL_DOMAIN}:${THUNDER_PORT}|g" "${THUNDER_DEPLOYMENT_FILE}"


# Update email.smtp host/username/from_address (and password if env var set).
# The sed range targets lines within the `email:` block only.
sed -i'' -e "/^email:/,/^[^ ]/ s|host:.*|host: \"${SMTP_HOST}\"|" "${THUNDER_DEPLOYMENT_FILE}"
sed -i'' -e "/^email:/,/^[^ ]/ s|username:.*|username: \"${SMTP_USERNAME}\"|" "${THUNDER_DEPLOYMENT_FILE}"
sed -i'' -e "/^email:/,/^[^ ]/ s|from_address:.*|from_address: \"${SMTP_FROM_ADDRESS}\"|" "${THUNDER_DEPLOYMENT_FILE}"
Comment on lines +67 to +69
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The sed substitution patterns are a bit broad and could accidentally match other keys that contain these strings (e.g., smtp_host: or backup_host:). It's safer to anchor the match to the start of the line and preserve indentation using a capture group. Additionally, the range /^email:/,/^[^ ]/ can be fragile if there are comments starting at the first column within the email: block, as sed will stop the range at the first such comment. Quoting numeric or string values in the generated YAML is acceptable per repository guidelines.

Suggested change
sed -i'' -e "/^email:/,/^[^ ]/ s|host:.*|host: \"${SMTP_HOST}\"|" "${THUNDER_DEPLOYMENT_FILE}"
sed -i'' -e "/^email:/,/^[^ ]/ s|username:.*|username: \"${SMTP_USERNAME}\"|" "${THUNDER_DEPLOYMENT_FILE}"
sed -i'' -e "/^email:/,/^[^ ]/ s|from_address:.*|from_address: \"${SMTP_FROM_ADDRESS}\"|" "${THUNDER_DEPLOYMENT_FILE}"
sed -i'' -e "/^email:/,/^[^ ]/ s|^\([[:space:]]*\)host:.*|\1host: \"${SMTP_HOST}\"|" "${THUNDER_DEPLOYMENT_FILE}"
sed -i'' -e "/^email:/,/^[^ ]/ s|^\([[:space:]]*\)username:.*|\1username: \"${SMTP_USERNAME}\"|" "${THUNDER_DEPLOYMENT_FILE}"
sed -i'' -e "/^email:/,/^[^ ]/ s|^\([[:space:]]*\)from_address:.*|\1from_address: \"${SMTP_FROM_ADDRESS}\"|" "${THUNDER_DEPLOYMENT_FILE}"
References
  1. In shell scripts that generate YAML, it is acceptable to quote numeric values as strings if the consuming application is not strict about the data type.

if [[ -n "${SMTP_PASSWORD}" ]]; then
sed -i'' -e "/^email:/,/^[^ ]/ s|password:.*|password: \"${SMTP_PASSWORD}\"|" "${THUNDER_DEPLOYMENT_FILE}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Anchoring the match to the start of the line is recommended here as well to avoid accidental matches with other keys. Quoting the value as a string is acceptable per repository guidelines.

Suggested change
sed -i'' -e "/^email:/,/^[^ ]/ s|password:.*|password: \"${SMTP_PASSWORD}\"|" "${THUNDER_DEPLOYMENT_FILE}"
sed -i'' -e "/^email:/,/^[^ ]/ s|^\([[:space:]]*\)password:.*|\1password: \"${SMTP_PASSWORD}\"|" "${THUNDER_DEPLOYMENT_FILE}"
References
  1. In shell scripts that generate YAML, it is acceptable to quote numeric values as strings if the consuming application is not strict about the data type.

fi

# Remove backup file
rm -f "${THUNDER_DEPLOYMENT_FILE}.bak"

echo -e "Thunder deployment configuration updated with domain: ${MAIL_DOMAIN} and port: ${THUNDER_PORT}"
else
echo -e "Warning: Thunder deployment.yaml not found at ${THUNDER_DEPLOYMENT_FILE}"
Expand Down
4 changes: 4 additions & 0 deletions services/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,13 @@ services:
- ./silver-config/thunder/deployment.yaml:/opt/thunder/repository/conf/deployment.yaml:ro
- ./../scripts/thunder/01-default-resources.sh:/opt/thunder/bootstrap/01-default-resources.sh
- ./../scripts/thunder/02-sample-resources.sh:/opt/thunder/bootstrap/02-sample-resources.sh
- ./../conf/silver.yaml:/opt/thunder/bootstrap/silver.yaml:ro
environment:
- THUNDER_ADMIN_USERNAME=${THUNDER_ADMIN_USERNAME:-admin}
- THUNDER_ADMIN_PASSWORD=${THUNDER_ADMIN_PASSWORD:-admin}
- THUNDER_SMTP_PASSWORD=${THUNDER_SMTP_PASSWORD:-}
- THUNDER_CONTACT_USERNAME=${THUNDER_CONTACT_USERNAME:-contact}
- THUNDER_DOMAIN_USER_SCHEMA_NAME=${THUNDER_DOMAIN_USER_SCHEMA_NAME:-Contact}
depends_on:
thunder-db-init:
condition: service_completed_successfully
Expand Down
Loading