Automaattisäännöt: vastaanottaja-ehto, prioriteetti, abuse-tyyppi

- Lisää "Vastaanottaja sisältää" -ehto (to_contains) sääntöihin
- Lisää "Aseta prioriteetti" -toimenpide (set_priority)
- Lisää "Abuse" tikettityyppi
- Korjaa DB-schema: subject_contains, to_contains, enabled, set_priority, set_tags sarakkeet
- Parsii To-headerit sähköposteista säännön matchausta varten
- Mahdollistaa esim. abuse@-postien automaattisen tyypityksen ja prioriteetin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 12:27:29 +02:00
parent c0b003c2f9
commit 306dc6c5cc
4 changed files with 66 additions and 15 deletions

15
api.php
View File

@@ -368,6 +368,10 @@ class ImapClient {
$date = $dateStr ? @date('Y-m-d H:i:s', strtotime($dateStr)) : date('Y-m-d H:i:s'); $date = $dateStr ? @date('Y-m-d H:i:s', strtotime($dateStr)) : date('Y-m-d H:i:s');
if (!$date) $date = date('Y-m-d H:i:s'); if (!$date) $date = date('Y-m-d H:i:s');
// Parse To
$toRaw = $this->decodeMimeHeader($headers['to'] ?? '');
$toEmails = $this->parseCcAddresses($toRaw);
// Parse CC // Parse CC
$ccRaw = $this->decodeMimeHeader($headers['cc'] ?? ''); $ccRaw = $this->decodeMimeHeader($headers['cc'] ?? '');
$ccEmails = $this->parseCcAddresses($ccRaw); $ccEmails = $this->parseCcAddresses($ccRaw);
@@ -379,6 +383,7 @@ class ImapClient {
'subject' => $subject, 'subject' => $subject,
'from_email' => $fromParsed['email'], 'from_email' => $fromParsed['email'],
'from_name' => $this->decodeMimeHeader($fromParsed['name']), 'from_name' => $this->decodeMimeHeader($fromParsed['name']),
'to' => $toEmails,
'message_id' => $messageId, 'message_id' => $messageId,
'in_reply_to' => $inReplyTo, 'in_reply_to' => $inReplyTo,
'references' => $references, 'references' => $references,
@@ -3011,6 +3016,7 @@ switch ($action) {
]; ];
// Apply auto-rules // Apply auto-rules
$toAddresses = implode(' ', $email['to'] ?? []);
foreach ($rules as $rule) { foreach ($rules as $rule) {
if (empty($rule['enabled'])) continue; if (empty($rule['enabled'])) continue;
$match = true; $match = true;
@@ -3026,9 +3032,16 @@ switch ($action) {
$match = false; $match = false;
} }
} }
if (!empty($rule['to_contains'])) {
$needle = strtolower($rule['to_contains']);
if (strpos(strtolower($toAddresses), $needle) === false) {
$match = false;
}
}
if ($match) { if ($match) {
if (!empty($rule['set_status'])) $ticket['status'] = $rule['set_status']; if (!empty($rule['set_status'])) $ticket['status'] = $rule['set_status'];
if (!empty($rule['set_type'])) $ticket['type'] = $rule['set_type']; if (!empty($rule['set_type'])) $ticket['type'] = $rule['set_type'];
if (!empty($rule['set_priority'])) $ticket['priority'] = $rule['set_priority'];
if (!empty($rule['set_tags'])) { if (!empty($rule['set_tags'])) {
$ruleTags = array_map('trim', explode(',', $rule['set_tags'])); $ruleTags = array_map('trim', explode(',', $rule['set_tags']));
$ticket['tags'] = array_values(array_unique(array_merge($ticket['tags'], $ruleTags))); $ticket['tags'] = array_values(array_unique(array_merge($ticket['tags'], $ruleTags)));
@@ -3426,8 +3439,10 @@ switch ($action) {
'name' => trim($input['name'] ?? ''), 'name' => trim($input['name'] ?? ''),
'from_contains' => trim($input['from_contains'] ?? ''), 'from_contains' => trim($input['from_contains'] ?? ''),
'subject_contains' => trim($input['subject_contains'] ?? ''), 'subject_contains' => trim($input['subject_contains'] ?? ''),
'to_contains' => trim($input['to_contains'] ?? ''),
'set_status' => $input['set_status'] ?? '', 'set_status' => $input['set_status'] ?? '',
'set_type' => $input['set_type'] ?? '', 'set_type' => $input['set_type'] ?? '',
'set_priority' => $input['set_priority'] ?? '',
'set_tags' => trim($input['set_tags'] ?? ''), 'set_tags' => trim($input['set_tags'] ?? ''),
'auto_close_days' => intval($input['auto_close_days'] ?? 0), 'auto_close_days' => intval($input['auto_close_days'] ?? 0),
'enabled' => $input['enabled'] ?? true, 'enabled' => $input['enabled'] ?? true,

41
db.php
View File

@@ -617,6 +617,11 @@ function initDatabase(): void {
"ALTER TABLE devices ADD COLUMN laitetila_id VARCHAR(20) DEFAULT NULL AFTER site_id", "ALTER TABLE devices ADD COLUMN laitetila_id VARCHAR(20) DEFAULT NULL AFTER site_id",
"ALTER TABLE document_folders ADD COLUMN customer_id VARCHAR(20) DEFAULT NULL AFTER company_id", "ALTER TABLE document_folders ADD COLUMN customer_id VARCHAR(20) DEFAULT NULL AFTER company_id",
"ALTER TABLE customer_connections ADD COLUMN gateway_device_id VARCHAR(20) DEFAULT NULL AFTER ip", "ALTER TABLE customer_connections ADD COLUMN gateway_device_id VARCHAR(20) DEFAULT NULL AFTER ip",
"ALTER TABLE ticket_rules ADD COLUMN subject_contains VARCHAR(255) DEFAULT '' AFTER from_contains",
"ALTER TABLE ticket_rules ADD COLUMN to_contains VARCHAR(255) DEFAULT '' AFTER subject_contains",
"ALTER TABLE ticket_rules ADD COLUMN enabled BOOLEAN DEFAULT TRUE AFTER auto_close_days",
"ALTER TABLE ticket_rules ADD COLUMN set_priority VARCHAR(20) DEFAULT '' AFTER type_set",
"ALTER TABLE ticket_rules ADD COLUMN set_tags VARCHAR(255) DEFAULT '' AFTER set_priority",
]; ];
foreach ($alters as $sql) { foreach ($alters as $sql) {
try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ }
@@ -1538,6 +1543,7 @@ function dbLoadTicketRules(string $companyId): array {
foreach ($rules as &$r) { foreach ($rules as &$r) {
$r['priority'] = (int)$r['priority']; $r['priority'] = (int)$r['priority'];
$r['auto_close_days'] = (int)$r['auto_close_days']; $r['auto_close_days'] = (int)$r['auto_close_days'];
$r['enabled'] = !empty($r['enabled']);
unset($r['company_id']); unset($r['company_id']);
} }
return $rules; return $rules;
@@ -1545,23 +1551,30 @@ function dbLoadTicketRules(string $companyId): array {
function dbSaveTicketRule(string $companyId, array $rule): void { function dbSaveTicketRule(string $companyId, array $rule): void {
_dbExecute(" _dbExecute("
INSERT INTO ticket_rules (id, company_id, name, from_contains, priority, tag, assign_to, status_set, type_set, auto_close_days) INSERT INTO ticket_rules (id, company_id, name, from_contains, subject_contains, to_contains, priority, tag, assign_to, status_set, type_set, set_priority, set_tags, auto_close_days, enabled)
VALUES (:id, :company_id, :name, :from_contains, :priority, :tag, :assign_to, :status_set, :type_set, :auto_close_days) VALUES (:id, :company_id, :name, :from_contains, :subject_contains, :to_contains, :priority, :tag, :assign_to, :status_set, :type_set, :set_priority, :set_tags, :auto_close_days, :enabled)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
name = VALUES(name), from_contains = VALUES(from_contains), priority = VALUES(priority), name = VALUES(name), from_contains = VALUES(from_contains), subject_contains = VALUES(subject_contains),
to_contains = VALUES(to_contains), priority = VALUES(priority),
tag = VALUES(tag), assign_to = VALUES(assign_to), status_set = VALUES(status_set), tag = VALUES(tag), assign_to = VALUES(assign_to), status_set = VALUES(status_set),
type_set = VALUES(type_set), auto_close_days = VALUES(auto_close_days) type_set = VALUES(type_set), set_priority = VALUES(set_priority), set_tags = VALUES(set_tags),
auto_close_days = VALUES(auto_close_days), enabled = VALUES(enabled)
", [ ", [
'id' => $rule['id'], 'id' => $rule['id'],
'company_id' => $companyId, 'company_id' => $companyId,
'name' => $rule['name'] ?? '', 'name' => $rule['name'] ?? '',
'from_contains' => $rule['from_contains'] ?? '', 'from_contains' => $rule['from_contains'] ?? '',
'priority' => $rule['priority'] ?? 0, 'subject_contains' => $rule['subject_contains'] ?? '',
'tag' => $rule['tag'] ?? '', 'to_contains' => $rule['to_contains'] ?? '',
'assign_to' => $rule['assign_to'] ?? '', 'priority' => $rule['priority'] ?? 0,
'status_set' => $rule['status_set'] ?? '', 'tag' => $rule['tag'] ?? '',
'type_set' => $rule['type_set'] ?? '', 'assign_to' => $rule['assign_to'] ?? '',
'auto_close_days' => $rule['auto_close_days'] ?? 0, 'status_set' => $rule['status_set'] ?? '',
'type_set' => $rule['type_set'] ?? '',
'set_priority' => $rule['set_priority'] ?? '',
'set_tags' => $rule['set_tags'] ?? '',
'auto_close_days' => $rule['auto_close_days'] ?? 0,
'enabled' => !empty($rule['enabled']) ? 1 : 0,
]); ]);
} }

View File

@@ -1082,6 +1082,7 @@
<option value="laskutus">Laskutus</option> <option value="laskutus">Laskutus</option>
<option value="tekniikka">Tekniikka</option> <option value="tekniikka">Tekniikka</option>
<option value="vika">Vika</option> <option value="vika">Vika</option>
<option value="abuse">Abuse</option>
<option value="muu">Muu</option> <option value="muu">Muu</option>
</select> </select>
<select id="ticket-status-filter" style="padding:9px 12px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.88rem;"> <select id="ticket-status-filter" style="padding:9px 12px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.88rem;">
@@ -1213,12 +1214,16 @@
<input type="text" id="rule-form-name" placeholder="esim. Sulje notifikaatiot"> <input type="text" id="rule-form-name" placeholder="esim. Sulje notifikaatiot">
</div> </div>
<div class="form-group full-width" style="margin-top:0.5rem;"> <div class="form-group full-width" style="margin-top:0.5rem;">
<label style="font-weight:600;color:#0f3460;">Ehdot (molemmat pitää täsmätä jos täytetty)</label> <label style="font-weight:600;color:#0f3460;">Ehdot (kaikki täytetyt pitää täsmätä)</label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Lähettäjä sisältää</label> <label>Lähettäjä sisältää</label>
<input type="text" id="rule-form-from" placeholder="esim. noreply@"> <input type="text" id="rule-form-from" placeholder="esim. noreply@">
</div> </div>
<div class="form-group">
<label>Vastaanottaja sisältää</label>
<input type="text" id="rule-form-to" placeholder="esim. abuse@ tai laskutus@">
</div>
<div class="form-group"> <div class="form-group">
<label>Otsikko sisältää</label> <label>Otsikko sisältää</label>
<input type="text" id="rule-form-subject" placeholder="esim. saatavuuskysely"> <input type="text" id="rule-form-subject" placeholder="esim. saatavuuskysely">
@@ -1242,9 +1247,19 @@
<option value="laskutus">Laskutus</option> <option value="laskutus">Laskutus</option>
<option value="tekniikka">Tekniikka</option> <option value="tekniikka">Tekniikka</option>
<option value="vika">Vika</option> <option value="vika">Vika</option>
<option value="abuse">Abuse</option>
<option value="muu">Muu</option> <option value="muu">Muu</option>
</select> </select>
</div> </div>
<div class="form-group">
<label>Aseta prioriteetti</label>
<select id="rule-form-priority">
<option value="">Ei muuteta</option>
<option value="normaali">Normaali</option>
<option value="tärkeä">Tärkeä</option>
<option value="urgent">Kiireellinen</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label>Aseta tagit (pilkulla eroteltuna)</label> <label>Aseta tagit (pilkulla eroteltuna)</label>
<input type="text" id="rule-form-tags" placeholder="esim. notification, automaatti"> <input type="text" id="rule-form-tags" placeholder="esim. notification, automaatti">

View File

@@ -1348,6 +1348,7 @@ const ticketTypeLabels = {
laskutus: 'Laskutus', laskutus: 'Laskutus',
tekniikka: 'Tekniikka', tekniikka: 'Tekniikka',
vika: 'Vika', vika: 'Vika',
abuse: 'Abuse',
muu: 'Muu', muu: 'Muu',
}; };
@@ -1991,13 +1992,16 @@ function renderRules() {
list.innerHTML = '<div style="text-align:center;padding:2rem;color:#aaa;">Ei sääntöjä vielä. Lisää ensimmäinen sääntö.</div>'; list.innerHTML = '<div style="text-align:center;padding:2rem;color:#aaa;">Ei sääntöjä vielä. Lisää ensimmäinen sääntö.</div>';
return; return;
} }
const priorityLabels = { normaali: 'Normaali', 'tärkeä': 'Tärkeä', urgent: 'Kiireellinen' };
list.innerHTML = ticketRules.map(r => { list.innerHTML = ticketRules.map(r => {
const conditions = []; const conditions = [];
if (r.from_contains) conditions.push('Lähettäjä: <strong>' + esc(r.from_contains) + '</strong>'); if (r.from_contains) conditions.push('Lähettäjä: <strong>' + esc(r.from_contains) + '</strong>');
if (r.to_contains) conditions.push('Vastaanottaja: <strong>' + esc(r.to_contains) + '</strong>');
if (r.subject_contains) conditions.push('Otsikko: <strong>' + esc(r.subject_contains) + '</strong>'); if (r.subject_contains) conditions.push('Otsikko: <strong>' + esc(r.subject_contains) + '</strong>');
const actions = []; const actions = [];
if (r.set_status) actions.push('Tila → ' + (ticketStatusLabels[r.set_status] || r.set_status)); if (r.set_status) actions.push('Tila → ' + (ticketStatusLabels[r.set_status] || r.set_status));
if (r.set_type) actions.push('Tyyppi → ' + (ticketTypeLabels[r.set_type] || r.set_type)); if (r.set_type) actions.push('Tyyppi → ' + (ticketTypeLabels[r.set_type] || r.set_type));
if (r.set_priority) actions.push('Prioriteetti → ' + (priorityLabels[r.set_priority] || r.set_priority));
if (r.set_tags) actions.push('Tagit: #' + r.set_tags.split(',').map(t => t.trim()).join(' #')); if (r.set_tags) actions.push('Tagit: #' + r.set_tags.split(',').map(t => t.trim()).join(' #'));
if (r.auto_close_days) actions.push('Auto-close: ' + r.auto_close_days + 'pv'); if (r.auto_close_days) actions.push('Auto-close: ' + r.auto_close_days + 'pv');
return `<div style="display:flex;justify-content:space-between;align-items:center;padding:0.75rem 1rem;background:${r.enabled ? '#f8f9fb' : '#fafafa'};border:1px solid #e8ebf0;border-radius:8px;margin-bottom:0.5rem;opacity:${r.enabled ? '1' : '0.5'};"> return `<div style="display:flex;justify-content:space-between;align-items:center;padding:0.75rem 1rem;background:${r.enabled ? '#f8f9fb' : '#fafafa'};border:1px solid #e8ebf0;border-radius:8px;margin-bottom:0.5rem;opacity:${r.enabled ? '1' : '0.5'};">
@@ -2038,9 +2042,11 @@ function showRuleForm(rule) {
document.getElementById('rule-form-id').value = rule ? rule.id : ''; document.getElementById('rule-form-id').value = rule ? rule.id : '';
document.getElementById('rule-form-name').value = rule ? rule.name : ''; document.getElementById('rule-form-name').value = rule ? rule.name : '';
document.getElementById('rule-form-from').value = rule ? rule.from_contains : ''; document.getElementById('rule-form-from').value = rule ? rule.from_contains : '';
document.getElementById('rule-form-to').value = rule ? (rule.to_contains || '') : '';
document.getElementById('rule-form-subject').value = rule ? rule.subject_contains : ''; document.getElementById('rule-form-subject').value = rule ? rule.subject_contains : '';
document.getElementById('rule-form-status').value = rule ? (rule.set_status || '') : ''; document.getElementById('rule-form-status').value = rule ? (rule.set_status || '') : '';
document.getElementById('rule-form-type').value = rule ? (rule.set_type || '') : ''; document.getElementById('rule-form-type').value = rule ? (rule.set_type || '') : '';
document.getElementById('rule-form-priority').value = rule ? (rule.set_priority || '') : '';
document.getElementById('rule-form-tags').value = rule ? (rule.set_tags || '') : ''; document.getElementById('rule-form-tags').value = rule ? (rule.set_tags || '') : '';
document.getElementById('rule-form-autoclose').value = rule ? (rule.auto_close_days || '') : ''; document.getElementById('rule-form-autoclose').value = rule ? (rule.auto_close_days || '') : '';
editingRuleId = rule ? rule.id : null; editingRuleId = rule ? rule.id : null;
@@ -2062,9 +2068,11 @@ document.getElementById('btn-save-rule').addEventListener('click', async () => {
const data = { const data = {
name, name,
from_contains: document.getElementById('rule-form-from').value.trim(), from_contains: document.getElementById('rule-form-from').value.trim(),
to_contains: document.getElementById('rule-form-to').value.trim(),
subject_contains: document.getElementById('rule-form-subject').value.trim(), subject_contains: document.getElementById('rule-form-subject').value.trim(),
set_status: document.getElementById('rule-form-status').value, set_status: document.getElementById('rule-form-status').value,
set_type: document.getElementById('rule-form-type').value, set_type: document.getElementById('rule-form-type').value,
set_priority: document.getElementById('rule-form-priority').value,
set_tags: document.getElementById('rule-form-tags').value.trim(), set_tags: document.getElementById('rule-form-tags').value.trim(),
auto_close_days: parseInt(document.getElementById('rule-form-autoclose').value) || 0, auto_close_days: parseInt(document.getElementById('rule-form-autoclose').value) || 0,
enabled: true, enabled: true,