Merge pull request 'feat: initial version' (#1) from chore/initial-version into main
All checks were successful
Test Stalwart Installation Action / Error Handling Tests (push) Successful in 19s
Test Stalwart Installation Action / Full Configuration (Domains + Users) (push) Successful in 33s
Test Stalwart Installation Action / Basic Installation (No Config) (push) Successful in 49s
Test Stalwart Installation Action / Test Summary (push) Successful in 3s
All checks were successful
Test Stalwart Installation Action / Error Handling Tests (push) Successful in 19s
Test Stalwart Installation Action / Full Configuration (Domains + Users) (push) Successful in 33s
Test Stalwart Installation Action / Basic Installation (No Config) (push) Successful in 49s
Test Stalwart Installation Action / Test Summary (push) Successful in 3s
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
400
.github/workflows/test.yml
vendored
Normal file
400
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,400 @@
|
||||
name: Test Stalwart Installation Action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# Test basic installation without configuration
|
||||
test-basic-install:
|
||||
name: Basic Installation (No Config)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq curl
|
||||
|
||||
- name: Run basic installation
|
||||
uses: ./
|
||||
# No inputs - should install with defaults
|
||||
|
||||
- name: Wait for service to fully start
|
||||
run: |
|
||||
echo "Waiting for Stalwart to fully initialize..."
|
||||
sleep 15
|
||||
|
||||
- name: Verify Stalwart service is running
|
||||
run: |
|
||||
sleep 10 # Give service time to start
|
||||
|
||||
# Check if systemd is available
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1; then
|
||||
echo "Checking service status with systemd..."
|
||||
sudo systemctl status stalwart --no-pager || true
|
||||
if sudo systemctl is-active --quiet stalwart; then
|
||||
echo "✓ Stalwart service is running (systemd)"
|
||||
else
|
||||
echo "::warning::Stalwart service not active in systemd, checking process..."
|
||||
fi
|
||||
else
|
||||
echo "::warning::systemd not available, checking process directly..."
|
||||
fi
|
||||
|
||||
# Check if process is running
|
||||
if pgrep -x stalwart >/dev/null; then
|
||||
echo "✓ Stalwart process is running"
|
||||
else
|
||||
echo "::error::Stalwart process is not running"
|
||||
ps aux | grep stalwart || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check Stalwart binary
|
||||
run: |
|
||||
if [ ! -f /opt/stalwart/bin/stalwart ]; then
|
||||
echo "::error::Stalwart binary not found"
|
||||
exit 1
|
||||
fi
|
||||
/opt/stalwart/bin/stalwart --version
|
||||
|
||||
- name: Test web admin accessibility
|
||||
run: |
|
||||
# Wait for API to be ready
|
||||
for i in {1..30}; do
|
||||
if curl -sf http://localhost:8080/login >/dev/null 2>&1; then
|
||||
echo "✓ Web admin is accessible"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "::error::Web admin not accessible after 60 seconds"
|
||||
exit 1
|
||||
|
||||
# Test full configuration with domains and users
|
||||
test-full-config:
|
||||
name: Full Configuration (Domains + Users)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq curl
|
||||
|
||||
- name: Install with full configuration
|
||||
id: install
|
||||
uses: ./
|
||||
with:
|
||||
domains: |
|
||||
[
|
||||
{
|
||||
"name": "test1.local",
|
||||
"description": "Primary test domain"
|
||||
},
|
||||
{
|
||||
"name": "test2.local",
|
||||
"description": "Secondary test domain"
|
||||
}
|
||||
]
|
||||
users: |
|
||||
[
|
||||
{
|
||||
"email": "user1@test1.local",
|
||||
"password": "UserPass123!",
|
||||
"name": "Test User One",
|
||||
"quota": 1073741824
|
||||
},
|
||||
{
|
||||
"email": "user2@test1.local",
|
||||
"password": "UserPass456!",
|
||||
"name": "Test User Two",
|
||||
"quota": 2147483648
|
||||
},
|
||||
{
|
||||
"email": "admin@test2.local",
|
||||
"password": "AdminUser789!",
|
||||
"name": "Admin User",
|
||||
"quota": 5368709120
|
||||
}
|
||||
]
|
||||
|
||||
- name: Wait for service to fully start
|
||||
run: |
|
||||
echo "Waiting for Stalwart to fully initialize..."
|
||||
sleep 15
|
||||
|
||||
- name: Verify service is running
|
||||
run: |
|
||||
# Check process
|
||||
if ! pgrep -x stalwart >/dev/null; then
|
||||
echo "::error::Stalwart process is not running"
|
||||
echo "Process list:"
|
||||
ps aux | grep stalwart || true
|
||||
echo "PID file:"
|
||||
cat /var/run/stalwart.pid 2>/dev/null || echo "No PID file"
|
||||
echo "Logs:"
|
||||
tail -50 /opt/stalwart/logs/*.log 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Stalwart is running"
|
||||
|
||||
- name: Verify service is accessible
|
||||
run: |
|
||||
# Wait for API to be ready
|
||||
echo "Waiting for Stalwart API..."
|
||||
for i in {1..30}; do
|
||||
if curl -sf http://localhost:8080/login >/dev/null 2>&1; then
|
||||
echo "✓ API is ready"
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "::error::API not accessible after 60 seconds"
|
||||
exit 1
|
||||
|
||||
- name: Verify domains and users were created
|
||||
run: |
|
||||
echo "=== Reading Admin Password ==="
|
||||
if [ -f /tmp/stalwart_admin_password ]; then
|
||||
ADMIN_PASSWORD=$(cat /tmp/stalwart_admin_password)
|
||||
echo "✓ Admin password retrieved"
|
||||
else
|
||||
echo "::error::Admin password file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Verifying Domains ==="
|
||||
DOMAINS_RESPONSE=$(curl -s -u "admin:$ADMIN_PASSWORD" \
|
||||
"http://localhost:8080/api/principal?types=domain&limit=100")
|
||||
|
||||
DOMAIN_COUNT=$(echo "$DOMAINS_RESPONSE" | jq '.data.total // 0')
|
||||
echo "Total domains found: $DOMAIN_COUNT"
|
||||
|
||||
# List domains
|
||||
echo "$DOMAINS_RESPONSE" | jq -r '.data.items[] | " - \(.name): \(.description // "No description")"'
|
||||
|
||||
# Verify specific domains exist
|
||||
if echo "$DOMAINS_RESPONSE" | jq -e '.data.items[] | select(.name == "test1.local")' >/dev/null; then
|
||||
echo "✓ Domain test1.local exists"
|
||||
else
|
||||
echo "::error::Domain test1.local not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$DOMAINS_RESPONSE" | jq -e '.data.items[] | select(.name == "test2.local")' >/dev/null; then
|
||||
echo "✓ Domain test2.local exists"
|
||||
else
|
||||
echo "::error::Domain test2.local not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Verifying Users ==="
|
||||
USERS_RESPONSE=$(curl -s -u "admin:$ADMIN_PASSWORD" \
|
||||
"http://localhost:8080/api/principal?types=individual&limit=100")
|
||||
|
||||
USER_COUNT=$(echo "$USERS_RESPONSE" | jq '.data.total // 0')
|
||||
echo "Total users found: $USER_COUNT"
|
||||
|
||||
# List users
|
||||
echo "$USERS_RESPONSE" | jq -r '.data.items[] | " - \(.name) (\(.emails[0])): roles=\(.roles)"'
|
||||
|
||||
# Verify we have at least the 3 users we created
|
||||
if [ "$USER_COUNT" -lt 3 ]; then
|
||||
echo "::error::Expected at least 3 users, found $USER_COUNT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify specific users exist and have correct roles
|
||||
for user_email in "user1@test1.local" "user2@test1.local" "admin@test2.local"; do
|
||||
USER_DATA=$(echo "$USERS_RESPONSE" | jq --arg email "$user_email" '.data.items[] | select(.emails[] == $email)')
|
||||
|
||||
if [ -z "$USER_DATA" ]; then
|
||||
echo "::error::User $user_email not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if user has "user" role
|
||||
HAS_USER_ROLE=$(echo "$USER_DATA" | jq '.roles | contains(["user"])')
|
||||
if [ "$HAS_USER_ROLE" = "true" ]; then
|
||||
echo "✓ User $user_email exists with 'user' role"
|
||||
else
|
||||
echo "::error::User $user_email exists but missing 'user' role"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✓ All domains and users verified successfully"
|
||||
|
||||
- name: Verify unauthenticated JMAP access
|
||||
run: |
|
||||
echo "Testing unauthenticated JMAP endpoint..."
|
||||
|
||||
# Call without authentication (follow redirects with -L)
|
||||
HTTP_CODE=$(curl -s -L \
|
||||
-o /tmp/jmap_response_no_auth.json \
|
||||
-w "%{http_code}" \
|
||||
"http://localhost:8080/.well-known/jmap")
|
||||
|
||||
echo "HTTP Status Code: $HTTP_CODE"
|
||||
echo "JMAP Response:"
|
||||
cat /tmp/jmap_response_no_auth.json
|
||||
echo ""
|
||||
|
||||
# Check if request succeeded
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "::error::JMAP endpoint returned HTTP $HTTP_CODE for unauthenticated request"
|
||||
cat /tmp/jmap_response_no_auth.json || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify username is empty (no authentication)
|
||||
USERNAME=$(cat /tmp/jmap_response_no_auth.json | jq -r '.username // empty')
|
||||
|
||||
if [ -z "$USERNAME" ]; then
|
||||
echo "✓ Unauthenticated access returns empty username"
|
||||
else
|
||||
echo "::warning::Expected empty username for unauthenticated request, got '$USERNAME'"
|
||||
fi
|
||||
|
||||
- name: Verify created user can authenticate
|
||||
run: |
|
||||
echo "Testing user authentication via JMAP endpoint..."
|
||||
|
||||
# Test user1@test1.local authentication (follow redirects with -L)
|
||||
HTTP_CODE=$(curl -s -L \
|
||||
-o /tmp/jmap_response_auth.json \
|
||||
-w "%{http_code}" \
|
||||
-u "user1@test1.local:UserPass123!" \
|
||||
"http://localhost:8080/.well-known/jmap")
|
||||
|
||||
echo "HTTP Status Code: $HTTP_CODE"
|
||||
echo "JMAP Response:"
|
||||
cat /tmp/jmap_response_auth.json
|
||||
echo ""
|
||||
|
||||
# Check if request succeeded
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "::error::JMAP endpoint returned HTTP $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify username field contains our test user
|
||||
USERNAME=$(cat /tmp/jmap_response_auth.json | jq -r '.username // empty')
|
||||
|
||||
if [ "$USERNAME" = "user1@test1.local" ]; then
|
||||
echo "✓ User authentication successful: $USERNAME"
|
||||
else
|
||||
echo "::error::Expected username 'user1@test1.local', got '$USERNAME'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify accounts object is not empty (means user is authenticated)
|
||||
ACCOUNTS=$(cat /tmp/jmap_response_auth.json | jq '.accounts // {}')
|
||||
if [ "$ACCOUNTS" != "{}" ]; then
|
||||
echo "✓ User has active accounts"
|
||||
else
|
||||
echo "::error::User accounts are empty (authentication may have failed)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Show logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Stalwart Service Status ==="
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
sudo systemctl status stalwart --no-pager || true
|
||||
fi
|
||||
|
||||
echo -e "\n=== Process Status ==="
|
||||
ps aux | grep stalwart || true
|
||||
|
||||
echo -e "\n=== Stalwart Logs ==="
|
||||
if command -v journalctl >/dev/null 2>&1; then
|
||||
sudo journalctl -u stalwart -n 100 --no-pager || true
|
||||
else
|
||||
sudo tail -n 100 /opt/stalwart/logs/*.log 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo -e "\n=== Configuration File ==="
|
||||
sudo cat /opt/stalwart/etc/config.toml || true
|
||||
|
||||
echo -e "\n=== Network Ports ==="
|
||||
sudo netstat -tuln | grep -E ':(25|587|465|993|8080)' || true
|
||||
sudo ss -tuln | grep -E ':(25|587|465|993|8080)' || true
|
||||
|
||||
# Test error handling
|
||||
test-error-handling:
|
||||
name: Error Handling Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test invalid JSON (domains)
|
||||
id: test_invalid_domains
|
||||
continue-on-error: true
|
||||
uses: ./
|
||||
with:
|
||||
domains: 'invalid json string'
|
||||
|
||||
- name: Verify invalid JSON was caught
|
||||
run: |
|
||||
if [ "${{ steps.test_invalid_domains.outcome }}" = "success" ]; then
|
||||
echo "::error::Action should have failed with invalid JSON"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Invalid domains JSON was properly rejected"
|
||||
|
||||
- name: Clean up after failed test
|
||||
if: always()
|
||||
run: |
|
||||
# Stop service
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
sudo systemctl stop stalwart || true
|
||||
sudo systemctl disable stalwart || true
|
||||
fi
|
||||
|
||||
# Kill process if still running
|
||||
sudo pkill -9 stalwart || true
|
||||
|
||||
# Remove installation
|
||||
sudo rm -rf /opt/stalwart || true
|
||||
|
||||
# Summary job
|
||||
test-summary:
|
||||
name: Test Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-basic-install, test-full-config]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Check test results
|
||||
run: |
|
||||
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Basic Install: ${{ needs.test-basic-install.result }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Full Config: ${{ needs.test-full-config.result }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Fail if any required test failed
|
||||
if [ "${{ needs.test-basic-install.result }}" != "success" ] || \
|
||||
[ "${{ needs.test-full-config.result }}" != "success" ]; then
|
||||
echo "::error::One or more tests failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ All tests passed!"
|
||||
459
README.md
459
README.md
@@ -1,2 +1,459 @@
|
||||
# action-stalwart-install
|
||||
# Stalwart Mail Server Installation Action
|
||||
|
||||
A GitHub Action to install and configure [Stalwart Mail Server](https://stalw.art/) with optional automated setup for admin password, domains, and users.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **One-step installation** of Stalwart Mail Server
|
||||
- 🔐 **Automated configuration** via REST API
|
||||
- 🌐 **Multi-domain support** with JSON array input
|
||||
- 👥 **Bulk user creation** from JSON configuration
|
||||
- 🔒 **Secure handling** of passwords using GitHub Secrets
|
||||
- 📦 **Cross-platform** support (Linux systemd/init.d, macOS)
|
||||
- ✅ **Production-ready** service setup with auto-start
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Root/sudo access (action must run as root)
|
||||
- Required commands: `curl`, `jq`, `tar`
|
||||
- Linux (Ubuntu, Debian, RHEL, etc.) or macOS
|
||||
- Network access to download Stalwart binaries
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Installation (No Configuration)
|
||||
|
||||
```yaml
|
||||
name: Install Stalwart
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install Stalwart Mail Server
|
||||
uses: Nodarx/action-module-install@v1
|
||||
# This installs with default settings
|
||||
# Web admin: http://localhost:8080/login
|
||||
# Default password: changeme
|
||||
```
|
||||
|
||||
### Full Automated Setup
|
||||
|
||||
```yaml
|
||||
name: Install and Configure Stalwart
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install and Configure Stalwart
|
||||
uses: Nodarx/action-module-install@v1
|
||||
with:
|
||||
# Use GitHub Secrets for sensitive data!
|
||||
admin_password: ${{ secrets.STALWART_ADMIN_PASSWORD }}
|
||||
|
||||
domains: |
|
||||
[
|
||||
{
|
||||
"name": "example.com",
|
||||
"description": "Primary domain"
|
||||
},
|
||||
{
|
||||
"name": "example.org",
|
||||
"description": "Secondary domain"
|
||||
}
|
||||
]
|
||||
|
||||
users: |
|
||||
[
|
||||
{
|
||||
"email": "admin@example.com",
|
||||
"password": "${{ secrets.ADMIN_USER_PASSWORD }}",
|
||||
"name": "System Administrator",
|
||||
"quota": 5368709120
|
||||
},
|
||||
{
|
||||
"email": "support@example.com",
|
||||
"password": "${{ secrets.SUPPORT_USER_PASSWORD }}",
|
||||
"name": "Support Team",
|
||||
"quota": 2147483648
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Required | Default | Description |
|
||||
|-------|----------|---------|-------------|
|
||||
| `admin_password` | No | `changeme` | Admin password for Stalwart web interface. **Use GitHub Secrets!** |
|
||||
| `domains` | No | `""` | JSON array of domains to create. See [Domain Schema](#domain-json-schema) |
|
||||
| `users` | No | `""` | JSON array of users to create. See [User Schema](#user-json-schema) |
|
||||
|
||||
## JSON Schemas
|
||||
|
||||
### Domain JSON Schema
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "example.com", // Required: domain name
|
||||
"description": "Primary domain" // Optional: description
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `name` (string, **required**): Domain name (e.g., "example.com")
|
||||
- `description` (string, optional): Human-readable description
|
||||
|
||||
### User JSON Schema
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"email": "user@example.com", // Required: email address
|
||||
"password": "SecurePass123!", // Required: user password
|
||||
"name": "Full Name", // Optional: display name
|
||||
"quota": 1073741824 // Optional: storage quota in bytes
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `email` (string, **required**): User email address
|
||||
- `password` (string, **required**): User password (use GitHub Secrets!)
|
||||
- `name` (string, optional): Display name (defaults to email if not provided)
|
||||
- `quota` (integer, optional): Storage quota in bytes (default: 1GB = 1073741824)
|
||||
|
||||
**Common quota values:**
|
||||
- 1 GB = `1073741824`
|
||||
- 5 GB = `5368709120`
|
||||
- 10 GB = `10737418240`
|
||||
- 50 GB = `53687091200`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Basic Installation Only
|
||||
|
||||
Install Stalwart without any configuration. You'll configure it manually via web UI.
|
||||
|
||||
```yaml
|
||||
- uses: Nodarx/action-module-install@v1
|
||||
```
|
||||
|
||||
After installation, access the web admin at `http://your-server:8080/login` with username `admin` and password `changeme`.
|
||||
|
||||
### Example 2: Set Admin Password Only
|
||||
|
||||
```yaml
|
||||
- uses: Nodarx/action-module-install@v1
|
||||
with:
|
||||
admin_password: ${{ secrets.STALWART_ADMIN_PASSWORD }}
|
||||
```
|
||||
|
||||
### Example 3: Create Domains Only
|
||||
|
||||
```yaml
|
||||
- uses: Nodarx/action-module-install@v1
|
||||
with:
|
||||
admin_password: ${{ secrets.STALWART_ADMIN_PASSWORD }}
|
||||
domains: |
|
||||
[
|
||||
{"name": "example.com", "description": "Main"},
|
||||
{"name": "example.net", "description": "Secondary"}
|
||||
]
|
||||
```
|
||||
|
||||
### Example 4: Complete Setup with Multiple Users
|
||||
|
||||
```yaml
|
||||
- uses: Nodarx/action-module-install@v1
|
||||
with:
|
||||
admin_password: ${{ secrets.STALWART_ADMIN_PASSWORD }}
|
||||
|
||||
domains: |
|
||||
[
|
||||
{"name": "mycompany.com"},
|
||||
{"name": "mycompany.net"}
|
||||
]
|
||||
|
||||
users: |
|
||||
[
|
||||
{
|
||||
"email": "ceo@mycompany.com",
|
||||
"password": "${{ secrets.CEO_PASSWORD }}",
|
||||
"name": "CEO",
|
||||
"quota": 10737418240
|
||||
},
|
||||
{
|
||||
"email": "team@mycompany.com",
|
||||
"password": "${{ secrets.TEAM_PASSWORD }}",
|
||||
"name": "Team Mailbox",
|
||||
"quota": 5368709120
|
||||
},
|
||||
{
|
||||
"email": "noreply@mycompany.com",
|
||||
"password": "${{ secrets.NOREPLY_PASSWORD }}",
|
||||
"name": "No Reply",
|
||||
"quota": 1073741824
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Example 5: Using JSON from Files
|
||||
|
||||
Store your configuration in separate files:
|
||||
|
||||
```yaml
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Stalwart
|
||||
uses: Nodarx/action-module-install@v1
|
||||
with:
|
||||
admin_password: ${{ secrets.STALWART_ADMIN_PASSWORD }}
|
||||
domains: ${{ readFile('.github/stalwart/domains.json') }}
|
||||
users: ${{ readFile('.github/stalwart/users.json') }}
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### 🔒 Always Use GitHub Secrets
|
||||
|
||||
**NEVER** hardcode passwords in your workflow files!
|
||||
|
||||
```yaml
|
||||
# ❌ WRONG - Password visible in repository
|
||||
- uses: Nodarx/action-module-install@v1
|
||||
with:
|
||||
admin_password: "MyPassword123"
|
||||
|
||||
# ✅ CORRECT - Password stored in GitHub Secrets
|
||||
- uses: Nodarx/action-module-install@v1
|
||||
with:
|
||||
admin_password: ${{ secrets.STALWART_ADMIN_PASSWORD }}
|
||||
```
|
||||
|
||||
### Setting Up GitHub Secrets
|
||||
|
||||
1. Go to your repository → **Settings** → **Secrets and variables** → **Actions**
|
||||
2. Click **New repository secret**
|
||||
3. Add your secrets:
|
||||
- `STALWART_ADMIN_PASSWORD`
|
||||
- `USER1_PASSWORD`
|
||||
- `USER2_PASSWORD`
|
||||
- etc.
|
||||
|
||||
### Password Requirements
|
||||
|
||||
- Use strong, unique passwords (16+ characters)
|
||||
- Include uppercase, lowercase, numbers, and symbols
|
||||
- Never reuse passwords across services
|
||||
- Rotate passwords regularly
|
||||
|
||||
### Additional Security Tips
|
||||
|
||||
- Restrict Stalwart web admin to localhost or VPN
|
||||
- Configure firewall rules (ports 25, 465, 587, 993, 8080)
|
||||
- Enable TLS/SSL for all email protocols
|
||||
- Regularly update Stalwart to latest version
|
||||
- Monitor logs for suspicious activity
|
||||
- Use fail2ban or similar intrusion prevention
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Prerequisites Check**: Validates root access and required commands (`curl`, `jq`, `tar`)
|
||||
2. **Installation**: Downloads and installs Stalwart Mail Server binary
|
||||
3. **Service Setup**: Creates system user and service (systemd/init.d/launchd)
|
||||
4. **API Wait**: Waits for Stalwart API to become available (up to 60 seconds)
|
||||
5. **Authentication**: Authenticates with default password (`changeme`)
|
||||
6. **Password Update**: Changes admin password if provided
|
||||
7. **Domain Creation**: Creates domains via REST API
|
||||
8. **User Creation**: Creates users with passwords and quotas via REST API
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Action Fails: "Required command 'jq' not found"
|
||||
|
||||
Install `jq` before running the action:
|
||||
|
||||
```yaml
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq curl
|
||||
|
||||
- name: Install Stalwart
|
||||
uses: Nodarx/action-module-install@v1
|
||||
```
|
||||
|
||||
### Action Fails: "This action must run as root"
|
||||
|
||||
Use `sudo` in your workflow:
|
||||
|
||||
```yaml
|
||||
- name: Install Stalwart
|
||||
run: sudo -E env "PATH=$PATH" ...
|
||||
```
|
||||
|
||||
Or use a container that runs as root:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
options: --user root
|
||||
```
|
||||
|
||||
### Stalwart API Timeout
|
||||
|
||||
If the API doesn't become available in 60 seconds:
|
||||
- Check system resources (CPU, memory)
|
||||
- Review Stalwart logs: `journalctl -u stalwart -n 50`
|
||||
- Verify port 8080 is not already in use: `netstat -tuln | grep 8080`
|
||||
|
||||
### Domain/User Creation Fails
|
||||
|
||||
- Verify JSON syntax is valid (use a JSON validator)
|
||||
- Check Stalwart logs for detailed errors
|
||||
- Ensure domains are created before users
|
||||
- Verify email addresses match created domains
|
||||
|
||||
### "Failed to authenticate" Error
|
||||
|
||||
- Installation might already be configured
|
||||
- Try accessing web UI manually: `http://localhost:8080/login`
|
||||
- Check if admin password was previously changed
|
||||
- Review configuration script logs
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Installation Path
|
||||
|
||||
The installation path is fixed at `/opt/stalwart` to match Stalwart defaults. If you need a different path, fork this action and modify `STALWART_INSTALL_PATH`.
|
||||
|
||||
### Running in Docker
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ubuntu:22.04
|
||||
options: --privileged
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y curl jq sudo systemd
|
||||
|
||||
- name: Install Stalwart
|
||||
uses: Nodarx/action-module-install@v1
|
||||
with:
|
||||
admin_password: ${{ secrets.STALWART_ADMIN_PASSWORD }}
|
||||
```
|
||||
|
||||
### Post-Installation Configuration
|
||||
|
||||
After installation, Stalwart's web admin is available at `http://localhost:8080/login`. You can:
|
||||
- Configure SMTP, IMAP, POP3 settings
|
||||
- Set up SSL/TLS certificates
|
||||
- Configure spam filters and antivirus
|
||||
- Manage additional domains and users
|
||||
- View logs and statistics
|
||||
|
||||
## Service Management
|
||||
|
||||
### Check Service Status
|
||||
|
||||
```bash
|
||||
# Systemd (most Linux distributions)
|
||||
sudo systemctl status stalwart
|
||||
|
||||
# Init.d (older systems)
|
||||
sudo service stalwart status
|
||||
|
||||
# macOS
|
||||
sudo launchctl list | grep stalwart
|
||||
```
|
||||
|
||||
### Restart Service
|
||||
|
||||
```bash
|
||||
# Systemd
|
||||
sudo systemctl restart stalwart
|
||||
|
||||
# Init.d
|
||||
sudo service stalwart restart
|
||||
|
||||
# macOS
|
||||
sudo launchctl stop system/stalwart.mail
|
||||
sudo launchctl start system/stalwart.mail
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# Systemd
|
||||
sudo journalctl -u stalwart -f
|
||||
|
||||
# Traditional logs
|
||||
sudo tail -f /opt/stalwart/logs/*.log
|
||||
```
|
||||
|
||||
## Uninstallation
|
||||
|
||||
To remove Stalwart:
|
||||
|
||||
```bash
|
||||
# Stop service
|
||||
sudo systemctl stop stalwart
|
||||
sudo systemctl disable stalwart
|
||||
|
||||
# Remove service file
|
||||
sudo rm /etc/systemd/system/stalwart.service
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Remove installation directory
|
||||
sudo rm -rf /opt/stalwart
|
||||
|
||||
# Remove system user (optional)
|
||||
sudo userdel stalwart
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please:
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This action is licensed under the AGPL-3.0 License. See [LICENSE](LICENSE) for details.
|
||||
|
||||
Stalwart Mail Server is developed by [Stalwart Labs](https://stalw.art/) and is licensed under AGPL-3.0-only OR LicenseRef-SEL.
|
||||
|
||||
## Support
|
||||
|
||||
- **Stalwart Documentation**: https://stalw.art/docs
|
||||
- **Issue Tracker**: https://github.com/Nodarx/action-module-install/issues
|
||||
- **Stalwart Community**: https://github.com/stalwartlabs/stalwart/discussions
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Based on the official [Stalwart installation script](https://github.com/stalwartlabs/stalwart)
|
||||
- Thanks to the Stalwart Labs team for creating an excellent mail server
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is an unofficial GitHub Action and is not affiliated with or endorsed by Stalwart Labs.
|
||||
|
||||
83
action.yml
Normal file
83
action.yml
Normal file
@@ -0,0 +1,83 @@
|
||||
name: 'Install Stalwart Mail Server'
|
||||
description: 'Installs and configures Stalwart email server with optional admin password, domains, and users'
|
||||
author: 'Nodarx'
|
||||
|
||||
branding:
|
||||
icon: 'mail'
|
||||
color: 'blue'
|
||||
|
||||
inputs:
|
||||
domains:
|
||||
description: 'JSON array of domains to create. Example: [{"name":"example.com","description":"Primary domain"}]'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
users:
|
||||
description: 'JSON array of users to create. Example: [{"email":"user@example.com","password":"pass123","name":"User Name","quota":1073741824}]'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Check prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::Checking prerequisites"
|
||||
|
||||
# Check if running as root
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "::error::This action must run as root. Use 'sudo' or run in a container with root privileges."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for required commands
|
||||
for cmd in curl jq tar; do
|
||||
if ! command -v $cmd &> /dev/null; then
|
||||
echo "::error::Required command '$cmd' not found. Please install it first."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✓ All prerequisites met"
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Install Stalwart Mail Server
|
||||
shell: bash
|
||||
env:
|
||||
STALWART_INSTALL_PATH: '/opt/stalwart'
|
||||
run: |
|
||||
echo "::group::Installing Stalwart Mail Server"
|
||||
chmod +x "${{ github.action_path }}/scripts/install.sh"
|
||||
"${{ github.action_path }}/scripts/install.sh"
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Configure Stalwart
|
||||
shell: bash
|
||||
if: ${{ inputs.domains != '' || inputs.users != '' }}
|
||||
env:
|
||||
STALWART_DOMAINS: ${{ inputs.domains }}
|
||||
STALWART_USERS: ${{ inputs.users }}
|
||||
STALWART_INSTALL_PATH: '/opt/stalwart'
|
||||
run: |
|
||||
|
||||
# Mask user passwords from JSON
|
||||
if [ -n "$STALWART_USERS" ]; then
|
||||
echo "$STALWART_USERS" | jq -r '.[].password // empty' | while read -r pass; do
|
||||
if [ -n "$pass" ]; then
|
||||
echo "::add-mask::$pass"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "::group::Configuring Stalwart"
|
||||
chmod +x "${{ github.action_path }}/scripts/configure.sh"
|
||||
"${{ github.action_path }}/scripts/configure.sh"
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Display completion message
|
||||
shell: bash
|
||||
run: |
|
||||
HOSTNAME=$(hostname -f 2>/dev/null || echo "localhost")
|
||||
echo "::notice::🎉 Stalwart Mail Server installation complete!"
|
||||
echo "::notice::Web admin: http://$HOSTNAME:8080/login"
|
||||
333
scripts/configure.sh
Executable file
333
scripts/configure.sh
Executable file
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Stalwart Post-Installation Configuration Script
|
||||
# Configures admin password, domains, and users via REST API
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Source utility functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/utils.sh"
|
||||
|
||||
# Configuration
|
||||
readonly STALWART_PATH="${STALWART_INSTALL_PATH:-/opt/stalwart}"
|
||||
readonly API_URL="http://localhost:8080/api"
|
||||
readonly MAX_RETRIES=60
|
||||
readonly RETRY_DELAY=2
|
||||
|
||||
# Read the generated password from installation
|
||||
DEFAULT_ADMIN_PASSWORD="changeme"
|
||||
if [ -f "${STALWART_PATH}/.init_password" ]; then
|
||||
DEFAULT_ADMIN_PASSWORD=$(cat "${STALWART_PATH}/.init_password" | tr -d '\n\r')
|
||||
log_info "Using generated password from installation"
|
||||
# Clean up the password file for security
|
||||
rm -f "${STALWART_PATH}/.init_password"
|
||||
else
|
||||
log_warning "Password file not found, using default password"
|
||||
fi
|
||||
readonly DEFAULT_ADMIN_PASSWORD
|
||||
|
||||
# Environment variables (passed from action.yml)
|
||||
DOMAINS_JSON="${STALWART_DOMAINS:-}"
|
||||
USERS_JSON="${STALWART_USERS:-}"
|
||||
|
||||
# Main configuration function
|
||||
main() {
|
||||
log_info "Starting Stalwart post-installation configuration"
|
||||
|
||||
# Wait for Stalwart API to be ready
|
||||
if ! wait_for_stalwart_api; then
|
||||
log_error "Stalwart API failed to become ready within timeout"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Set current password (start with generated one)
|
||||
local current_password="$DEFAULT_ADMIN_PASSWORD"
|
||||
|
||||
# Test authentication with generated password
|
||||
log_info "Verifying API access with generated password..."
|
||||
if ! test_auth "$current_password"; then
|
||||
log_error "Failed to authenticate with generated password"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "API authentication verified"
|
||||
log_info "📝 Generated admin password: ${current_password}"
|
||||
log_warning "⚠️ Save this password securely - it won't be shown again!"
|
||||
|
||||
# Save admin password to temp file for testing/debugging (remove in production)
|
||||
echo "$current_password" > /tmp/stalwart_admin_password
|
||||
chmod 600 /tmp/stalwart_admin_password
|
||||
|
||||
# Create domains if provided
|
||||
if [ -n "$DOMAINS_JSON" ]; then
|
||||
log_info "Creating domains..."
|
||||
if validate_json "$DOMAINS_JSON"; then
|
||||
create_domains "$current_password" "$DOMAINS_JSON"
|
||||
else
|
||||
log_error "Invalid domains JSON format"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_info "No domains specified, skipping domain creation"
|
||||
fi
|
||||
|
||||
# Create users if provided
|
||||
if [ -n "$USERS_JSON" ]; then
|
||||
log_info "Creating users..."
|
||||
if validate_json "$USERS_JSON"; then
|
||||
create_users "$current_password" "$USERS_JSON"
|
||||
else
|
||||
log_error "Invalid users JSON format"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_info "No users specified, skipping user creation"
|
||||
fi
|
||||
|
||||
log_success "Stalwart configuration completed successfully!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Wait for Stalwart API to be ready
|
||||
wait_for_stalwart_api() {
|
||||
log_info "Waiting for Stalwart API to be ready (timeout: ${MAX_RETRIES}s)..."
|
||||
|
||||
local attempt=0
|
||||
while [ $attempt -lt $MAX_RETRIES ]; do
|
||||
# Try to access the API login endpoint
|
||||
if curl -sf -m 5 "${API_URL%/api}/login" >/dev/null 2>&1; then
|
||||
log_success "Stalwart API is ready"
|
||||
return 0
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
if [ $attempt -lt $MAX_RETRIES ]; then
|
||||
sleep $RETRY_DELAY
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Test authentication with Stalwart API using Basic Auth
|
||||
# Args: $1 = password
|
||||
# Returns: 0 if auth works, 1 otherwise
|
||||
test_auth() {
|
||||
local password="$1"
|
||||
|
||||
local http_code
|
||||
local response
|
||||
|
||||
# Test by querying domains endpoint (works on fresh and configured systems)
|
||||
response=$(curl -s -w "\n%{http_code}" \
|
||||
-u "admin:${password}" \
|
||||
"${API_URL}/principal?types=domain&limit=1")
|
||||
|
||||
http_code=$(echo "$response" | tail -n 1)
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
return 0
|
||||
else
|
||||
log_error "Authentication test failed with HTTP $http_code"
|
||||
response=$(echo "$response" | sed '$d')
|
||||
log_error "Response: $response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create domains from JSON array
|
||||
# Args: $1 = password, $2 = domains JSON array
|
||||
create_domains() {
|
||||
local password="$1"
|
||||
local domains_json="$2"
|
||||
|
||||
local domain_count
|
||||
domain_count=$(echo "$domains_json" | jq 'length' 2>/dev/null)
|
||||
|
||||
if [ -z "$domain_count" ] || [ "$domain_count" = "0" ]; then
|
||||
log_warning "No domains found in JSON array"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Creating $domain_count domain(s)..."
|
||||
|
||||
local index=0
|
||||
local created=0
|
||||
local failed=0
|
||||
|
||||
while [ $index -lt "$domain_count" ]; do
|
||||
local domain
|
||||
domain=$(echo "$domains_json" | jq -c ".[$index]" 2>/dev/null)
|
||||
|
||||
if [ -z "$domain" ] || [ "$domain" = "null" ]; then
|
||||
log_warning "Skipping invalid domain at index $index"
|
||||
failed=$((failed + 1))
|
||||
index=$((index + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
local domain_name
|
||||
domain_name=$(echo "$domain" | jq -r '.name // empty' 2>/dev/null)
|
||||
|
||||
if [ -z "$domain_name" ]; then
|
||||
log_warning "Skipping domain without 'name' field at index $index"
|
||||
failed=$((failed + 1))
|
||||
index=$((index + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Build API payload with required structure
|
||||
local payload
|
||||
payload=$(echo "$domain" | jq '{
|
||||
type: "domain",
|
||||
quota: (.quota // 0),
|
||||
name: .name,
|
||||
description: (.description // ""),
|
||||
secrets: [],
|
||||
emails: [],
|
||||
urls: [],
|
||||
memberOf: [],
|
||||
roles: [],
|
||||
lists: [],
|
||||
members: [],
|
||||
enabledPermissions: [],
|
||||
disabledPermissions: [],
|
||||
externalMembers: []
|
||||
}' 2>/dev/null)
|
||||
|
||||
# Create domain via API
|
||||
if curl -sf -X POST "${API_URL}/principal" \
|
||||
-u "admin:${password}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" >/dev/null 2>&1; then
|
||||
log_success "✓ Created domain: $domain_name"
|
||||
created=$((created + 1))
|
||||
else
|
||||
log_warning "✗ Failed to create domain: $domain_name"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
|
||||
index=$((index + 1))
|
||||
done
|
||||
|
||||
log_info "Domain creation summary: $created created, $failed failed"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Create users from JSON array
|
||||
# Args: $1 = password, $2 = users JSON array
|
||||
create_users() {
|
||||
local password="$1"
|
||||
local users_json="$2"
|
||||
|
||||
local user_count
|
||||
user_count=$(echo "$users_json" | jq 'length' 2>/dev/null)
|
||||
|
||||
if [ -z "$user_count" ] || [ "$user_count" = "0" ]; then
|
||||
log_warning "No users found in JSON array"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Creating $user_count user(s)..."
|
||||
|
||||
local index=0
|
||||
local created=0
|
||||
local failed=0
|
||||
|
||||
while [ $index -lt "$user_count" ]; do
|
||||
local user
|
||||
user=$(echo "$users_json" | jq -c ".[$index]" 2>/dev/null)
|
||||
|
||||
if [ -z "$user" ] || [ "$user" = "null" ]; then
|
||||
log_warning "Skipping invalid user at index $index"
|
||||
failed=$((failed + 1))
|
||||
index=$((index + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
local email
|
||||
email=$(echo "$user" | jq -r '.email // empty' 2>/dev/null)
|
||||
|
||||
if [ -z "$email" ]; then
|
||||
log_warning "Skipping user without 'email' field at index $index"
|
||||
failed=$((failed + 1))
|
||||
index=$((index + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Hash the password with SHA-512 (API requires hashed passwords in secrets array)
|
||||
local hashed_password=""
|
||||
local raw_password
|
||||
raw_password=$(echo "$user" | jq -r '.password // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$raw_password" ]; then
|
||||
if command -v mkpasswd >/dev/null 2>&1; then
|
||||
hashed_password=$(mkpasswd -m sha-512 "$raw_password")
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
hashed_password=$(openssl passwd -6 "$raw_password")
|
||||
else
|
||||
log_warning "Cannot hash password for $email - no mkpasswd or openssl found"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build API payload with required structure
|
||||
local payload
|
||||
if [ -n "$hashed_password" ]; then
|
||||
payload=$(echo "$user" | jq --arg email "$email" --arg pwd "$hashed_password" '{
|
||||
type: "individual",
|
||||
quota: (.quota // 0),
|
||||
name: $email,
|
||||
description: (.name // ""),
|
||||
secrets: [$pwd],
|
||||
emails: [$email],
|
||||
urls: [],
|
||||
memberOf: [],
|
||||
roles: ["user"],
|
||||
lists: [],
|
||||
members: [],
|
||||
enabledPermissions: [],
|
||||
disabledPermissions: [],
|
||||
externalMembers: []
|
||||
}' 2>/dev/null)
|
||||
else
|
||||
payload=$(echo "$user" | jq --arg email "$email" '{
|
||||
type: "individual",
|
||||
quota: (.quota // 0),
|
||||
name: $email,
|
||||
description: (.name // ""),
|
||||
secrets: [],
|
||||
emails: [$email],
|
||||
urls: [],
|
||||
memberOf: [],
|
||||
roles: ["user"],
|
||||
lists: [],
|
||||
members: [],
|
||||
enabledPermissions: [],
|
||||
disabledPermissions: [],
|
||||
externalMembers: []
|
||||
}' 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Create user via API
|
||||
if curl -sf -X POST "${API_URL}/principal" \
|
||||
-u "admin:${password}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" >/dev/null 2>&1; then
|
||||
log_success "✓ Created user: $email"
|
||||
created=$((created + 1))
|
||||
else
|
||||
log_warning "✗ Failed to create user: $email"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
|
||||
index=$((index + 1))
|
||||
done
|
||||
|
||||
log_info "User creation summary: $created created, $failed failed"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Execute main function
|
||||
main "$@"
|
||||
967
scripts/install.sh
Executable file
967
scripts/install.sh
Executable file
@@ -0,0 +1,967 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
# Modified for GitHub Actions integration
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
#
|
||||
|
||||
# Stalwart install script -- based on the rustup installation script.
|
||||
# Enhanced with GitHub Actions logging and error handling
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly BASE_URL="https://github.com/stalwartlabs/stalwart/releases/latest/download"
|
||||
|
||||
main() {
|
||||
downloader --check
|
||||
need_cmd uname
|
||||
need_cmd mktemp
|
||||
need_cmd chmod
|
||||
need_cmd mkdir
|
||||
need_cmd rm
|
||||
need_cmd rmdir
|
||||
need_cmd tar
|
||||
|
||||
# Make sure we are running as root
|
||||
if [ "$(id -u)" -ne 0 ] ; then
|
||||
err "❌ Install failed: This program needs to run as root."
|
||||
fi
|
||||
|
||||
# Detect OS
|
||||
local _os="unknown"
|
||||
local _uname="$(uname)"
|
||||
_account="stalwart"
|
||||
if [ "${_uname}" = "Linux" ]; then
|
||||
_os="linux"
|
||||
elif [ "${_uname}" = "Darwin" ]; then
|
||||
_os="macos"
|
||||
_account="_stalwart"
|
||||
fi
|
||||
|
||||
# Read arguments or use environment variable
|
||||
local _dir="${STALWART_INSTALL_PATH:-/opt/stalwart}"
|
||||
|
||||
# Default component setting
|
||||
local _component="stalwart"
|
||||
|
||||
# Loop through the arguments
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--fdb)
|
||||
_component="stalwart-foundationdb"
|
||||
;;
|
||||
*)
|
||||
if [ -n "$arg" ]; then
|
||||
_dir=$arg
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Detect platform architecture
|
||||
get_architecture || return 1
|
||||
local _arch="$RETVAL"
|
||||
assert_nz "$_arch" "arch"
|
||||
|
||||
# Create directories
|
||||
say "📁 Creating directories..."
|
||||
ensure mkdir -p "$_dir" "$_dir/bin" "$_dir/etc" "$_dir/logs"
|
||||
|
||||
# Download latest binary
|
||||
say "⏳ Downloading ${_component} for ${_arch}..."
|
||||
local _file="${_dir}/bin/stalwart.tar.gz"
|
||||
local _url="${BASE_URL}/${_component}-${_arch}.tar.gz"
|
||||
ensure mkdir -p "$_dir"
|
||||
ensure downloader "$_url" "$_file" "$_arch"
|
||||
ensure tar zxvf "$_file" -C "$_dir/bin" > /dev/null
|
||||
if [ "$_component" = "stalwart-foundationdb" ]; then
|
||||
ignore mv "$_dir/bin/stalwart-foundationdb" "$_dir/bin/stalwart"
|
||||
fi
|
||||
ignore chmod +x "$_dir/bin/stalwart"
|
||||
ignore rm "$_file"
|
||||
|
||||
# Create system account
|
||||
if ! id -u ${_account} > /dev/null 2>&1; then
|
||||
say "🖥️ Creating '${_account}' account..."
|
||||
if [ "${_os}" = "macos" ]; then
|
||||
local _last_uid="$(dscacheutil -q user | grep uid | awk '{print $2}' | sort -n | tail -n 1)"
|
||||
local _last_gid="$(dscacheutil -q group | grep gid | awk '{print $2}' | sort -n | tail -n 1)"
|
||||
local _uid="$((_last_uid+1))"
|
||||
local _gid="$((_last_gid+1))"
|
||||
|
||||
ensure dscl /Local/Default -create Groups/_stalwart
|
||||
ensure dscl /Local/Default -create Groups/_stalwart Password \*
|
||||
ensure dscl /Local/Default -create Groups/_stalwart PrimaryGroupID $_gid
|
||||
ensure dscl /Local/Default -create Groups/_stalwart RealName "Stalwart service"
|
||||
ensure dscl /Local/Default -create Groups/_stalwart RecordName _stalwart stalwart
|
||||
|
||||
ensure dscl /Local/Default -create Users/_stalwart
|
||||
ensure dscl /Local/Default -create Users/_stalwart NFSHomeDirectory /Users/_stalwart
|
||||
ensure dscl /Local/Default -create Users/_stalwart Password \*
|
||||
ensure dscl /Local/Default -create Users/_stalwart PrimaryGroupID $_gid
|
||||
ensure dscl /Local/Default -create Users/_stalwart RealName "Stalwart service"
|
||||
ensure dscl /Local/Default -create Users/_stalwart RecordName _stalwart stalwart
|
||||
ensure dscl /Local/Default -create Users/_stalwart UniqueID $_uid
|
||||
ensure dscl /Local/Default -create Users/_stalwart UserShell /bin/bash
|
||||
|
||||
ensure dscl /Local/Default -delete /Users/_stalwart AuthenticationAuthority
|
||||
ensure dscl /Local/Default -delete /Users/_stalwart PasswordPolicyOptions
|
||||
else
|
||||
ensure useradd ${_account} -s /usr/sbin/nologin -M -r -U
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run init and capture the generated password
|
||||
say "⚙️ Initializing Stalwart configuration..."
|
||||
local _init_output
|
||||
_init_output=$($_dir/bin/stalwart --init "$_dir" 2>&1)
|
||||
echo "$_init_output"
|
||||
|
||||
# Extract the generated password from output
|
||||
# Output format: "🔑 Your administrator account is 'admin' with password 'XXXXX'."
|
||||
local _generated_password
|
||||
_generated_password=$(echo "$_init_output" | sed -n "s/.*with password '\([^']*\)'.*/\1/p")
|
||||
|
||||
# Save the generated password for configure.sh to use
|
||||
if [ -n "$_generated_password" ]; then
|
||||
say "📝 Saving generated password for configuration..."
|
||||
echo "$_generated_password" > "$_dir/.init_password"
|
||||
chmod 644 "$_dir/.init_password"
|
||||
else
|
||||
say "⚠️ Warning: Could not extract generated password"
|
||||
fi
|
||||
|
||||
# Set permissions
|
||||
say "🔐 Setting permissions..."
|
||||
ensure chown -R ${_account}:${_account} "$_dir"
|
||||
ensure chmod -R 755 "$_dir"
|
||||
ensure chmod 700 "$_dir/etc/config.toml"
|
||||
|
||||
# Create service file
|
||||
say "🚀 Starting service..."
|
||||
if [ "${_os}" = "linux" ]; then
|
||||
# Check if systemd is actually running (not just installed)
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
|
||||
say "Using systemd to manage service..."
|
||||
create_service_linux_systemd "$_dir"
|
||||
elif command -v service >/dev/null 2>&1; then
|
||||
say "Using init.d to manage service..."
|
||||
create_service_linux_initd "$_dir"
|
||||
else
|
||||
say "No service manager detected, starting manually..."
|
||||
start_service_manually "$_dir" "${_account}"
|
||||
fi
|
||||
elif [ "${_os}" = "macos" ]; then
|
||||
create_service_macos "$_dir"
|
||||
fi
|
||||
|
||||
# Wait for service to be responsive
|
||||
say "⏳ Waiting for Stalwart to start..."
|
||||
local _wait_attempts=0
|
||||
local _max_wait=30
|
||||
while [ $_wait_attempts -lt $_max_wait ]; do
|
||||
if curl -sf -m 2 "http://localhost:8080/login" >/dev/null 2>&1; then
|
||||
say "✓ Stalwart service is responding"
|
||||
break
|
||||
fi
|
||||
_wait_attempts=$((_wait_attempts + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Installation complete
|
||||
local _host=$(hostname -f 2>/dev/null || echo "localhost")
|
||||
say "✅ Installation complete! Stalwart service is running."
|
||||
say "📝 Web admin will be available at: http://$_host:8080/login"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Functions to create service files
|
||||
create_service_linux_systemd() {
|
||||
local _dir="$1"
|
||||
cat <<EOF | sed "s|__PATH__|$_dir|g" > /etc/systemd/system/stalwart.service
|
||||
[Unit]
|
||||
Description=Stalwart
|
||||
Conflicts=postfix.service sendmail.service exim4.service
|
||||
ConditionPathExists=__PATH__/etc/config.toml
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
LimitNOFILE=65536
|
||||
KillMode=process
|
||||
KillSignal=SIGINT
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
ExecStart=__PATH__/bin/stalwart --config=__PATH__/etc/config.toml
|
||||
SyslogIdentifier=stalwart
|
||||
User=stalwart
|
||||
Group=stalwart
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
if systemctl daemon-reload 2>/dev/null && \
|
||||
systemctl enable stalwart.service 2>/dev/null && \
|
||||
systemctl restart stalwart.service 2>/dev/null; then
|
||||
say "✓ Service started with systemd"
|
||||
else
|
||||
say "⚠ systemd commands failed, falling back to manual start"
|
||||
start_service_manually "$_dir" "stalwart"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to start service manually when systemd is not available
|
||||
start_service_manually() {
|
||||
local _dir="$1"
|
||||
local _account="$2"
|
||||
|
||||
say "Starting Stalwart manually in background..."
|
||||
|
||||
# Create a simple wrapper script
|
||||
cat > /usr/local/bin/stalwart-service <<EOF
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Increase file descriptor limit
|
||||
ulimit -n 65536
|
||||
|
||||
# Change to service user and run stalwart
|
||||
if command -v runuser >/dev/null 2>&1; then
|
||||
exec runuser -u ${_account} -- ${_dir}/bin/stalwart --config=${_dir}/etc/config.toml
|
||||
elif command -v su >/dev/null 2>&1; then
|
||||
exec su -s /bin/bash ${_account} -c "${_dir}/bin/stalwart --config=${_dir}/etc/config.toml"
|
||||
else
|
||||
# Fallback: run as current user
|
||||
exec ${_dir}/bin/stalwart --config=${_dir}/etc/config.toml
|
||||
fi
|
||||
EOF
|
||||
|
||||
chmod +x /usr/local/bin/stalwart-service
|
||||
|
||||
# Start in background and redirect output
|
||||
nohup /usr/local/bin/stalwart-service > ${_dir}/logs/stalwart.out 2>&1 &
|
||||
local _pid=$!
|
||||
|
||||
# Save PID for later management
|
||||
echo $_pid > /var/run/stalwart.pid
|
||||
|
||||
# Give it a moment to start
|
||||
sleep 2
|
||||
|
||||
# Verify it's running
|
||||
if kill -0 $_pid 2>/dev/null; then
|
||||
say "✓ Stalwart started successfully (PID: $_pid)"
|
||||
else
|
||||
say "⚠ Warning: Stalwart may not have started successfully"
|
||||
fi
|
||||
}
|
||||
|
||||
create_service_linux_initd() {
|
||||
local _dir="$1"
|
||||
cat <<"EOF" | sed "s|__PATH__|$_dir|g" > /etc/init.d/stalwart
|
||||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: stalwart
|
||||
# Required-Start: $network
|
||||
# Required-Stop: $network
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Stalwart Server
|
||||
# Description: Starts and stops the Stalwart Server
|
||||
# Conflicts: postfix sendmail
|
||||
### END INIT INFO
|
||||
|
||||
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
||||
|
||||
. /lib/init/vars.sh
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
# Service Config
|
||||
DAEMON=__PATH__/bin/stalwart
|
||||
DAEMON_ARGS="--config=__PATH__/etc/config.toml"
|
||||
PIDFILE=/var/run/stalwart.pid
|
||||
ULIMIT_NOFILE=65536
|
||||
|
||||
# Exit if the package is not installed
|
||||
[ -x "$DAEMON" ] || exit 0
|
||||
|
||||
# Exit if config file doesn't exist
|
||||
[ -f "__PATH__/etc/config.toml" ] || exit 0
|
||||
|
||||
# Read configuration variable file if it is present
|
||||
[ -r /etc/default/stalwart ] && . /etc/default/stalwart
|
||||
|
||||
# Increase file descriptor limit
|
||||
ulimit -n $ULIMIT_NOFILE
|
||||
|
||||
do_start()
|
||||
{
|
||||
# Return
|
||||
# 0 if daemon has been started
|
||||
# 1 if daemon was already running
|
||||
# 2 if daemon could not be started
|
||||
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|
||||
|| return 1
|
||||
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \
|
||||
--background --make-pidfile --chuid stalwart:stalwart \
|
||||
-- $DAEMON_ARGS \
|
||||
|| return 2
|
||||
}
|
||||
|
||||
do_stop()
|
||||
{
|
||||
# Return
|
||||
# 0 if daemon has been stopped
|
||||
# 1 if daemon was already stopped
|
||||
# 2 if daemon could not be stopped
|
||||
# other if a failure occurred
|
||||
start-stop-daemon --stop --quiet --retry=INT/30/KILL/5 --pidfile $PIDFILE --name stalwart
|
||||
RETVAL="$?"
|
||||
[ "$RETVAL" = 2 ] && return 2
|
||||
# Wait for children to finish too if this is a daemon that forks
|
||||
# and if the daemon is only ever run from this initscript.
|
||||
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
|
||||
[ "$?" = 2 ] && return 2
|
||||
# Many daemons don't delete their pidfiles when they exit.
|
||||
rm -f $PIDFILE
|
||||
return "$RETVAL"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
[ "$VERBOSE" != no ] && log_daemon_msg "Starting Stalwart Server" "stalwart"
|
||||
do_start
|
||||
case "$?" in
|
||||
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
|
||||
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
|
||||
esac
|
||||
;;
|
||||
stop)
|
||||
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping Stalwart Server" "stalwart"
|
||||
do_stop
|
||||
case "$?" in
|
||||
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
|
||||
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
|
||||
esac
|
||||
;;
|
||||
status)
|
||||
status_of_proc "$DAEMON" "stalwart" && exit 0 || exit $?
|
||||
;;
|
||||
restart)
|
||||
log_daemon_msg "Restarting Stalwart Server" "stalwart"
|
||||
do_stop
|
||||
case "$?" in
|
||||
0|1)
|
||||
do_start
|
||||
case "$?" in
|
||||
0) log_end_msg 0 ;;
|
||||
1) log_end_msg 1 ;; # Old process is still running
|
||||
*) log_end_msg 1 ;; # Failed to start
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
# Failed to stop
|
||||
log_end_msg 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "Usage: /etc/init.d/stalwart {start|stop|status|restart}" >&2
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x /etc/init.d/stalwart
|
||||
|
||||
cat <<EOF > /etc/default/stalwart
|
||||
# Configuration for Stalwart init script being run during
|
||||
# the boot sequence
|
||||
|
||||
# Set to 'yes' to enable additional verbosity
|
||||
#VERBOSE=no
|
||||
EOF
|
||||
update-rc.d stalwart defaults
|
||||
service stalwart start
|
||||
}
|
||||
|
||||
create_service_macos() {
|
||||
local _dir="$1"
|
||||
cat <<EOF | sed "s|__PATH__|$_dir|g" > /Library/LaunchAgents/stalwart.mail.plist
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>stalwart.mail</string>
|
||||
<key>ServiceDescription</key>
|
||||
<string>Stalwart</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>__PATH__/bin/stalwart</string>
|
||||
<string>--config=__PATH__/etc/config.toml</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
launchctl load /Library/LaunchAgents/stalwart.mail.plist
|
||||
launchctl enable system/stalwart.mail
|
||||
launchctl start system/stalwart.mail
|
||||
}
|
||||
|
||||
|
||||
get_architecture() {
|
||||
local _ostype _cputype _bitness _arch _clibtype
|
||||
_ostype="$(uname -s)"
|
||||
_cputype="$(uname -m)"
|
||||
_clibtype="gnu"
|
||||
|
||||
if [ "$_ostype" = Linux ]; then
|
||||
if [ "$(uname -o)" = Android ]; then
|
||||
_ostype=Android
|
||||
fi
|
||||
if ldd --version 2>&1 | grep -q 'musl'; then
|
||||
_clibtype="musl"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$_ostype" = Darwin ] && [ "$_cputype" = i386 ]; then
|
||||
# Darwin `uname -m` lies
|
||||
if sysctl hw.optional.x86_64 | grep -q ': 1'; then
|
||||
_cputype=x86_64
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$_ostype" = SunOS ]; then
|
||||
# Both Solaris and illumos presently announce as "SunOS" in "uname -s"
|
||||
# so use "uname -o" to disambiguate. We use the full path to the
|
||||
# system uname in case the user has coreutils uname first in PATH,
|
||||
# which has historically sometimes printed the wrong value here.
|
||||
if [ "$(/usr/bin/uname -o)" = illumos ]; then
|
||||
_ostype=illumos
|
||||
fi
|
||||
|
||||
# illumos systems have multi-arch userlands, and "uname -m" reports the
|
||||
# machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86
|
||||
# systems. Check for the native (widest) instruction set on the
|
||||
# running kernel:
|
||||
if [ "$_cputype" = i86pc ]; then
|
||||
_cputype="$(isainfo -n)"
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$_ostype" in
|
||||
|
||||
Android)
|
||||
_ostype=linux-android
|
||||
;;
|
||||
|
||||
Linux)
|
||||
check_proc
|
||||
_ostype=unknown-linux-$_clibtype
|
||||
_bitness=$(get_bitness)
|
||||
;;
|
||||
|
||||
FreeBSD)
|
||||
_ostype=unknown-freebsd
|
||||
;;
|
||||
|
||||
NetBSD)
|
||||
_ostype=unknown-netbsd
|
||||
;;
|
||||
|
||||
DragonFly)
|
||||
_ostype=unknown-dragonfly
|
||||
;;
|
||||
|
||||
Darwin)
|
||||
_ostype=apple-darwin
|
||||
;;
|
||||
|
||||
illumos)
|
||||
_ostype=unknown-illumos
|
||||
;;
|
||||
|
||||
MINGW* | MSYS* | CYGWIN* | Windows_NT)
|
||||
_ostype=pc-windows-gnu
|
||||
;;
|
||||
|
||||
*)
|
||||
err "unrecognized OS type: $_ostype"
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
case "$_cputype" in
|
||||
|
||||
i386 | i486 | i686 | i786 | x86)
|
||||
_cputype=i686
|
||||
;;
|
||||
|
||||
xscale | arm)
|
||||
_cputype=arm
|
||||
if [ "$_ostype" = "linux-android" ]; then
|
||||
_ostype=linux-androideabi
|
||||
fi
|
||||
;;
|
||||
|
||||
armv6l)
|
||||
_cputype=arm
|
||||
if [ "$_ostype" = "linux-android" ]; then
|
||||
_ostype=linux-androideabi
|
||||
else
|
||||
_ostype="${_ostype}eabihf"
|
||||
fi
|
||||
;;
|
||||
|
||||
armv7l | armv8l)
|
||||
_cputype=armv7
|
||||
if [ "$_ostype" = "linux-android" ]; then
|
||||
_ostype=linux-androideabi
|
||||
else
|
||||
_ostype="${_ostype}eabihf"
|
||||
fi
|
||||
;;
|
||||
|
||||
aarch64 | arm64)
|
||||
_cputype=aarch64
|
||||
;;
|
||||
|
||||
x86_64 | x86-64 | x64 | amd64)
|
||||
_cputype=x86_64
|
||||
;;
|
||||
|
||||
mips)
|
||||
_cputype=$(get_endianness mips '' el)
|
||||
;;
|
||||
|
||||
mips64)
|
||||
if [ "$_bitness" -eq 64 ]; then
|
||||
# only n64 ABI is supported for now
|
||||
_ostype="${_ostype}abi64"
|
||||
_cputype=$(get_endianness mips64 '' el)
|
||||
fi
|
||||
;;
|
||||
|
||||
ppc)
|
||||
_cputype=powerpc
|
||||
;;
|
||||
|
||||
ppc64)
|
||||
_cputype=powerpc64
|
||||
;;
|
||||
|
||||
ppc64le)
|
||||
_cputype=powerpc64le
|
||||
;;
|
||||
|
||||
s390x)
|
||||
_cputype=s390x
|
||||
;;
|
||||
riscv64)
|
||||
_cputype=riscv64gc
|
||||
;;
|
||||
*)
|
||||
err "unknown CPU type: $_cputype"
|
||||
|
||||
esac
|
||||
|
||||
# Detect 64-bit linux with 32-bit userland
|
||||
if [ "${_ostype}" = unknown-linux-gnu ] && [ "${_bitness}" -eq 32 ]; then
|
||||
case $_cputype in
|
||||
x86_64)
|
||||
if [ -n "${RUSTUP_CPUTYPE:-}" ]; then
|
||||
_cputype="$RUSTUP_CPUTYPE"
|
||||
else {
|
||||
# 32-bit executable for amd64 = x32
|
||||
if is_host_amd64_elf; then {
|
||||
echo "This host is running an x32 userland; as it stands, x32 support is poor," 1>&2
|
||||
echo "and there isn't a native toolchain -- you will have to install" 1>&2
|
||||
echo "multiarch compatibility with i686 and/or amd64, then select one" 1>&2
|
||||
echo "by re-running this script with the RUSTUP_CPUTYPE environment variable" 1>&2
|
||||
echo "set to i686 or x86_64, respectively." 1>&2
|
||||
echo 1>&2
|
||||
echo "You will be able to add an x32 target after installation by running" 1>&2
|
||||
echo " rustup target add x86_64-unknown-linux-gnux32" 1>&2
|
||||
exit 1
|
||||
}; else
|
||||
_cputype=i686
|
||||
fi
|
||||
}; fi
|
||||
;;
|
||||
mips64)
|
||||
_cputype=$(get_endianness mips '' el)
|
||||
;;
|
||||
powerpc64)
|
||||
_cputype=powerpc
|
||||
;;
|
||||
aarch64)
|
||||
_cputype=armv7
|
||||
if [ "$_ostype" = "linux-android" ]; then
|
||||
_ostype=linux-androideabi
|
||||
else
|
||||
_ostype="${_ostype}eabihf"
|
||||
fi
|
||||
;;
|
||||
riscv64gc)
|
||||
err "riscv64 with 32-bit userland unsupported"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Detect armv7 but without the CPU features Rust needs in that build,
|
||||
# and fall back to arm.
|
||||
# See https://github.com/rust-lang/rustup.rs/issues/587.
|
||||
if [ "$_ostype" = "unknown-linux-gnueabihf" ] && [ "$_cputype" = armv7 ]; then
|
||||
if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then
|
||||
# At least one processor does not have NEON.
|
||||
_cputype=arm
|
||||
fi
|
||||
fi
|
||||
|
||||
_arch="${_cputype}-${_ostype}"
|
||||
|
||||
RETVAL="$_arch"
|
||||
}
|
||||
|
||||
check_proc() {
|
||||
# Check for /proc by looking for the /proc/self/exe link
|
||||
# This is only run on Linux
|
||||
if ! test -L /proc/self/exe ; then
|
||||
err "fatal: Unable to find /proc/self/exe. Is /proc mounted? Installation cannot proceed without /proc."
|
||||
fi
|
||||
}
|
||||
|
||||
get_bitness() {
|
||||
need_cmd head
|
||||
# Architecture detection without dependencies beyond coreutils.
|
||||
# ELF files start out "\x7fELF", and the following byte is
|
||||
# 0x01 for 32-bit and
|
||||
# 0x02 for 64-bit.
|
||||
# The printf builtin on some shells like dash only supports octal
|
||||
# escape sequences, so we use those.
|
||||
local _current_exe_head
|
||||
_current_exe_head=$(head -c 5 /proc/self/exe )
|
||||
if [ "$_current_exe_head" = "$(printf '\177ELF\001')" ]; then
|
||||
echo 32
|
||||
elif [ "$_current_exe_head" = "$(printf '\177ELF\002')" ]; then
|
||||
echo 64
|
||||
else
|
||||
err "unknown platform bitness"
|
||||
fi
|
||||
}
|
||||
|
||||
is_host_amd64_elf() {
|
||||
need_cmd head
|
||||
need_cmd tail
|
||||
# ELF e_machine detection without dependencies beyond coreutils.
|
||||
# Two-byte field at offset 0x12 indicates the CPU,
|
||||
# but we're interested in it being 0x3E to indicate amd64, or not that.
|
||||
local _current_exe_machine
|
||||
_current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1)
|
||||
[ "$_current_exe_machine" = "$(printf '\076')" ]
|
||||
}
|
||||
|
||||
get_endianness() {
|
||||
local cputype=$1
|
||||
local suffix_eb=$2
|
||||
local suffix_el=$3
|
||||
|
||||
# detect endianness without od/hexdump, like get_bitness() does.
|
||||
need_cmd head
|
||||
need_cmd tail
|
||||
|
||||
local _current_exe_endianness
|
||||
_current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)"
|
||||
if [ "$_current_exe_endianness" = "$(printf '\001')" ]; then
|
||||
echo "${cputype}${suffix_el}"
|
||||
elif [ "$_current_exe_endianness" = "$(printf '\002')" ]; then
|
||||
echo "${cputype}${suffix_eb}"
|
||||
else
|
||||
err "unknown platform endianness"
|
||||
fi
|
||||
}
|
||||
|
||||
say() {
|
||||
printf '%s\n' "$1"
|
||||
}
|
||||
|
||||
err() {
|
||||
say "::error::$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
if ! check_cmd "$1"; then
|
||||
err "need '$1' (command not found)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_cmd() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
assert_nz() {
|
||||
if [ -z "$1" ]; then err "assert_nz $2"; fi
|
||||
}
|
||||
|
||||
# Run a command that should never fail. If the command fails execution
|
||||
# will immediately terminate with an error showing the failing
|
||||
# command.
|
||||
ensure() {
|
||||
if ! "$@"; then err "command failed: $*"; fi
|
||||
}
|
||||
|
||||
# This wraps curl or wget. Try curl first, if not installed,
|
||||
# use wget instead.
|
||||
downloader() {
|
||||
local _dld
|
||||
local _ciphersuites
|
||||
local _err
|
||||
local _status
|
||||
local _retry
|
||||
if check_cmd curl; then
|
||||
_dld=curl
|
||||
elif check_cmd wget; then
|
||||
_dld=wget
|
||||
else
|
||||
_dld='curl or wget' # to be used in error message of need_cmd
|
||||
fi
|
||||
|
||||
if [ "$1" = --check ]; then
|
||||
need_cmd "$_dld"
|
||||
elif [ "$_dld" = curl ]; then
|
||||
check_curl_for_retry_support
|
||||
_retry="$RETVAL"
|
||||
get_ciphersuites_for_curl
|
||||
_ciphersuites="$RETVAL"
|
||||
if [ -n "$_ciphersuites" ]; then
|
||||
_err=$(curl $_retry --proto '=https' --tlsv1.2 --ciphers "$_ciphersuites" --silent --show-error --fail --location "$1" --output "$2" 2>&1)
|
||||
_status=$?
|
||||
else
|
||||
echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure"
|
||||
if ! check_help_for "$3" curl --proto --tlsv1.2; then
|
||||
echo "Warning: Not enforcing TLS v1.2, this is potentially less secure"
|
||||
_err=$(curl $_retry --silent --show-error --fail --location "$1" --output "$2" 2>&1)
|
||||
_status=$?
|
||||
else
|
||||
_err=$(curl $_retry --proto '=https' --tlsv1.2 --silent --show-error --fail --location "$1" --output "$2" 2>&1)
|
||||
_status=$?
|
||||
fi
|
||||
fi
|
||||
if [ -n "$_err" ]; then
|
||||
if echo "$_err" | grep -q 404; then
|
||||
err "❌ Binary for platform '$3' not found, this platform may be unsupported."
|
||||
else
|
||||
echo "$_err" >&2
|
||||
fi
|
||||
fi
|
||||
return $_status
|
||||
elif [ "$_dld" = wget ]; then
|
||||
if [ "$(wget -V 2>&1|head -2|tail -1|cut -f1 -d" ")" = "BusyBox" ]; then
|
||||
echo "Warning: using the BusyBox version of wget. Not enforcing strong cipher suites for TLS or TLS v1.2, this is potentially less secure"
|
||||
_err=$(wget "$1" -O "$2" 2>&1)
|
||||
_status=$?
|
||||
else
|
||||
get_ciphersuites_for_wget
|
||||
_ciphersuites="$RETVAL"
|
||||
if [ -n "$_ciphersuites" ]; then
|
||||
_err=$(wget --https-only --secure-protocol=TLSv1_2 --ciphers "$_ciphersuites" "$1" -O "$2" 2>&1)
|
||||
_status=$?
|
||||
else
|
||||
echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure"
|
||||
if ! check_help_for "$3" wget --https-only --secure-protocol; then
|
||||
echo "Warning: Not enforcing TLS v1.2, this is potentially less secure"
|
||||
_err=$(wget "$1" -O "$2" 2>&1)
|
||||
_status=$?
|
||||
else
|
||||
_err=$(wget --https-only --secure-protocol=TLSv1_2 "$1" -O "$2" 2>&1)
|
||||
_status=$?
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ -n "$_err" ]; then
|
||||
if echo "$_err" | grep -q ' 404 Not Found'; then
|
||||
err "❌ Binary for platform '$3' not found, this platform may be unsupported."
|
||||
else
|
||||
echo "$_err" >&2
|
||||
fi
|
||||
fi
|
||||
return $_status
|
||||
else
|
||||
err "Unknown downloader" # should not reach here
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if curl supports the --retry flag, then pass it to the curl invocation.
|
||||
check_curl_for_retry_support() {
|
||||
local _retry_supported=""
|
||||
# "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc.
|
||||
if check_help_for "notspecified" "curl" "--retry"; then
|
||||
_retry_supported="--retry 3"
|
||||
fi
|
||||
|
||||
RETVAL="$_retry_supported"
|
||||
|
||||
}
|
||||
|
||||
check_help_for() {
|
||||
local _arch
|
||||
local _cmd
|
||||
local _arg
|
||||
_arch="$1"
|
||||
shift
|
||||
_cmd="$1"
|
||||
shift
|
||||
|
||||
local _category
|
||||
if "$_cmd" --help | grep -q 'For all options use the manual or "--help all".'; then
|
||||
_category="all"
|
||||
else
|
||||
_category=""
|
||||
fi
|
||||
|
||||
case "$_arch" in
|
||||
|
||||
*darwin*)
|
||||
if check_cmd sw_vers; then
|
||||
case $(sw_vers -productVersion) in
|
||||
10.*)
|
||||
# If we're running on macOS, older than 10.13, then we always
|
||||
# fail to find these options to force fallback
|
||||
if [ "$(sw_vers -productVersion | cut -d. -f2)" -lt 13 ]; then
|
||||
# Older than 10.13
|
||||
echo "Warning: Detected macOS platform older than 10.13"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
11.*)
|
||||
# We assume Big Sur will be OK for now
|
||||
;;
|
||||
*)
|
||||
# Unknown product version, warn and continue
|
||||
echo "Warning: Detected unknown macOS major version: $(sw_vers -productVersion)"
|
||||
echo "Warning TLS capabilities detection may fail"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
for _arg in "$@"; do
|
||||
if ! "$_cmd" --help $_category | grep -q -- "$_arg"; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
true # not strictly needed
|
||||
}
|
||||
|
||||
# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites
|
||||
# if support by local tools is detected. Detection currently supports these curl backends:
|
||||
# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty.
|
||||
get_ciphersuites_for_curl() {
|
||||
if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then
|
||||
# user specified custom cipher suites, assume they know what they're doing
|
||||
RETVAL="$RUSTUP_TLS_CIPHERSUITES"
|
||||
return
|
||||
fi
|
||||
|
||||
local _openssl_syntax="no"
|
||||
local _gnutls_syntax="no"
|
||||
local _backend_supported="yes"
|
||||
if curl -V | grep -q ' OpenSSL/'; then
|
||||
_openssl_syntax="yes"
|
||||
elif curl -V | grep -iq ' LibreSSL/'; then
|
||||
_openssl_syntax="yes"
|
||||
elif curl -V | grep -iq ' BoringSSL/'; then
|
||||
_openssl_syntax="yes"
|
||||
elif curl -V | grep -iq ' GnuTLS/'; then
|
||||
_gnutls_syntax="yes"
|
||||
else
|
||||
_backend_supported="no"
|
||||
fi
|
||||
|
||||
local _args_supported="no"
|
||||
if [ "$_backend_supported" = "yes" ]; then
|
||||
# "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc.
|
||||
if check_help_for "notspecified" "curl" "--tlsv1.2" "--ciphers" "--proto"; then
|
||||
_args_supported="yes"
|
||||
fi
|
||||
fi
|
||||
|
||||
local _cs=""
|
||||
if [ "$_args_supported" = "yes" ]; then
|
||||
if [ "$_openssl_syntax" = "yes" ]; then
|
||||
_cs=$(get_strong_ciphersuites_for "openssl")
|
||||
elif [ "$_gnutls_syntax" = "yes" ]; then
|
||||
_cs=$(get_strong_ciphersuites_for "gnutls")
|
||||
fi
|
||||
fi
|
||||
|
||||
RETVAL="$_cs"
|
||||
}
|
||||
|
||||
# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites
|
||||
# if support by local tools is detected. Detection currently supports these wget backends:
|
||||
# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty.
|
||||
get_ciphersuites_for_wget() {
|
||||
if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then
|
||||
# user specified custom cipher suites, assume they know what they're doing
|
||||
RETVAL="$RUSTUP_TLS_CIPHERSUITES"
|
||||
return
|
||||
fi
|
||||
|
||||
local _cs=""
|
||||
if wget -V | grep -q '\-DHAVE_LIBSSL'; then
|
||||
# "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc.
|
||||
if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then
|
||||
_cs=$(get_strong_ciphersuites_for "openssl")
|
||||
fi
|
||||
elif wget -V | grep -q '\-DHAVE_LIBGNUTLS'; then
|
||||
# "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc.
|
||||
if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then
|
||||
_cs=$(get_strong_ciphersuites_for "gnutls")
|
||||
fi
|
||||
fi
|
||||
|
||||
RETVAL="$_cs"
|
||||
}
|
||||
|
||||
# Return strong TLS 1.2-1.3 cipher suites in OpenSSL or GnuTLS syntax. TLS 1.2
|
||||
# excludes non-ECDHE and non-AEAD cipher suites. DHE is excluded due to bad
|
||||
# DH params often found on servers (see RFC 7919). Sequence matches or is
|
||||
# similar to Firefox 68 ESR with weak cipher suites disabled via about:config.
|
||||
# $1 must be openssl or gnutls.
|
||||
get_strong_ciphersuites_for() {
|
||||
if [ "$1" = "openssl" ]; then
|
||||
# OpenSSL is forgiving of unknown values, no problems with TLS 1.3 values on versions that don't support it yet.
|
||||
echo "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
|
||||
elif [ "$1" = "gnutls" ]; then
|
||||
# GnuTLS isn't forgiving of unknown values, so this may require a GnuTLS version that supports TLS 1.3 even if wget doesn't.
|
||||
# Begin with SECURE128 (and higher) then remove/add to build cipher suites. Produces same 9 cipher suites as OpenSSL but in slightly different order.
|
||||
echo "SECURE128:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-DTLS-ALL:-CIPHER-ALL:-MAC-ALL:-KX-ALL:+AEAD:+ECDHE-ECDSA:+ECDHE-RSA:+AES-128-GCM:+CHACHA20-POLY1305:+AES-256-GCM"
|
||||
fi
|
||||
}
|
||||
|
||||
# This is just for indicating that commands' results are being
|
||||
# intentionally ignored. Usually, because it's being executed
|
||||
# as part of error handling.
|
||||
ignore() {
|
||||
"$@"
|
||||
}
|
||||
|
||||
main "$@" || exit 1
|
||||
289
scripts/utils.sh
Executable file
289
scripts/utils.sh
Executable file
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Utility functions for Stalwart installation and configuration scripts
|
||||
#
|
||||
|
||||
# Color codes for output (if terminal supports it)
|
||||
if [ -t 1 ]; then
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
else
|
||||
RED=''
|
||||
GREEN=''
|
||||
YELLOW=''
|
||||
BLUE=''
|
||||
NC=''
|
||||
fi
|
||||
|
||||
# Logging functions with GitHub Actions workflow commands support
|
||||
|
||||
log_info() {
|
||||
local message="$1"
|
||||
echo -e "${BLUE}ℹ${NC} ${message}"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
local message="$1"
|
||||
echo -e "${GREEN}✓${NC} ${message}"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
local message="$1"
|
||||
echo -e "${YELLOW}⚠${NC} ${message}"
|
||||
# Also output as GitHub Actions warning
|
||||
echo "::warning::${message}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
local message="$1"
|
||||
echo -e "${RED}✗${NC} ${message}" >&2
|
||||
# Also output as GitHub Actions error
|
||||
echo "::error::${message}" >&2
|
||||
}
|
||||
|
||||
log_debug() {
|
||||
local message="$1"
|
||||
if [ "${DEBUG:-false}" = "true" ]; then
|
||||
echo -e "${BLUE}[DEBUG]${NC} ${message}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate JSON string
|
||||
# Args: $1 = JSON string
|
||||
# Returns: 0 if valid, 1 if invalid
|
||||
validate_json() {
|
||||
local json_string="$1"
|
||||
|
||||
if [ -z "$json_string" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! echo "$json_string" | jq empty >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check if a command exists
|
||||
# Args: $1 = command name
|
||||
# Returns: 0 if exists, 1 if not
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Ensure a command exists, exit with error if not
|
||||
# Args: $1 = command name
|
||||
require_command() {
|
||||
local cmd="$1"
|
||||
|
||||
if ! command_exists "$cmd"; then
|
||||
log_error "Required command '$cmd' not found. Please install it first."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Wait for a TCP port to be available
|
||||
# Args: $1 = host, $2 = port, $3 = timeout (seconds)
|
||||
# Returns: 0 if available, 1 if timeout
|
||||
wait_for_port() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
local timeout="${3:-30}"
|
||||
|
||||
local elapsed=0
|
||||
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
if timeout 1 bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if a URL is accessible
|
||||
# Args: $1 = URL, $2 = timeout (seconds, optional)
|
||||
# Returns: 0 if accessible, 1 if not
|
||||
check_url() {
|
||||
local url="$1"
|
||||
local timeout="${2:-5}"
|
||||
|
||||
if curl -sf -m "$timeout" "$url" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Retry a command multiple times
|
||||
# Args: $1 = max attempts, $2 = delay between attempts, $3+ = command and args
|
||||
# Returns: 0 if command succeeds, 1 if all attempts fail
|
||||
retry_command() {
|
||||
local max_attempts="$1"
|
||||
local delay="$2"
|
||||
shift 2
|
||||
local cmd=("$@")
|
||||
|
||||
local attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if "${cmd[@]}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ $attempt -lt $max_attempts ]; then
|
||||
log_debug "Command failed (attempt $attempt/$max_attempts), retrying in ${delay}s..."
|
||||
sleep "$delay"
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log_error "Command failed after $max_attempts attempts: ${cmd[*]}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Mask sensitive data in logs (for GitHub Actions)
|
||||
# Args: $1 = sensitive string
|
||||
mask_secret() {
|
||||
local secret="$1"
|
||||
|
||||
if [ -n "$secret" ]; then
|
||||
echo "::add-mask::${secret}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create a GitHub Actions group (collapsible section in logs)
|
||||
# Args: $1 = group name
|
||||
start_group() {
|
||||
local group_name="$1"
|
||||
echo "::group::${group_name}"
|
||||
}
|
||||
|
||||
# End a GitHub Actions group
|
||||
end_group() {
|
||||
echo "::endgroup::"
|
||||
}
|
||||
|
||||
# Set a GitHub Actions output
|
||||
# Args: $1 = output name, $2 = output value
|
||||
set_output() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
echo "${name}=${value}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::set-output name=${name}::${value}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate a random password
|
||||
# Args: $1 = length (default: 16)
|
||||
# Returns: random password on stdout
|
||||
generate_password() {
|
||||
local length="${1:-16}"
|
||||
|
||||
if command_exists openssl; then
|
||||
openssl rand -base64 "$length" | tr -d "=+/" | cut -c1-"$length"
|
||||
elif command_exists pwgen; then
|
||||
pwgen -s "$length" 1
|
||||
else
|
||||
# Fallback to /dev/urandom
|
||||
tr -dc 'A-Za-z0-9!@#$%^&*' </dev/urandom | head -c "$length"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
# Returns: 0 if root, 1 if not
|
||||
is_root() {
|
||||
[ "$(id -u)" -eq 0 ]
|
||||
}
|
||||
|
||||
# Ensure script is running as root
|
||||
require_root() {
|
||||
if ! is_root; then
|
||||
log_error "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Get the Linux distribution name
|
||||
get_distro() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
echo "$ID"
|
||||
elif [ -f /etc/lsb-release ]; then
|
||||
. /etc/lsb-release
|
||||
echo "$DISTRIB_ID" | tr '[:upper:]' '[:lower:]'
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if systemd is available
|
||||
has_systemd() {
|
||||
command_exists systemctl && [ -d /run/systemd/system ]
|
||||
}
|
||||
|
||||
# URL encode a string
|
||||
# Args: $1 = string to encode
|
||||
url_encode() {
|
||||
local string="$1"
|
||||
local strlen=${#string}
|
||||
local encoded=""
|
||||
local pos c o
|
||||
|
||||
for (( pos=0; pos<strlen; pos++ )); do
|
||||
c=${string:$pos:1}
|
||||
case "$c" in
|
||||
[-_.~a-zA-Z0-9] )
|
||||
o="${c}"
|
||||
;;
|
||||
* )
|
||||
printf -v o '%%%02x' "'$c"
|
||||
;;
|
||||
esac
|
||||
encoded+="${o}"
|
||||
done
|
||||
|
||||
echo "${encoded}"
|
||||
}
|
||||
|
||||
# Parse JSON value from a JSON string
|
||||
# Args: $1 = JSON string, $2 = key path (e.g., ".data.token")
|
||||
# Returns: value on stdout
|
||||
json_get() {
|
||||
local json="$1"
|
||||
local key="$2"
|
||||
|
||||
echo "$json" | jq -r "$key // empty" 2>/dev/null
|
||||
}
|
||||
|
||||
# Check if a file is writable or can be created
|
||||
# Args: $1 = file path
|
||||
# Returns: 0 if writable, 1 if not
|
||||
is_writable() {
|
||||
local filepath="$1"
|
||||
|
||||
if [ -e "$filepath" ]; then
|
||||
[ -w "$filepath" ]
|
||||
else
|
||||
local dirpath=$(dirname "$filepath")
|
||||
[ -w "$dirpath" ]
|
||||
fi
|
||||
}
|
||||
|
||||
# Export all functions for use in other scripts
|
||||
export -f log_info log_success log_warning log_error log_debug
|
||||
export -f validate_json command_exists require_command
|
||||
export -f wait_for_port check_url retry_command
|
||||
export -f mask_secret start_group end_group set_output
|
||||
export -f generate_password is_root require_root
|
||||
export -f get_distro has_systemd url_encode json_get is_writable
|
||||
Reference in New Issue
Block a user