Compare commits

..

2 Commits

Author SHA1 Message Date
3e5bf97f4c 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
Reviewed-on: #1
2026-02-15 14:24:12 +00:00
cc3fffbda1 feat: initial version
All checks were successful
Test Stalwart Installation Action / Error Handling Tests (pull_request) Successful in 23s
Test Stalwart Installation Action / Full Configuration (Domains + Users) (pull_request) Successful in 40s
Test Stalwart Installation Action / Basic Installation (No Config) (pull_request) Successful in 47s
Test Stalwart Installation Action / Test Summary (pull_request) Successful in 3s
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-02-15 09:21:37 -05:00
4 changed files with 385 additions and 257 deletions

View File

@@ -26,6 +26,11 @@ jobs:
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
@@ -73,61 +78,6 @@ jobs:
echo "::error::Web admin not accessible after 60 seconds"
exit 1
# Test installation with admin password
test-with-password:
name: Installation with Admin Password
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 admin password
uses: ./
with:
admin_password: 'TestPassword123!@#'
- name: Verify service is running
run: |
sleep 10
# Check if systemd is available
if command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1; then
sudo systemctl status stalwart --no-pager || true
fi
# Check process
if ! pgrep -x stalwart >/dev/null; then
echo "::error::Stalwart process is not running"
ps aux | grep stalwart || true
exit 1
fi
echo "✓ Stalwart is running"
- name: Test authentication with new password
run: |
# Wait for API
sleep 5
# Try to authenticate with new password
RESPONSE=$(curl -sf -X POST http://localhost:8080/api/authenticate \
-H "Content-Type: application/json" \
-d '{"login":"admin","password":"TestPassword123!@#"}')
TOKEN=$(echo "$RESPONSE" | jq -r '.token // empty')
if [ -z "$TOKEN" ]; then
echo "::error::Failed to authenticate with new password"
exit 1
fi
echo "✓ Successfully authenticated with new password"
# Test full configuration with domains and users
test-full-config:
name: Full Configuration (Domains + Users)
@@ -143,9 +93,9 @@ jobs:
sudo apt-get install -y jq curl
- name: Install with full configuration
id: install
uses: ./
with:
admin_password: 'AdminPass123!@#'
domains: |
[
{
@@ -179,69 +129,186 @@ jobs:
}
]
- name: Wait for service to fully start
run: |
echo "Waiting for Stalwart to fully initialize..."
sleep 15
- name: Verify service is running
run: |
sleep 10
# Check if systemd is available
if command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1; then
sudo systemctl status stalwart --no-pager || true
fi
# 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 admin password was changed
- name: Verify service is accessible
run: |
sleep 5
RESPONSE=$(curl -sf -X POST http://localhost:8080/api/authenticate \
-H "Content-Type: application/json" \
-d '{"login":"admin","password":"AdminPass123!@#"}')
TOKEN=$(echo "$RESPONSE" | jq -r '.token // empty')
if [ -z "$TOKEN" ]; then
echo "::error::Failed to authenticate with admin password"
# 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 "✓ Admin authentication successful"
echo "TOKEN=$TOKEN" >> $GITHUB_ENV
- name: Verify domains were created
run: |
# List domains via API
DOMAINS=$(curl -sf -X GET http://localhost:8080/api/domain \
-H "Authorization: Bearer $TOKEN")
echo ""
echo "=== Verifying Domains ==="
DOMAINS_RESPONSE=$(curl -s -u "admin:$ADMIN_PASSWORD" \
"http://localhost:8080/api/principal?types=domain&limit=100")
echo "Domains response: $DOMAINS"
DOMAIN_COUNT=$(echo "$DOMAINS_RESPONSE" | jq '.data.total // 0')
echo "Total domains found: $DOMAIN_COUNT"
# Check if our test domains exist
if echo "$DOMAINS" | jq -e '.[] | select(.name == "test1.local")' >/dev/null; then
echo "✓ Domain test1.local found"
# 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 "::warning::Domain test1.local not found (API might use different structure)"
echo "::error::Domain test1.local not found"
exit 1
fi
- name: Verify users were created
run: |
# List accounts via API
ACCOUNTS=$(curl -sf -X GET http://localhost:8080/api/account \
-H "Authorization: Bearer $TOKEN")
echo "Accounts response: $ACCOUNTS"
# Check if users exist
if echo "$ACCOUNTS" | jq -e '.[] | select(.name == "user1@test1.local")' >/dev/null; then
echo "✓ User user1@test1.local found"
if echo "$DOMAINS_RESPONSE" | jq -e '.data.items[] | select(.name == "test2.local")' >/dev/null; then
echo "✓ Domain test2.local exists"
else
echo "::warning::User user1@test1.local not found (API might use different structure)"
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
@@ -283,7 +350,6 @@ jobs:
continue-on-error: true
uses: ./
with:
admin_password: 'TestPass123'
domains: 'invalid json string'
- name: Verify invalid JSON was caught
@@ -309,53 +375,11 @@ jobs:
# Remove installation
sudo rm -rf /opt/stalwart || true
# Test on different Ubuntu versions
test-ubuntu-versions:
name: Test on Ubuntu ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04, ubuntu-22.04, ubuntu-24.04]
fail-fast: false
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 Stalwart
uses: ./
with:
admin_password: 'TestPassword123'
- name: Verify installation
run: |
sleep 10
# Check if process is running
if pgrep -x stalwart >/dev/null; then
echo "✓ Stalwart running on ${{ matrix.os }}"
else
echo "::error::Stalwart not running on ${{ matrix.os }}"
ps aux | grep stalwart || true
exit 1
fi
- name: Test API accessibility
run: |
sleep 5
curl -sf http://localhost:8080/login >/dev/null
echo "✓ API accessible on ${{ matrix.os }}"
# Summary job
test-summary:
name: Test Summary
runs-on: ubuntu-latest
needs: [test-basic-install, test-with-password, test-full-config, test-ubuntu-versions]
needs: [test-basic-install, test-full-config]
if: always()
steps:
@@ -364,13 +388,10 @@ jobs:
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Basic Install: ${{ needs.test-basic-install.result }}" >> $GITHUB_STEP_SUMMARY
echo "- With Password: ${{ needs.test-with-password.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Full Config: ${{ needs.test-full-config.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Ubuntu Versions: ${{ needs.test-ubuntu-versions.result }}" >> $GITHUB_STEP_SUMMARY
# Fail if any required test failed
if [ "${{ needs.test-basic-install.result }}" != "success" ] || \
[ "${{ needs.test-with-password.result }}" != "success" ] || \
[ "${{ needs.test-full-config.result }}" != "success" ]; then
echo "::error::One or more tests failed"
exit 1

View File

@@ -7,11 +7,6 @@ branding:
color: 'blue'
inputs:
admin_password:
description: 'Admin password for Stalwart web interface (use GitHub Secrets). If not provided, defaults to "changeme"'
required: false
default: ''
domains:
description: 'JSON array of domains to create. Example: [{"name":"example.com","description":"Primary domain"}]'
required: false
@@ -59,17 +54,12 @@ runs:
- name: Configure Stalwart
shell: bash
if: ${{ inputs.admin_password != '' || inputs.domains != '' || inputs.users != '' }}
if: ${{ inputs.domains != '' || inputs.users != '' }}
env:
STALWART_ADMIN_PASSWORD: ${{ inputs.admin_password }}
STALWART_DOMAINS: ${{ inputs.domains }}
STALWART_USERS: ${{ inputs.users }}
STALWART_INSTALL_PATH: '/opt/stalwart'
run: |
# Mask sensitive data
if [ -n "$STALWART_ADMIN_PASSWORD" ]; then
echo "::add-mask::$STALWART_ADMIN_PASSWORD"
fi
# Mask user passwords from JSON
if [ -n "$STALWART_USERS" ]; then
@@ -90,9 +80,4 @@ runs:
run: |
HOSTNAME=$(hostname -f 2>/dev/null || echo "localhost")
echo "::notice::🎉 Stalwart Mail Server installation complete!"
echo "::notice::Web admin: http://$HOSTNAME:8080/login"
if [ -n "${{ inputs.admin_password }}" ]; then
echo "::notice::Admin credentials configured via inputs"
else
echo "::notice::Default admin password: changeme (change this immediately!)"
fi
echo "::notice::Web admin: http://$HOSTNAME:8080/login"

View File

@@ -13,12 +13,22 @@ source "${SCRIPT_DIR}/utils.sh"
# Configuration
readonly STALWART_PATH="${STALWART_INSTALL_PATH:-/opt/stalwart}"
readonly API_URL="http://localhost:8080/api"
readonly DEFAULT_ADMIN_PASSWORD="changeme"
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)
ADMIN_PASSWORD="${STALWART_ADMIN_PASSWORD:-}"
DOMAINS_JSON="${STALWART_DOMAINS:-}"
USERS_JSON="${STALWART_USERS:-}"
@@ -32,50 +42,29 @@ main() {
return 1
fi
# Authenticate with default password first
log_info "Authenticating with Stalwart API..."
local auth_token
if ! auth_token=$(authenticate "$DEFAULT_ADMIN_PASSWORD"); then
log_error "Failed to authenticate with default admin password"
log_info "This might mean Stalwart has already been configured"
# Try with provided password if different
if [ -n "$ADMIN_PASSWORD" ] && [ "$ADMIN_PASSWORD" != "$DEFAULT_ADMIN_PASSWORD" ]; then
log_info "Trying with provided admin password..."
if ! auth_token=$(authenticate "$ADMIN_PASSWORD"); then
log_error "Failed to authenticate with provided password"
return 1
fi
else
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 "Authentication successful"
log_success "API authentication verified"
log_info "📝 Generated admin password: ${current_password}"
log_warning "⚠️ Save this password securely - it won't be shown again!"
# Update admin password if provided and different from default
if [ -n "$ADMIN_PASSWORD" ] && [ "$ADMIN_PASSWORD" != "$DEFAULT_ADMIN_PASSWORD" ]; then
log_info "Updating admin password..."
if update_admin_password "$auth_token" "$ADMIN_PASSWORD"; then
log_success "Admin password updated successfully"
# Re-authenticate with new password
if ! auth_token=$(authenticate "$ADMIN_PASSWORD"); then
log_error "Failed to re-authenticate with new password"
return 1
fi
else
log_warning "Failed to update admin password, continuing with default"
fi
else
log_info "No admin password provided, keeping default (changeme)"
log_warning "⚠️ Remember to change the default password!"
fi
# 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 "$auth_token" "$DOMAINS_JSON"
create_domains "$current_password" "$DOMAINS_JSON"
else
log_error "Invalid domains JSON format"
return 1
@@ -88,7 +77,7 @@ main() {
if [ -n "$USERS_JSON" ]; then
log_info "Creating users..."
if validate_json "$USERS_JSON"; then
create_users "$auth_token" "$USERS_JSON"
create_users "$current_password" "$USERS_JSON"
else
log_error "Invalid users JSON format"
return 1
@@ -122,52 +111,36 @@ wait_for_stalwart_api() {
return 1
}
# Authenticate with Stalwart API
# Test authentication with Stalwart API using Basic Auth
# Args: $1 = password
# Returns: JWT token on stdout
authenticate() {
# Returns: 0 if auth works, 1 otherwise
test_auth() {
local password="$1"
local http_code
local response
local token
response=$(curl -sf -X POST "${API_URL}/authenticate" \
-H "Content-Type: application/json" \
-d "{\"login\":\"admin\",\"password\":\"${password}\"}" 2>&1) || {
return 1
}
# 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")
token=$(echo "$response" | jq -r '.token // empty' 2>/dev/null)
http_code=$(echo "$response" | tail -n 1)
if [ -z "$token" ]; then
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
echo "$token"
return 0
}
# Update admin password
# Args: $1 = auth token, $2 = new password
update_admin_password() {
local token="$1"
local new_password="$2"
local response
response=$(curl -sf -X PUT "${API_URL}/account/admin" \
-H "Authorization: Bearer ${token}" \
-H "Content-Type: application/json" \
-d "{\"password\":\"${new_password}\"}" 2>&1) || {
return 1
}
return 0
}
# Create domains from JSON array
# Args: $1 = auth token, $2 = domains JSON array
# Args: $1 = password, $2 = domains JSON array
create_domains() {
local token="$1"
local password="$1"
local domains_json="$2"
local domain_count
@@ -205,11 +178,30 @@ create_domains() {
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}/domain" \
-H "Authorization: Bearer ${token}" \
if curl -sf -X POST "${API_URL}/principal" \
-u "admin:${password}" \
-H "Content-Type: application/json" \
-d "$domain" >/dev/null 2>&1; then
-d "$payload" >/dev/null 2>&1; then
log_success "✓ Created domain: $domain_name"
created=$((created + 1))
else
@@ -225,9 +217,9 @@ create_domains() {
}
# Create users from JSON array
# Args: $1 = auth token, $2 = users JSON array
# Args: $1 = password, $2 = users JSON array
create_users() {
local token="$1"
local password="$1"
local users_json="$2"
local user_count
@@ -265,18 +257,62 @@ create_users() {
continue
fi
# Build API payload (name, password, description, quota)
# 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
payload=$(echo "$user" | jq '{
name: .email,
password: .password,
description: (.name // .email),
quota: (.quota // 1073741824)
}' 2>/dev/null)
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}/account" \
-H "Authorization: Bearer ${token}" \
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"

View File

@@ -114,9 +114,25 @@ main() {
fi
fi
# Run init
# Run init and capture the generated password
say "⚙️ Initializing Stalwart configuration..."
ignore $_dir/bin/stalwart --init "$_dir"
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..."
@@ -127,16 +143,34 @@ main() {
# Create service file
say "🚀 Starting service..."
if [ "${_os}" = "linux" ]; then
local _issystemdlinux=$(command -v systemctl)
if [ -n "$_issystemdlinux" ]; 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"
else
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."
@@ -171,9 +205,61 @@ AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable stalwart.service
systemctl restart stalwart.service
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() {