Add database vacuuming, in/outbox purging, fixes
This commit is contained in:
@@ -6,7 +6,10 @@
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Casey\\Daemon\\": "src/"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
|
||||
@@ -6,7 +6,10 @@ caseConfirmUrl: 'https://casey.domain.tld/confirm/{code}'
|
||||
teamInviteReminder: 1d
|
||||
teamInviteCount: 3
|
||||
caseConfirmation: true
|
||||
# Set to false to disable the mail functionality entirely
|
||||
mailEnabled: true
|
||||
# Delete entries from the mail spool after period
|
||||
mailSpoolExpiry: 7d
|
||||
mailSubjectFormat: '[%reference%] %prefix%: %subject%'
|
||||
mailSubjectPrefix: 'Support case'
|
||||
mailSyncTimer: 10
|
||||
|
||||
@@ -11,7 +11,8 @@ html {
|
||||
|
||||
body {
|
||||
background: var(--bg-main);
|
||||
font-family: monospace;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 10pt;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
@@ -40,6 +41,26 @@ table.table {
|
||||
}
|
||||
}
|
||||
|
||||
table.props {
|
||||
border-collapse: collapse;
|
||||
border: solid 1px var(--border-color);
|
||||
width: 100%;
|
||||
& tr {
|
||||
border-top: solid 1px var(--border-color);
|
||||
border-bottom: solid 1px var(--border-color);
|
||||
}
|
||||
& th {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding-right: 1.5em;
|
||||
padding: 0.25rem;
|
||||
background: #ddd;
|
||||
}
|
||||
& td {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.w-0 {
|
||||
width: 0px;
|
||||
white-space: nowrap;
|
||||
@@ -57,13 +78,13 @@ table.table {
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
margin: 0.25rem 0rem;
|
||||
margin: 0.40rem 0rem 0.20rem 0rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: relative;
|
||||
top: -0.05rem;
|
||||
padding: 0.2rem 0.1rem 0.1rem 0.1rem;
|
||||
top: -0.15rem;
|
||||
padding: 0.05rem 0.1rem 0.10rem 0.1rem;
|
||||
font-size: 70%;
|
||||
font-weight: bold;
|
||||
border-radius: 0.25rem;
|
||||
@@ -76,6 +97,10 @@ h1,h2,h3,h4,h5,h6 {
|
||||
background: #c9a61a;
|
||||
color: white;
|
||||
}
|
||||
&.-blue {
|
||||
background: #2462c8;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
@@ -119,4 +144,9 @@ button {
|
||||
.d-flex-row { display: flex; flex-direction: row; }
|
||||
.d-flex-column { display: flex; flex-direction: column; }
|
||||
.d-block { display: block; }
|
||||
.flex-fill { flex-grow: 1; }
|
||||
.flex-fill { flex-grow: 1; }
|
||||
|
||||
.small { font-size:90%; }
|
||||
.muted { color: #666; }
|
||||
|
||||
.fw-bold { font-weight: bold; }
|
||||
@@ -3,32 +3,51 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>Case {{ case.reference }}: {{ case.subject }}</h3>
|
||||
<div class="small muted fw-bold">{%
|
||||
if (case.is_closed) %}Closed case{%
|
||||
elseif (case.is_accepted) %}Open case{%
|
||||
elseif (case.is_confirmed) %}Pending case{%
|
||||
else %}Unconfirmed case{%
|
||||
endif %}: {{ case.reference }}</div>
|
||||
{# <div>{{ case|json_encode }}</div> #}
|
||||
<h1 style="margin-top:0.1rem;">"{{ case.subject }}"</h1>
|
||||
|
||||
<div style="display:flex;">
|
||||
<div style="flex-grow:1;">
|
||||
<h4>Details</h4>
|
||||
<table class="props">
|
||||
{% for prop in [ 'uuid', 'reference', 'created', 'updated', 'subject', 'category', 'tags', 'is_closed', 'resolution' ] %}
|
||||
<div>{{prop}}: <span style="font-weight:bold">{{case[prop]|json_encode}}</span></div>
|
||||
<tr>
|
||||
<th>{{prop}}</th>
|
||||
<td>{{case[prop]|json_encode}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-left:1rem; min-width:30%;">
|
||||
<h4>Participants</h4>
|
||||
<div style="border: solid 1px var(--border-color); border-top:none;">
|
||||
{% for participant in case.participants %}
|
||||
<div>
|
||||
<div style="border-top: solid 1px var(--border-color); padding:0.25rem;">
|
||||
<div>
|
||||
<span style="font-weight:bold;">{{participant.name}}</span>
|
||||
{% if participant.team %}<span class="badge -green">TEAM</span>{% endif %}
|
||||
{% if participant.uuid == case.creator.uuid %}<span class="badge -yellow">CREATOR</span>{% endif %}
|
||||
{% if participant.uuid == case.creator.uuid %}<span class="badge -blue">CREATOR</span>{% endif %}
|
||||
</div>
|
||||
<div style="font-size:80%;">{{participant.email}}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top:1rem;">Metadata</h4>
|
||||
<table class="props">
|
||||
{% for key,value in case.metadata %}
|
||||
<div>{{ key }}: <span style="font-weight:bold;">{{value|json_encode}}</span></div>
|
||||
<tr>
|
||||
<th style="width:30%;">{{ key }}</th>
|
||||
<td>{{value|json_encode}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class Configuration implements EventEmitterInterface
|
||||
const CONFIG_TEAM_INVITE_REMINDER = 'teamInviteReminder';
|
||||
const CONFIG_TEAM_INVITE_COUNT = 'teamInviteCount';
|
||||
const CONFIG_MAIL_ENABLE = 'mailEnable';
|
||||
const CONFIG_MAIL_SPOOL_EXPIRY = 'mailSpoolExpiry';
|
||||
const CONFIG_MAIL_SUBJECT_FORMAT = 'mailSubjectFormat';
|
||||
const CONFIG_MAIL_SUBJECT_PREFIX = 'mailSubjectPrefix';
|
||||
const CONFIG_MAIL_SYNC_TIMER = 'mailSyncTimer';
|
||||
@@ -52,6 +53,7 @@ class Configuration implements EventEmitterInterface
|
||||
self::CONFIG_TEAM_INVITE_COUNT => 3,
|
||||
self::CONFIG_CASE_CONFIRMATION => false,
|
||||
self::CONFIG_MAIL_ENABLE => true,
|
||||
self::CONFIG_MAIL_SPOOL_EXPIRY => "7d",
|
||||
self::CONFIG_MAIL_SUBJECT_FORMAT => "[%reference%] %prefix%: %subject%",
|
||||
self::CONFIG_MAIL_SUBJECT_PREFIX => "Case",
|
||||
self::CONFIG_MAIL_SYNC_TIMER => 60,
|
||||
|
||||
@@ -409,7 +409,7 @@ class CaseController
|
||||
* @param int $participant
|
||||
* @return PromiseInterface
|
||||
*/
|
||||
#[Route(path:"/case/{uuid}/participant/{participant}", methods:["DELETE"])]
|
||||
#[Route(path:"/case/{uuid}/participant/{participant}", methods:["DELETE"], requires:["@ADMIN"])]
|
||||
public function deleteCaseParticipant(ServerRequestInterface $request, Deferred $deferred, string $uuid, int $participant): PromiseInterface
|
||||
{
|
||||
// TODO delete participant
|
||||
@@ -451,13 +451,20 @@ class CaseController
|
||||
$result = $schemaService->validateData("message", $message, [ "from", "message" ]);
|
||||
|
||||
$this->logger->debug("Checking message source: {$message->from}");
|
||||
if (preg_match('|^(.+?) <(.+?)>$|', $message->from, $m)) {
|
||||
$fromName = $m[1];
|
||||
$fromMail = $m[2];
|
||||
} else {
|
||||
$fromName = $message->from;
|
||||
$fromMail = $message->from;
|
||||
}
|
||||
try {
|
||||
$personId = $this->database->findPersonIdFromQuery($message->from);
|
||||
$personId = $this->database->findPersonIdFromQuery($fromMail);
|
||||
$person = $this->database->getPerson($personId);
|
||||
$this->logger->debug(" {$message->from} -> {$personId} (existing)");
|
||||
} catch (\Exception $e) {
|
||||
// TODO ensure only e-mail addresses
|
||||
$person = $this->database->createPerson($message->from, $message->from, false);
|
||||
$person = $this->database->createPerson($fromName, $fromMail, false);
|
||||
$personId = $person['id'];
|
||||
$this->logger->debug(" {$message->from} -> {$personId} (created)");
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ class Database
|
||||
]);
|
||||
$this->configureSchema();
|
||||
printf("Database INIT: %s\n", $this->dataDirectory);
|
||||
|
||||
Loop::addPeriodicTimer(3600, fn() => $this->queryOne("VACUUM"));
|
||||
}
|
||||
|
||||
private function configureSchema(): void
|
||||
@@ -408,6 +410,9 @@ class Database
|
||||
|
||||
public function updateCase(array $data, int $id): void
|
||||
{
|
||||
$ckey = "case:{$id}";
|
||||
$this->cache->delete($ckey);
|
||||
|
||||
$data['updated'] = date('Y-m-d H:i:s P');
|
||||
$this->connection->update('cases', $data, [ 'id' => $id ]);
|
||||
}
|
||||
@@ -422,6 +427,9 @@ class Database
|
||||
*/
|
||||
public function addCaseMessage(int $id, int $personId, array $message): array
|
||||
{
|
||||
$ckey = "case:{$id}";
|
||||
$this->cache->delete($ckey);
|
||||
|
||||
$data = [
|
||||
'case_id' => $id,
|
||||
'person_id' => $personId,
|
||||
|
||||
@@ -175,6 +175,7 @@ class MailerService
|
||||
$this->syncImapInbox();
|
||||
$this->sendSpool();
|
||||
$this->processInbox();
|
||||
$this->prune();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,4 +349,13 @@ class MailerService
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function prune(): void
|
||||
{
|
||||
$duration = $this->config->get(Configuration::CONFIG_MAIL_SPOOL_EXPIRY);
|
||||
$secs = duration($duration);
|
||||
$cutoff = date("Y-m-d H:i:s P",time()-$secs);
|
||||
$this->database->queryOne("DELETE FROM inbox WHERE received < :cutoff", [ 'cutoff' => $cutoff ]);
|
||||
$this->database->queryOne("DELETE FROM outbox WHERE sent < :cutoff", [ 'cutoff' => $cutoff ]);
|
||||
}
|
||||
}
|
||||
|
||||
31
src/functions.php
Normal file
31
src/functions.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Convert a duration
|
||||
*
|
||||
* @param integer|string $duration Duration as string or integer
|
||||
* @param string $base Base unit to use when duration is an integer
|
||||
* @return integer
|
||||
*/
|
||||
function duration(int|string $duration, string $base='s'): int {
|
||||
if (ctype_digit($duration)) {
|
||||
$duration = sprintf("%d%s", $duration, $base);
|
||||
}
|
||||
if (preg_match_all("|((\d+)(\w))|", "7d12h", $m)) {
|
||||
$range = array_combine($m[3], $m[2]);
|
||||
$result = 0;
|
||||
foreach ($range as $unit=>$value) {
|
||||
$result += ($value * match ($unit) {
|
||||
'w' => 60*60*24*7,
|
||||
'd' => 60*60*24,
|
||||
'h' => 60*60,
|
||||
'm' => 60,
|
||||
's' => 1,
|
||||
default => throw new \Error("Invalid base unit passed to duration(): {$base}"),
|
||||
});
|
||||
}
|
||||
return $result;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user