Skip to content

Commit

Permalink
Merge pull request #408 from strukturag/reminder-emails
Browse files Browse the repository at this point in the history
Add background job to send out daily reminders for missing email signatures.
  • Loading branch information
fancycode authored Jul 23, 2024
2 parents f7af444 + 7885b4c commit 853842d
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 188 deletions.
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ See [the website](https://www.certificate24.com) for further information.
<job>OCA\Certificate24\BackgroundJob\FetchSigned</job>
<job>OCA\Certificate24\BackgroundJob\ResendMails</job>
<job>OCA\Certificate24\BackgroundJob\RetryDownloads</job>
<job>OCA\Certificate24\BackgroundJob\SendReminders</job>
</background-jobs>

<settings>
Expand Down
121 changes: 121 additions & 0 deletions lib/BackgroundJob/SendReminders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2024, struktur AG.
*
* @author Joachim Bauch <bauch@struktur.de>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Certificate24\BackgroundJob;

use OCA\Certificate24\Config;
use OCA\Certificate24\Mails;
use OCA\Certificate24\Requests;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
use OCP\Files\IRootFolder;
use OCP\IUserManager;

/**
* Send reminder emails for signature requests that have not been processed
* for at least 24 hours.
*/
class SendReminders extends TimedJob {
public const REMINDER_MAX_AGE_HOURS = 24;

private IUserManager $userManager;
private IRootFolder $root;
private Config $config;
private Requests $requests;
private Mails $mails;

public function __construct(ITimeFactory $timeFactory,
IUserManager $userManager,
IRootFolder $root,
Config $config,
Requests $requests,
Mails $mails) {
parent::__construct($timeFactory);

// Every 6 hours.
$this->setInterval(6 * 24 * 60);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);

$this->userManager = $userManager;
$this->root = $root;
$this->config = $config;
$this->requests = $requests;
$this->mails = $mails;
}

protected function run($argument): void {
if (!$this->config->sendReminderMails()) {
return;
}

$pending = $this->requests->getReminderEmails(self::REMINDER_MAX_AGE_HOURS);
foreach ($pending['single'] as $entry) {
$user = $this->userManager->get($entry['user_id']);
if (!$user) {
// Should not happen, requests will get deleted if the owner is deleted.
continue;
}

$files = $this->root->getUserFolder($user->getUID())->getById($entry['file_id']);
if (empty($files)) {
// Should not happen, requests will get deleted if the associated file is deleted.
continue;
}

$file = $files[0];
$recipient = [
'type' => $entry['recipient_type'],
'value' => $entry['recipient'],
'display_name' => $entry['recipient_display_name'],
'c24_signature_id' => $entry['c24_signature_id'],
];
$this->mails->sendRequestMail($entry['id'], $user, $file, $recipient, $entry['c24_server']);
}

foreach ($pending['multi'] as $entry) {
$request = $entry['request'];
$user = $this->userManager->get($request['user_id']);
if (!$user) {
// Should not happen, requests will get deleted if the owner is deleted.
continue;
}

$files = $this->root->getUserFolder($user->getUID())->getById($request['file_id']);
if (empty($files)) {
// Should not happen, requests will get deleted if the associated file is deleted.
continue;
}

$file = $files[0];
$recipient = [
'type' => $entry['type'],
'value' => $entry['value'],
'display_name' => $entry['display_name'],
'c24_signature_id' => $entry['c24_signature_id'],
];
$this->mails->sendRequestMail($entry['request_id'], $user, $file, $recipient, $request['c24_server']);
}
}
}
4 changes: 4 additions & 0 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ public function isBackgroundVerifyEnabled(): bool {
return $this->config->getAppValue(self::APP_ID, 'background_verify', 'true') === 'true';
}

public function sendReminderMails(): bool {
return $this->config->getAppValue(self::APP_ID, 'send_reminder_mails', 'true') === 'true';
}

public function getSignatureImage(IUser $user): ?ISimpleFile {
try {
$folder = $this->appData->getFolder($user->getUID());
Expand Down
55 changes: 55 additions & 0 deletions lib/Requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,61 @@ public function getPendingEmails() {
return $pending;
}

public function getReminderEmails(int $maxAgeHours) {
$maxDate = new \DateTime();
$maxDate = $maxDate->sub(new \DateInterval('PT' . $maxAgeHours . 'H'));
$maxDate->setTimezone(new \DateTimeZone('UTC'));

$query = $this->db->getQueryBuilder();
$query->select('*')
->from('c24_requests')
->where($query->expr()->eq('recipient_type', $query->createNamedParameter('email')))
->andWhere($query->expr()->isNull('signed'))
->andWhere($query->expr()->lte('email_sent', $query->createNamedParameter($maxDate, 'datetimetz')));
$result = $query->executeQuery();

$pending = [];
$recipients = [];
while ($row = $result->fetch()) {
if ($row['metadata']) {
$row['metadata'] = json_decode($row['metadata'], true);
}

$row['recipients'] = $this->getRecipients($row);
$recipients[] = $row;
}
$result->closeCursor();
$pending['single'] = $recipients;

$query = $this->db->getQueryBuilder();
$query->select('*')
->from('c24_recipients')
->where($query->expr()->eq('type', $query->createNamedParameter('email')))
->andWhere($query->expr()->isNull('signed'))
->andWhere($query->expr()->lte('email_sent', $query->createNamedParameter($maxDate, 'datetimetz')));
$result = $query->executeQuery();
$recipients = [];
$requests = [];
while ($row = $result->fetch()) {
if (!isset($requests[$row['request_id']])) {
$requests[$row['request_id']] = $this->getRequestById($row['request_id']);
if (!$requests[$row['request_id']]) {
$this->logger->warning('Request ' . $row['request_id'] . ' no longer exists for pending email of ' . $row['type'] . ' ' . $row['value']);
continue;
}
}
$signed = $row['signed'];
if (is_string($signed)) {
$row['signed'] = $this->parseDateTime($signed);
}
$row['request'] = $requests[$row['request_id']];
$recipients[] = $row;
}
$result->closeCursor();
$pending['multi'] = $recipients;
return $pending;
}

public function getPendingDownloads() {
$query = $this->db->getQueryBuilder();
$query->select('*')
Expand Down
1 change: 1 addition & 0 deletions lib/Settings/Admin/AdminSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public function getForm(): TemplateResponse {
'signed_save_mode' => $this->config->getSignedSaveMode(),
'insecure_skip_verify' => $this->config->insecureSkipVerify(),
'background_verify' => $this->config->isBackgroundVerifyEnabled(),
'send_reminder_mails' => $this->config->sendReminderMails(),
'delete_max_age' => $this->config->getDeleteMaxAge(),
'last_verified' => $last ? $last->format(\DateTime::ATOM) : null,
'unverified_count' => $this->verify->getUnverifiedCount(),
Expand Down
71 changes: 55 additions & 16 deletions src/components/AdminSettings/InstanceSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,32 @@
@update:checked="debounceUpdateBackgroundVerify">
{{ t('certificate24', 'Verify document signatures in the background.') }}
</NcCheckboxRadioSwitch>
<div v-if="settings.last_verified">
{{ t('certificate24', 'Last verification: {timestamp}', {
timestamp: formatDate(settings.last_verified),
}) }}
<div class="radioswitch-details">
<div v-if="settings.last_verified">
{{ t('certificate24', 'Last verification: {timestamp}', {
timestamp: formatDate(settings.last_verified),
}) }}
</div>
<div v-else>
{{ t('certificate24', 'Last verification: none yet') }}
</div>
<div v-if="settings.unverified_count !== null">
{{ t('certificate24', 'Number of pending verifications: {count}', {
count: settings.unverified_count,
}) }}
</div>
<NcButton :disabled="clearing"
@click="clearVerification">
{{ t('certificate24', 'Clear verification cache') }}
</NcButton>
</div>
<div v-else>
{{ t('certificate24', 'Last verification: none yet') }}
</div>
<div v-if="settings.unverified_count !== null">
{{ t('certificate24', 'Number of pending verifications: {count}', {
count: settings.unverified_count,
}) }}
</div>
<NcButton :disabled="clearing"
@click="clearVerification">
{{ t('certificate24', 'Clear verification cache') }}
</NcButton>
</div>
<div>
<NcCheckboxRadioSwitch :checked.sync="settings.send_reminder_mails"
type="switch"
@update:checked="debounceUpdateSendReminderMails">
{{ t('certificate24', 'Send reminder mails to email recipients that have not signed their request.') }}
</NcCheckboxRadioSwitch>
</div>
</NcSettingsSection>
</template>
Expand Down Expand Up @@ -160,6 +169,36 @@ export default {
}.bind(this),
)
},

debounceUpdateSendReminderMails: debounce(function() {
this.updateSendReminderMails()
}, 500),

updateSendReminderMails() {
this.loading = true

const self = this
OCP.AppConfig.setValue('certificate24', 'send_reminder_mails', this.settings.send_reminder_mails, {
success() {
showSuccess(t('certificate24', 'Settings saved'))
self.loading = false
},
error() {
showError(t('certificate24', 'Could not save settings'))
self.loading = false
},
})
},

},
}
</script>

<style lang="scss" scoped>
.radioswitch-details {
--icon-width: 36px;
--icon-height: 16px;
margin-left: calc(4px + var(--icon-width));
padding-left: calc((var(--default-clickable-area) - var(--icon-height)) / 2);
}
</style>
5 changes: 5 additions & 0 deletions tests/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
<code>IRootFolder</code>
</MissingDependency>
</file>
<file src="lib/BackgroundJob/SendReminders.php">
<MissingDependency occurrences="4">
<code>IRootFolder</code>
</MissingDependency>
</file>
<file src="lib/Controller/ApiController.php">
<EmptyArrayAccess occurrences="1">
<code>$userCache[$userId]</code>
Expand Down
Loading

0 comments on commit 853842d

Please sign in to comment.