Skip to content
Merged
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
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
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
"OCA\\NcwTools\\": "lib/"
}
},
"autoload-dev": {
"classmap": [
"tests/stubs/"
]
},
"scripts": {
"post-install-cmd": [
"@composer bin all install --ansi"
Expand Down
7 changes: 3 additions & 4 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,22 @@

namespace OCA\NcwTools\AppInfo;

use OCA\NcwTools\Listeners\InstallationCompletedEventListener;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
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);
}

public function register(IRegistrationContext $context): void {
$context->registerEventListener(InstallationCompletedEvent::class, InstallationCompletedEventListener::class);
}

public function boot(IBootContext $context): void {
Expand Down
162 changes: 162 additions & 0 deletions lib/BackgroundJob/PostSetupJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 STRATO GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\NcwTools\BackgroundJob;

use OCA\NcwTools\AppInfo\Application;
use OCA\NcwTools\Helper\WelcomeMailHelper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\TimedJob;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;

class PostSetupJob extends TimedJob {
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,
private IConfig $config,
private IUserManager $userManager,
private IClientService $clientService,
ITimeFactory $timeFactory,
private IJobList $jobList,
private WelcomeMailHelper $welcomeMailHelper,
) {
parent::__construct($timeFactory);
$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',
]);
}

/**
* 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, 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 === self::JOB_STATUS_UNKNOWN) {
$this->logger->warning('Job status unknown, waiting for initialization');
return;
}

$initAdminId = (string)$argument;
$this->logger->info('Starting post-installation job', ['adminUserId' => $initAdminId]);
$this->sendInitialWelcomeMail($initAdminId);
$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->info('System not ready, will retry sending welcome email', [
'adminUserId' => $adminUserId,
'url' => $overwriteUrl,
]);
return;
}
if (! $this->userManager->userExists($adminUserId)) {
$this->logger->warning('Admin user not found, cannot send welcome email', [
'adminUserId' => $adminUserId,
]);
return;
}

$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('Checking URL availability', ['url' => $url]);
$response = $client->get($url);
$statusCode = $response->getStatusCode();
return $statusCode >= 200 && $statusCode < 300;
} catch (\Exception $ex) {
$this->logger->info('URL not yet accessible', [
'url' => $url,
'exception' => $ex->getMessage(),
]);
return false;
}
}
}
67 changes: 67 additions & 0 deletions lib/Helper/WelcomeMailHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 STRATO GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\NcwTools\Helper;

use OCA\Settings\Mailer\NewUserMailHelper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory;
use OCP\Mail\IMailer;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use OCP\Util;

class WelcomeMailHelper {
public function __construct(
private Defaults $defaults,
private ICrypto $crypto,
private IMailer $mailer,
private IURLGenerator $urlGenerator,
private IFactory $l10NFactory,
private ISecureRandom $secureRandom,
private IConfig $config,
private ITimeFactory $timeFactory,
) {
}

/**
* 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(
$this->defaults,
$this->urlGenerator,
$this->l10NFactory,
$this->mailer,
$this->secureRandom,
$this->timeFactory,
$this->config,
$this->crypto,
Util::getDefaultEmailAddress('no-reply')
);

$mailTmpl = $newUserMailHelper->generateTemplate($user, $generatePasswordResetToken);
if ($mailTmpl === null) {
// User has no email address, cannot send welcome mail
return;
}
$newUserMailHelper->sendMail($user, $mailTmpl);
}
}
58 changes: 58 additions & 0 deletions lib/Listeners/InstallationCompletedEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 STRATO GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\NcwTools\Listeners;

use OCA\NcwTools\AppInfo\Application;
use OCA\NcwTools\BackgroundJob\PostSetupJob;
use OCP\BackgroundJob\IJobList;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IAppConfig;
use OCP\Install\Events\InstallationCompletedEvent;
use Psr\Log\LoggerInterface;

/**
* @template-implements IEventListener<InstallationCompletedEvent>
*/
class InstallationCompletedEventListener implements IEventListener {

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, PostSetupJob::JOB_STATUS_CONFIG_KEY, PostSetupJob::JOB_STATUS_INIT);

$adminUserId = $event->getAdminUsername();
if ($adminUserId === null) {
$this->logger->warning('No admin user provided in InstallationCompletedEvent');
return;
}

$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]);
}
}
Loading
Loading