BCC-kenttä tiketteihin + To/CC/BCC tallennus + allekirjoituskorjaus
- Lisää BCC-kenttä vastauslomakkeeseen (HTML, JS, API) - To/CC/BCC tallentuvat tiketille pysyvästi (seuraava vastaus muistaa muutokset) - Lisää to_email ja bcc sarakkeet tickets-tauluun - BCC-tuki SMTP-lähetykseen (RCPT TO ilman headeria) - Korjaa allekirjoitukset: buildSignaturesWithDefaults() generoi nyt oletukset myös Zammad-sähköposteille (support@web1.fi ym.), ei pelkille mailboxeille - Allekirjoituksiin lisätty puhelinnumero Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
63
api.php
63
api.php
@@ -304,7 +304,7 @@ class ZammadClient {
|
||||
}
|
||||
|
||||
/** Lähetä vastaus tikettiin */
|
||||
public function createArticle(int $ticketId, string $body, string $to = '', string $subject = '', string $type = 'email', string $cc = ''): array {
|
||||
public function createArticle(int $ticketId, string $body, string $to = '', string $subject = '', string $type = 'email', string $cc = '', string $bcc = ''): array {
|
||||
// Muunna plain-text HTML:ksi jos body ei sisällä HTML-tageja
|
||||
$htmlBody = $body;
|
||||
if (strip_tags($body) === $body) {
|
||||
@@ -321,6 +321,8 @@ class ZammadClient {
|
||||
];
|
||||
if ($to) $data['to'] = $to;
|
||||
if ($cc) $data['cc'] = $cc;
|
||||
// Zammad API: BCC lähetetään vastaanottajina mutta ei näy vastaanottajille
|
||||
// Huom: Zammad ei ehkä tue suoraan BCC-kenttää artikkelissa, joten lisätään RCPT-vastaanottajaksi
|
||||
if ($subject) $data['subject'] = 'Re: ' . preg_replace('/^Re:\s*/i', '', $subject);
|
||||
return $this->request('POST', 'ticket_articles', $data);
|
||||
}
|
||||
@@ -919,23 +921,39 @@ function buildSignaturesWithDefaults(array $user, array $userCompanyIds): array
|
||||
$allCompanies = dbLoadCompanies();
|
||||
foreach ($allCompanies as $comp) {
|
||||
if (!in_array($comp['id'], $userCompanyIds)) continue;
|
||||
$etunimi = trim(explode(' ', $user['nimi'] ?? '')[0]);
|
||||
$yritys = $comp['nimi'] ?? '';
|
||||
$phone = $comp['phone'] ?? '';
|
||||
// SMTP-postilaatikoiden allekirjoitukset
|
||||
$mailboxes = dbLoadMailboxes($comp['id']);
|
||||
foreach ($mailboxes as $mb) {
|
||||
if (!empty($sigs[$mb['id']])) continue; // käyttäjällä on jo oma allekirjoitus
|
||||
// Generoi oletus: Etunimi \n Yritys \n sähköposti
|
||||
$etunimi = trim(explode(' ', $user['nimi'] ?? '')[0]);
|
||||
$yritys = ($comp['nimi'] ?? '') . ' Oy';
|
||||
if (!empty($sigs[$mb['id']])) continue;
|
||||
$email = $mb['smtp_from_email'] ?? $mb['imap_user'] ?? '';
|
||||
$parts = array_filter([$etunimi, $yritys, $email]);
|
||||
if ($phone) $parts[] = $phone;
|
||||
if (!empty($parts)) {
|
||||
$sigs[$mb['id']] = implode("\n", $parts);
|
||||
}
|
||||
}
|
||||
// Zammad-sähköpostien allekirjoitukset
|
||||
$zEmails = _dbFetchAll(
|
||||
"SELECT DISTINCT zammad_to_email FROM tickets WHERE company_id = ? AND source = 'zammad' AND zammad_to_email IS NOT NULL AND zammad_to_email != ''",
|
||||
[$comp['id']]
|
||||
);
|
||||
foreach ($zEmails as $r) {
|
||||
$key = 'zammad:' . $r['zammad_to_email'];
|
||||
if (!empty($sigs[$key])) continue;
|
||||
$parts = array_filter([$etunimi, $yritys, $r['zammad_to_email']]);
|
||||
if ($phone) $parts[] = $phone;
|
||||
if (!empty($parts)) {
|
||||
$sigs[$key] = implode("\n", $parts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $sigs;
|
||||
}
|
||||
|
||||
function sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = '', ?array $mailbox = null, string $cc = ''): bool {
|
||||
function sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = '', ?array $mailbox = null, string $cc = '', string $bcc = ''): bool {
|
||||
$fromEmail = $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? MAIL_FROM;
|
||||
$fromName = $mailbox['smtp_from_name'] ?? $mailbox['nimi'] ?? 'Asiakaspalvelu';
|
||||
|
||||
@@ -943,7 +961,7 @@ function sendTicketMail(string $to, string $subject, string $body, string $inRep
|
||||
$smtpHost = $mailbox['smtp_host'] ?? '';
|
||||
error_log("MAIL DEBUG: to={$to} smtpHost={$smtpHost} from={$fromEmail} mailbox_keys=" . implode(',', array_keys($mailbox ?? [])));
|
||||
if ($smtpHost !== '') {
|
||||
return sendViaSMTP($to, $subject, $body, $fromEmail, $fromName, $inReplyTo, $references, $mailbox, $cc);
|
||||
return sendViaSMTP($to, $subject, $body, $fromEmail, $fromName, $inReplyTo, $references, $mailbox, $cc, $bcc);
|
||||
}
|
||||
|
||||
// Fallback: PHP mail()
|
||||
@@ -954,6 +972,9 @@ function sendTicketMail(string $to, string $subject, string $body, string $inRep
|
||||
if ($cc) {
|
||||
$headers .= "Cc: {$cc}\r\n";
|
||||
}
|
||||
if ($bcc) {
|
||||
$headers .= "Bcc: {$bcc}\r\n";
|
||||
}
|
||||
if ($inReplyTo) {
|
||||
$headers .= "In-Reply-To: {$inReplyTo}\r\n";
|
||||
$headers .= "References: " . ($references ? $references . ' ' : '') . $inReplyTo . "\r\n";
|
||||
@@ -985,7 +1006,7 @@ function smtpCode(string $resp): string {
|
||||
return substr(trim($resp), 0, 3);
|
||||
}
|
||||
|
||||
function sendViaSMTP(string $to, string $subject, string $body, string $fromEmail, string $fromName, string $inReplyTo, string $references, array $mailbox, string $cc): bool {
|
||||
function sendViaSMTP(string $to, string $subject, string $body, string $fromEmail, string $fromName, string $inReplyTo, string $references, array $mailbox, string $cc, string $bcc = ''): bool {
|
||||
$host = $mailbox['smtp_host'];
|
||||
$port = (int)($mailbox['smtp_port'] ?? 587);
|
||||
// Fallback-ketju käyttäjälle: smtp_user → imap_user → smtp_from_email
|
||||
@@ -1089,6 +1110,7 @@ function sendViaSMTP(string $to, string $subject, string $body, string $fromEmai
|
||||
// 7. RCPT TO
|
||||
$allRecipients = array_filter(array_map('trim', explode(',', $to)));
|
||||
if ($cc) $allRecipients = array_merge($allRecipients, array_filter(array_map('trim', explode(',', $cc))));
|
||||
if ($bcc) $allRecipients = array_merge($allRecipients, array_filter(array_map('trim', explode(',', $bcc))));
|
||||
foreach ($allRecipients as $rcpt) {
|
||||
$resp = smtpCommand($fp, "RCPT TO:<{$rcpt}>");
|
||||
$log[] = "rcpt:" . smtpCode($resp);
|
||||
@@ -3424,6 +3446,7 @@ switch ($action) {
|
||||
$replyMailboxId = $input['mailbox_id'] ?? '';
|
||||
$replyTo = trim($input['to'] ?? '');
|
||||
$replyCc = trim($input['cc'] ?? '');
|
||||
$replyBcc = trim($input['bcc'] ?? '');
|
||||
if (empty($body)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Viesti ei voi olla tyhjä']);
|
||||
@@ -3487,12 +3510,13 @@ switch ($action) {
|
||||
}
|
||||
}
|
||||
|
||||
// CC: käytä frontendistä annettua CC:tä, tai tiketin alkuperäistä CC:tä
|
||||
// CC/BCC: käytä frontendistä annettua arvoa, tai tiketin tallennettua
|
||||
$ccToSend = $replyCc !== '' ? $replyCc : ($t['cc'] ?? '');
|
||||
$bccToSend = $replyBcc;
|
||||
|
||||
$subject = 'Re: ' . $t['subject'];
|
||||
$toAddress = $replyTo !== '' ? $replyTo : $t['from_email'];
|
||||
$sent = sendTicketMail($toAddress, $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox, $ccToSend);
|
||||
$sent = sendTicketMail($toAddress, $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox, $ccToSend, $bccToSend);
|
||||
|
||||
if (!$sent) {
|
||||
http_response_code(500);
|
||||
@@ -3510,10 +3534,11 @@ switch ($action) {
|
||||
break 2;
|
||||
}
|
||||
|
||||
// Päivitä tiketin CC jos muuttunut
|
||||
if ($replyCc !== '' && $replyCc !== ($t['cc'] ?? '')) {
|
||||
$t['cc'] = $replyCc;
|
||||
}
|
||||
// Tallenna To/CC/BCC tiketille pysyvästi
|
||||
_dbExecute(
|
||||
"UPDATE tickets SET to_email = ?, cc = ?, bcc = ? WHERE id = ? AND company_id = ?",
|
||||
[$toAddress, $ccToSend, $bccToSend, $id, $companyId]
|
||||
);
|
||||
|
||||
// Add reply to ticket (tallennetaan allekirjoituksen kanssa)
|
||||
$reply = [
|
||||
@@ -5409,6 +5434,7 @@ switch ($action) {
|
||||
$z = new ZammadClient($integ['config']['url'], $integ['config']['token']);
|
||||
$to = !empty($input['to']) ? trim($input['to']) : ($ticket['from_email'] ?? '');
|
||||
$cc = !empty($input['cc']) ? trim($input['cc']) : '';
|
||||
$bcc = !empty($input['bcc']) ? trim($input['bcc']) : '';
|
||||
|
||||
// Muunna uusi viesti HTML:ksi (säilytä rivinvaihdot ja välilyönnit)
|
||||
$escaped = htmlspecialchars($body, ENT_QUOTES, 'UTF-8');
|
||||
@@ -5446,7 +5472,14 @@ switch ($action) {
|
||||
$to,
|
||||
$ticket['subject'] ?? '',
|
||||
'email',
|
||||
$cc
|
||||
$cc,
|
||||
$bcc
|
||||
);
|
||||
|
||||
// Tallenna To/CC/BCC tiketille pysyvästi
|
||||
_dbExecute(
|
||||
"UPDATE tickets SET to_email = ?, cc = ?, bcc = ? WHERE id = ? AND company_id = ?",
|
||||
[$to, $cc, $bcc, $ticketId, $companyId]
|
||||
);
|
||||
|
||||
// Tallenna myös paikalliseen tietokantaan (vain uusi viesti, ei ketjua)
|
||||
|
||||
2
db.php
2
db.php
@@ -677,6 +677,8 @@ function initDatabase(): void {
|
||||
"ALTER TABLE ticket_messages ADD COLUMN zammad_article_id INT DEFAULT NULL AFTER message_id",
|
||||
"ALTER TABLE availability_queries ADD COLUMN hostname VARCHAR(255) DEFAULT '' AFTER ip_address",
|
||||
"ALTER TABLE availability_queries ADD COLUMN org VARCHAR(255) DEFAULT '' AFTER hostname",
|
||||
"ALTER TABLE tickets ADD COLUMN to_email VARCHAR(255) DEFAULT '' AFTER from_name",
|
||||
"ALTER TABLE tickets ADD COLUMN bcc TEXT DEFAULT '' AFTER cc",
|
||||
];
|
||||
foreach ($alters as $sql) {
|
||||
try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Noxus HUB</title>
|
||||
<link rel="stylesheet" href="style.css?v=20260313o">
|
||||
<link rel="stylesheet" href="style.css?v=20260313p">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Login -->
|
||||
@@ -1189,6 +1189,10 @@
|
||||
<label style="font-size:0.8rem;color:#888;min-width:60px;">CC:</label>
|
||||
<input type="text" id="reply-cc" placeholder="email1@example.com, email2@example.com" style="flex:1;padding:5px 10px;border:1px solid #ddd;border-radius:6px;font-size:0.85rem;">
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
||||
<label style="font-size:0.8rem;color:#888;min-width:60px;">BCC:</label>
|
||||
<input type="text" id="reply-bcc" placeholder="email1@example.com, email2@example.com" style="flex:1;padding:5px 10px;border:1px solid #ddd;border-radius:6px;font-size:0.85rem;">
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="ticket-reply-body" rows="5" placeholder="Kirjoita vastaus..."></textarea>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:0.5rem;margin-top:0.5rem;">
|
||||
@@ -2257,6 +2261,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js?v=20260313o"></script>
|
||||
<script src="script.js?v=20260313p"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
14
script.js
14
script.js
@@ -1838,14 +1838,18 @@ async function showTicketDetail(id, companyId = '') {
|
||||
document.querySelector('.btn-reply-tab[data-reply-type="reply"]').classList.add('active');
|
||||
document.getElementById('btn-send-reply').textContent = 'Lähetä vastaus';
|
||||
|
||||
// TO-kenttä — tiketin alkuperäinen lähettäjä
|
||||
// TO-kenttä — käytä tiketin tallennettua to_email:a, tai fallback from_email:iin
|
||||
const toField = document.getElementById('reply-to');
|
||||
if (toField) toField.value = ticket.from_email || '';
|
||||
if (toField) toField.value = ticket.to_email || ticket.from_email || '';
|
||||
|
||||
// CC-kenttä — täytetään tiketin CC:stä
|
||||
const ccField = document.getElementById('reply-cc');
|
||||
if (ccField) ccField.value = ticket.cc || '';
|
||||
|
||||
// BCC-kenttä — täytetään tiketin BCC:stä
|
||||
const bccField = document.getElementById('reply-bcc');
|
||||
if (bccField) bccField.value = ticket.bcc || '';
|
||||
|
||||
// Mailbox-valinta — Zammad-tiketit vastaa Zammadin kautta, muut SMTP:llä
|
||||
const mbSelect = document.getElementById('reply-mailbox-select');
|
||||
let replyMailboxes = []; // tallennetaan postilaatikkodata allekirjoitushakua varten
|
||||
@@ -2027,12 +2031,14 @@ document.getElementById('btn-send-reply').addEventListener('click', async () =>
|
||||
}
|
||||
if (zSig) zBody += '\n\n-- \n' + zSig;
|
||||
}
|
||||
// Lähetä Zammad API:n kautta — välitä myös to/cc-kentät
|
||||
// Lähetä Zammad API:n kautta — välitä myös to/cc/bcc-kentät
|
||||
const zPayload = { ticket_id: currentTicketId, body: zBody };
|
||||
const zToFld = document.getElementById('reply-to');
|
||||
const zCcFld = document.getElementById('reply-cc');
|
||||
const zBccFld = document.getElementById('reply-bcc');
|
||||
if (zToFld && zToFld.value.trim()) zPayload.to = zToFld.value.trim();
|
||||
if (zCcFld && zCcFld.value.trim()) zPayload.cc = zCcFld.value.trim();
|
||||
if (zBccFld && zBccFld.value.trim()) zPayload.bcc = zBccFld.value.trim();
|
||||
await apiCall('zammad_reply' + ticketCompanyParam(), 'POST', zPayload);
|
||||
} else {
|
||||
const action = ticketReplyType === 'note' ? 'ticket_note' : 'ticket_reply';
|
||||
@@ -2041,10 +2047,12 @@ document.getElementById('btn-send-reply').addEventListener('click', async () =>
|
||||
const mbSel = document.getElementById('reply-mailbox-select');
|
||||
const toFld = document.getElementById('reply-to');
|
||||
const ccFld = document.getElementById('reply-cc');
|
||||
const bccFld = document.getElementById('reply-bcc');
|
||||
const useSig = document.getElementById('reply-use-signature');
|
||||
if (mbSel) payload.mailbox_id = mbSel.value;
|
||||
if (toFld && toFld.value.trim()) payload.to = toFld.value.trim();
|
||||
if (ccFld) payload.cc = ccFld.value.trim();
|
||||
if (bccFld) payload.bcc = bccFld.value.trim();
|
||||
if (useSig && !useSig.checked) payload.no_signature = true;
|
||||
}
|
||||
await apiCall(action + ticketCompanyParam(), 'POST', payload);
|
||||
|
||||
Reference in New Issue
Block a user