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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev/docker/opencloud/csp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ directives:
# In contrast to bash and docker the default is given after the | character
- 'https://${ONLYOFFICE_DOMAIN|host.docker.internal:9981}/'
- 'https://${COLLABORA_DOMAIN|host.docker.internal:9980}/'
- 'https://host.docker.internal:9443/'
img-src:
- '''self'''
- 'data:'
Expand Down
100 changes: 100 additions & 0 deletions dev/docker/roundcube/autologin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php
/**
* Roundcube autologin endpoint for OpenCloud webmail integration.
* see https://medium.com/@matthias2handy/from-seafile-to-opencloud-building-a-self-hosted-webmail-cloud-integration-on-kubernetes-a4f3bb795d6f
*
* Verifies HMAC-SHA256 signed tokens and authenticates users against IMAP.
* Tokens contain: user ID, account ID, IMAP credentials, expiration.
*
* Query parameters:
* data - base64url-encoded JSON payload
* sig - HMAC-SHA256 signature (base64url)
* ts - Unix timestamp (seconds)
*/

define('SHARED_SECRET', getenv('ROUNDCUBE_SHARED_SECRET') ?: 'dev-shared-secret');
define('MAX_TOKEN_AGE', 120); // seconds

// base64url decode (RFC 4648 §5)
function base64url_decode(string $data): string|false {
$padded = str_pad(strtr($data, '-_', '+/'), strlen($data) + (4 - strlen($data) % 4) % 4, '=');
return base64_decode($padded);
}

// --- Validate request parameters ---

$data = $_GET['data'] ?? null;
$sig = $_GET['sig'] ?? null;
$ts = $_GET['ts'] ?? null;

if (!$data || !$sig || !$ts) {
http_response_code(400);
die('Missing parameters');
}

$tsInt = intval($ts);
$now = time();

// Check timestamp freshness
if (abs($now - $tsInt) > MAX_TOKEN_AGE) {
http_response_code(403);
die('Token expired (timestamp)');
}

// Verify HMAC-SHA256 signature
$message = $data . '.' . $ts;
$expectedSig = base64url_decode($sig);
$calculatedSig = hash_hmac('sha256', $message, SHARED_SECRET, true);

if (!$expectedSig || !hash_equals($calculatedSig, $expectedSig)) {
http_response_code(403);
die('Invalid signature');
}

// Decode and validate payload
$payloadJson = base64url_decode($data);
if (!$payloadJson) {
http_response_code(400);
die('Invalid payload encoding');
}

$payload = json_decode($payloadJson, true);
if (!$payload) {
http_response_code(400);
die('Invalid payload JSON');
}

// Check payload expiration
if (!isset($payload['exp']) || $payload['exp'] < $now) {
http_response_code(403);
die('Token expired (payload)');
}

$imapUser = $payload['email'] ?? null;
$imapPass = $payload['imapPass'] ?? null;

if (!$imapUser || !$imapPass) {
http_response_code(400);
die('Missing credentials in payload');
}

// --- Bootstrap Roundcube and authenticate ---

define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/');
require_once INSTALL_PATH . 'program/include/iniset.php';

$rcmail = rcmail::get_instance(0, 'xhr');

// Authenticate against IMAP
$auth = $rcmail->login($imapUser, $imapPass, $rcmail->config->get('default_host'), false);

if (!$auth) {
http_response_code(401);
die('IMAP authentication failed');
}

// Set session cookie and redirect to inbox
$rcmail->session->set_auth_cookie();

header('Location: ./');
exit;
39 changes: 39 additions & 0 deletions dev/docker/roundcube/config.inc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
/**
* Roundcube custom configuration for OpenCloud dev stack.
* see https://medium.com/@matthias2handy/from-seafile-to-opencloud-building-a-self-hosted-webmail-cloud-integration-on-kubernetes-a4f3bb795d6f
*
* This file is mounted as zzz-custom.php so it loads after the default config
* generated by the Docker image's entrypoint.
*/

// IMAP
$config['imap_host'] = 'stalwart:143';

// SMTP (port 25, no auth needed for local delivery)
$config['smtp_host'] = 'stalwart:25';
$config['smtp_user'] = '';
$config['smtp_pass'] = '';

// Database (SQLite for dev simplicity)
$config['db_dsnw'] = 'sqlite:////var/roundcube/db/roundcube.db?mode=0646';

// Session settings for iframe embedding
$config['session_lifetime'] = 600; // 10 hours (in minutes)
$config['session_samesite'] = 'None'; // required for cross-origin iframe
$config['use_https'] = true; // SameSite=None requires Secure flag

// Disable IP check (container IPs change)
$config['ip_check'] = false;

// Product name
$config['product_name'] = 'OpenCloud Mail';

// Allow all origins for development
$config['x_frame_options'] = false;

// Logging
$config['log_driver'] = 'stdout';

// Plugins
$config['plugins'] = [];
4 changes: 4 additions & 0 deletions dev/docker/stalwart/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,7 @@ tracer.console.level = "trace"
tracer.console.lossy = false
tracer.console.multiline = false
tracer.console.type = "stdout"

# Local-only delivery: prevent all outbound email
queue.route.local.type = "local"
queue.strategy.route = [ { else = "'local'" } ]
32 changes: 32 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ services:
- '--entrypoints.stalwart-tls.address=:443'
- '--entrypoints.stalwart-tls.http.middlewares=https_config@docker'
- '--entrypoints.stalwart-tls.http.tls.options=default'
- '--entrypoints.roundcube.address=:9443'
- '--entrypoints.stalwart.address=:8180'
labels:
traefik.enable: true
Expand All @@ -276,6 +277,7 @@ services:
- '10200:10200'
- '9980:9980'
- '8880:8880'
- '9443:9443'
- '443:443'
volumes:
- './dev/docker/traefik/certificates:/certificates'
Expand All @@ -286,6 +288,35 @@ services:
aliases:
- ${STALWART_DOMAIN:-stalwart.opencloud.test}

roundcube:
image: roundcube/roundcubemail:latest
container_name: web_roundcube
restart: unless-stopped
depends_on:
- stalwart
extra_hosts:
- host.docker.internal:${DOCKER_HOST:-host-gateway}
environment:
ROUNDCUBEMAIL_DEFAULT_HOST: stalwart
ROUNDCUBEMAIL_DEFAULT_PORT: '143'
ROUNDCUBEMAIL_SMTP_SERVER: stalwart
ROUNDCUBEMAIL_SMTP_PORT: '25'
ROUNDCUBEMAIL_DB_TYPE: sqlite
ROUNDCUBEMAIL_SKIN: elastic
volumes:
- ./dev/docker/roundcube/config.inc.php:/var/roundcube/config/zzz-custom.php:ro
- ./dev/docker/roundcube/autologin.php:/var/www/html/public_html/autologin.php:ro
- roundcube-data:/var/roundcube/db
labels:
traefik.enable: true
traefik.docker.network: traefik
traefik.http.routers.roundcube.tls: true
traefik.http.routers.roundcube.rule: Host(`host.docker.internal`) && PathPrefix(`/`)
traefik.http.routers.roundcube.entrypoints: roundcube
traefik.http.services.roundcube.loadbalancer.server.port: 80
networks:
- traefik

tika-service:
image: dadarek/wait-for-dependencies:latest
container_name: web_tika_service
Expand Down Expand Up @@ -355,6 +386,7 @@ volumes:
opencloud-config:
opencloud-federated-config:
stalwart-data:
roundcube-data:

networks:
traefik: