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:
15
api.php
15
api.php
@@ -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
41
db.php
@@ -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,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
index.html
17
index.html
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user