Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
53c08c2
fixup! feat(installation): implement InstallationCompletedEvent liste…
printminion-co Jan 21, 2026
cc87769
fixup! feat(installation): implement InstallationCompletedEvent liste…
printminion-co Jan 21, 2026
dd7947f
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
3abb0bf
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
e374051
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
8140ffd
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
95762ba
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
b60934d
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
7e715c3
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
ac6a432
fixup! feat(installation): implement InstallationCompletedEvent liste…
printminion-co Jan 21, 2026
be0e3a6
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
196f8d8
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
9742e89
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
6ace281
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
9c65cc1
fixup! feat(installation): implement InstallationCompletedEvent liste…
printminion-co Jan 21, 2026
70a03f1
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
05d6424
fixup! feat(installation): implement post setup job and welcome email…
printminion-co Jan 21, 2026
5ace7f1
chore: add .editorconfig for consistent coding style
printminion-co Jan 21, 2026
736a3d4
style(psalm): format XML for consistency and readability
printminion-co Jan 21, 2026
f32cb89
chore: add psalm baseline file
printminion-co Jan 21, 2026
9d39718
refactor: add NewUserMailHelper stub and move psalm suppressions to c…
printminion-co Jan 22, 2026
f5b63ce
test: improve NewUserMailHelper usage verification in tests
printminion-co Jan 22, 2026
57b7346
refactor: simplify and clean up WelcomeMailHelper tests
printminion-co Jan 22, 2026
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
47 changes: 47 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: 2026 STRATO GmbH
# SPDX-License-Identifier: AGPL-3.0-or-later

# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

# PHP files
[*.php]
indent_style = tab
indent_size = 4

# JSON files
[*.json]
indent_style = space
indent_size = 2

# YAML files
[*.{yml,yaml}]
indent_style = space
indent_size = 2

# XML files
[*.xml]
indent_style = tab
indent_size = 4

# Markdown files
[*.md]
trim_trailing_whitespace = false

# Shell scripts
[*.sh]
indent_style = tab
indent_size = 4

# Makefile
[Makefile]
indent_style = tab
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ NCW Tools enhances Nextcloud with advanced system utilities including event list
php occ app:enable ncw_tools
```

## Configuration

### Post-Installation Job Retry Interval

The app includes a background job that sends a welcome email to the admin user after installation. You can configure how often this job retries by setting the following system configuration value in your `config/config.php`:

```php
'ncw_tools.post_setup_job.retry_interval' => 5, // Retry every 5 seconds (default: 2)
```

**Configuration Options:**
- **Key:** `ncw_tools.post_setup_job.retry_interval`
- **Type:** Integer (seconds)
- **Default:** `2` seconds
- **Description:** Interval in seconds between retry attempts for the post-installation welcome email job

## Development

### Prerequisites
Expand Down
4 changes: 0 additions & 4 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,9 @@
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Install\Events\InstallationCompletedEvent;

/**
* @psalm-suppress UnusedClass
*/
class Application extends App implements IBootstrap {
public const APP_ID = 'ncw_tools';

/** @psalm-suppress PossiblyUnusedMethod */
public function __construct() {
parent::__construct(self::APP_ID);
}
Expand Down
114 changes: 87 additions & 27 deletions lib/BackgroundJob/PostSetupJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
use Psr\Log\LoggerInterface;

class PostSetupJob extends TimedJob {
/**
* @psalm-suppress PossiblyUnusedMethod - Constructor called by DI container
*/
public const JOB_STATUS_INIT = 'INIT';
public const JOB_STATUS_DONE = 'DONE';
public const JOB_STATUS_UNKNOWN = 'UNKNOWN';
public const JOB_STATUS_CONFIG_KEY = 'post_install';

public function __construct(
private LoggerInterface $logger,
private IAppConfig $appConfig,
Expand All @@ -37,65 +39,123 @@ public function __construct(
private WelcomeMailHelper $welcomeMailHelper,
) {
parent::__construct($timeFactory);
// Interval every 2 seconds
$this->setInterval(2);
$retryInterval = $this->config->getSystemValueInt('ncw_tools.post_setup_job.retry_interval', 2);
$this->setInterval($retryInterval);
$this->setTimeSensitivity(IJob::TIME_SENSITIVE);
$this->logger->debug('PostSetupJob initialized', [
'retryInterval' => $retryInterval,
'timeSensitivity' => 'TIME_SENSITIVE',
]);
}

/**
* @param mixed $argument
* Execute the post-installation job
*
* Checks if the job has already completed and sends the initial welcome email
* to the admin user. The job will retry until successful or marked as done.
*
* @param mixed $argument The admin user ID as a string
*/
protected function run($argument): void {
// string post install variable
// used to check if job has already run
$jobStatus = $this->appConfig->getValueString(Application::APP_ID, 'post_install', 'UNKNOWN');
if ($jobStatus === 'DONE') {
$this->logger->debug('Job was already successful, remove job from jobList');
$jobStatus = $this->appConfig->getValueString(Application::APP_ID, self::JOB_STATUS_CONFIG_KEY, self::JOB_STATUS_UNKNOWN);
if ($jobStatus === self::JOB_STATUS_DONE) {
$this->logger->info('Post-installation job already completed, removing from queue');
$this->jobList->remove($this);
return;
}

if ($jobStatus === 'UNKNOWN') {
$this->logger->debug('Could not load job status from database, wait for another retry');
if ($jobStatus === self::JOB_STATUS_UNKNOWN) {
$this->logger->warning('Job status unknown, waiting for initialization');
return;
}

$this->logger->debug('Post install job started');
$initAdminId = (string)$argument;
$this->logger->info('Starting post-installation job', ['adminUserId' => $initAdminId]);
$this->sendInitialWelcomeMail($initAdminId);
$this->logger->debug('Post install job finished');
$this->logger->info('Post-installation job completed', ['adminUserId' => $initAdminId]);
}

/**
* Send initial welcome email to the admin user
*
* Validates system URL configuration, checks URL availability, verifies user exists,
* and sends the welcome email with a password reset token. On success, marks the job
* as done and removes it from the queue. Failures are logged and will trigger a retry.
*
* @param string $adminUserId The admin user ID to send the welcome email to
*/
protected function sendInitialWelcomeMail(string $adminUserId): void {

$client = $this->clientService->newClient();
$overwriteUrl = (string)$this->config->getSystemValue('overwrite.cli.url');

if (empty($overwriteUrl)) {
$this->logger->warning('System URL not configured, cannot send welcome email', [
'adminUserId' => $adminUserId,
'config_key' => 'overwrite.cli.url',
]);
return;
}

if (! $this->isUrlAvailable($client, $overwriteUrl)) {
$this->logger->debug('domain is not ready yet, retry with cron until ' . $overwriteUrl . ' is accessible');
$this->logger->info('System not ready, will retry sending welcome email', [
'adminUserId' => $adminUserId,
'url' => $overwriteUrl,
]);
return;
}
if (! $this->userManager->userExists($adminUserId)) {
$this->logger->warning('Could not find install user, skip sending welcome mail');
} else {
$initAdminUser = $this->userManager->get($adminUserId);
if ($initAdminUser !== null) {
$this->welcomeMailHelper->sendWelcomeMail($initAdminUser, true);
}
$this->logger->warning('Admin user not found, cannot send welcome email', [
'adminUserId' => $adminUserId,
]);
return;
}
$this->appConfig->setValueString(Application::APP_ID, 'post_install', 'DONE');

$initAdminUser = $this->userManager->get($adminUserId);
if ($initAdminUser === null) {
$this->logger->error('Failed to retrieve admin user, will retry', [
'adminUserId' => $adminUserId,
]);
return;
}

try {
$this->welcomeMailHelper->sendWelcomeMail($initAdminUser, true);
} catch (\Throwable $e) {
$this->logger->error('Failed to send welcome email, will retry', [
'adminUserId' => $adminUserId,
'exception' => $e->getMessage(),
]);
return;
}

$this->appConfig->setValueString(Application::APP_ID, self::JOB_STATUS_CONFIG_KEY, self::JOB_STATUS_DONE);
$this->jobList->remove($this);
}

/**
* Check if the system URL is accessible
*
* Performs an HTTP GET request to the status.php endpoint to verify the system
* is ready. Returns true if the response status code is in the 2xx range.
*
* @param IClient $client The HTTP client to use for the request
* @param string $baseUrl The base URL to check (e.g., https://example.com)
* @return bool True if the URL is accessible, false otherwise
*/
private function isUrlAvailable(IClient $client, string $baseUrl): bool {

$url = $baseUrl . '/status.php';
try {
$this->logger->debug('Check URL: ' . $url);
$this->logger->debug('Checking URL availability', ['url' => $url]);
$response = $client->get($url);
return $response->getStatusCode() >= 200 && $response->getStatusCode() < 300;

$statusCode = $response->getStatusCode();
return $statusCode >= 200 && $statusCode < 300;
} catch (\Exception $ex) {
$this->logger->debug('Exception for ' . $url . '. Reason: ' . $ex->getMessage());
$this->logger->info('URL not yet accessible', [
'url' => $url,
'exception' => $ex->getMessage(),
]);
return false;
}
}
Expand Down
19 changes: 11 additions & 8 deletions lib/Helper/WelcomeMailHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

namespace OCA\NcwTools\Helper;

use OC\AppFramework\Utility\TimeFactory;
use OCA\Settings\Mailer\NewUserMailHelper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IURLGenerator;
Expand All @@ -22,9 +22,6 @@
use OCP\Util;

class WelcomeMailHelper {
/**
* @psalm-suppress PossiblyUnusedMethod - Constructor called by DI container
*/
public function __construct(
private Defaults $defaults,
private ICrypto $crypto,
Expand All @@ -33,13 +30,19 @@ public function __construct(
private IFactory $l10NFactory,
private ISecureRandom $secureRandom,
private IConfig $config,
private ITimeFactory $timeFactory,
) {
}

/**
* @psalm-suppress UndefinedClass - Using internal Nextcloud classes
* @psalm-suppress MixedAssignment
* @psalm-suppress MixedMethodCall
* Send a welcome email to a user with optional password reset token
*
* Creates a NewUserMailHelper instance and uses it to generate and send
* the welcome email template to the specified user.
*
* @param IUser $user The user to send the welcome email to
* @param bool $generatePasswordResetToken Whether to include a password reset token in the email
* @throws \Exception If email generation or sending fails
*/
public function sendWelcomeMail(IUser $user, bool $generatePasswordResetToken): void {
$newUserMailHelper = new NewUserMailHelper(
Expand All @@ -48,7 +51,7 @@ public function sendWelcomeMail(IUser $user, bool $generatePasswordResetToken):
$this->l10NFactory,
$this->mailer,
$this->secureRandom,
new TimeFactory(),
$this->timeFactory,
$this->config,
$this->crypto,
Util::getDefaultEmailAddress('no-reply')
Expand Down
57 changes: 21 additions & 36 deletions lib/Listeners/InstallationCompletedEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,59 +15,44 @@
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IAppConfig;
use OCP\Install\Events\InstallationCompletedEvent;
use Psr\Log\LoggerInterface;

/**
* @template-implements IEventListener<Event>
* @template-implements IEventListener<InstallationCompletedEvent>
*/
class InstallationCompletedEventListener implements IEventListener {
private string $adminConfigPath = '/vault/secrets/adminconfig';

private array $quotesArray = ['\\\'', '"', '\''];

/**
* @psalm-suppress PossiblyUnusedMethod - Constructor called by DI container
*/
public function __construct(
private IAppConfig $appConfig,
private LoggerInterface $logger,
private IJobList $jobList,
) {
}

/**
* Handle the InstallationCompletedEvent
*
* When installation completes, this listener schedules a PostSetupJob
* to send the initial welcome email to the admin user.
*
* @param Event $event The event to handle (must be InstallationCompletedEvent)
*/
public function handle(Event $event): void {
if (!$event instanceof InstallationCompletedEvent) {
return;
}

$this->appConfig->setValueString(Application::APP_ID, 'post_install', 'INIT');

$this->logger->debug('post Setup: init admin user');
$adminUserId = $this->initAdminUser();
$this->logger->debug('post Setup: admin user configured');

$this->logger->debug('post Setup: add send initial welcome mail job');
$this->jobList->add(PostSetupJob::class, $adminUserId);
$this->logger->debug('post Setup: job added');
}


protected function initAdminUser(): string {

// Read the configuration file line by line
$adminConfigLines = file($this->adminConfigPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// Initialize the associative array for the configuration
$adminConfig = [];
$this->appConfig->setValueString(Application::APP_ID, PostSetupJob::JOB_STATUS_CONFIG_KEY, PostSetupJob::JOB_STATUS_INIT);

// Iterate through the lines and extract the variables
foreach ($adminConfigLines as $line) {
$parts = explode('=', $line, 2);
if (count($parts) === 2) {
[$key, $value] = $parts;
$adminConfig[trim($key)] = trim($value);
}
$adminUserId = $event->getAdminUsername();
if ($adminUserId === null) {
$this->logger->warning('No admin user provided in InstallationCompletedEvent');
return;
}

$adminUser = $adminConfig['NEXTCLOUD_ADMIN_USER'] ?? '';
/** @psalm-suppress MixedArgumentTypeCoercion */
return str_replace($this->quotesArray, '', $adminUser);
$this->logger->info('Scheduling welcome email job', ['adminUserId' => $adminUserId]);
$this->jobList->add(PostSetupJob::class, $adminUserId);
$this->logger->debug('Welcome email job scheduled successfully', ['adminUserId' => $adminUserId]);
}
}
2 changes: 2 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0"/>
Loading
Loading