diff --git a/api.php b/api.php index 983aa19..1957565 100644 --- a/api.php +++ b/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 = '', string $bcc = ''): array { + public function createArticle(int $ticketId, string $body, string $to = '', string $subject = '', string $type = 'email', string $cc = '', string $bcc = '', array $attachments = []): array { // Muunna plain-text HTML:ksi jos body ei sisällä HTML-tageja $htmlBody = $body; if (strip_tags($body) === $body) { @@ -321,9 +321,18 @@ 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); + // Liitetiedostot Zammad-muodossa + if (!empty($attachments)) { + $data['attachments'] = []; + foreach ($attachments as $att) { + $data['attachments'][] = [ + 'filename' => $att['name'] ?? 'attachment', + 'data' => $att['data'] ?? '', // base64 + 'mime-type' => $att['type'] ?? 'application/octet-stream', + ]; + } + } return $this->request('POST', 'ticket_articles', $data); } @@ -953,7 +962,7 @@ function buildSignaturesWithDefaults(array $user, array $userCompanyIds): array return $sigs; } -function sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = '', ?array $mailbox = null, string $cc = '', string $bcc = ''): bool { +function sendTicketMail(string $to, string $subject, string $body, string $inReplyTo = '', string $references = '', ?array $mailbox = null, string $cc = '', string $bcc = '', array $attachments = []): bool { $fromEmail = $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? MAIL_FROM; $fromName = $mailbox['smtp_from_name'] ?? $mailbox['nimi'] ?? 'Asiakaspalvelu'; @@ -961,7 +970,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, $bcc); + return sendViaSMTP($to, $subject, $body, $fromEmail, $fromName, $inReplyTo, $references, $mailbox, $cc, $bcc, $attachments); } // Fallback: PHP mail() @@ -1006,7 +1015,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, string $bcc = ''): bool { +function sendViaSMTP(string $to, string $subject, string $body, string $fromEmail, string $fromName, string $inReplyTo, string $references, array $mailbox, string $cc, string $bcc = '', array $attachments = []): bool { $host = $mailbox['smtp_host']; $port = (int)($mailbox['smtp_port'] ?? 587); // Fallback-ketju käyttäjälle: smtp_user → imap_user → smtp_from_email @@ -1134,11 +1143,40 @@ function sendViaSMTP(string $to, string $subject, string $body, string $fromEmai $msg .= "References: " . ($references ? $references . ' ' : '') . $inReplyTo . "\r\n"; } $msg .= "MIME-Version: 1.0\r\n"; - $msg .= "Content-Type: text/plain; charset=UTF-8\r\n"; - $msg .= "Content-Transfer-Encoding: base64\r\n"; $msg .= "Date: " . date('r') . "\r\n"; - $msg .= "\r\n"; - $msg .= chunk_split(base64_encode($body)); + + if (!empty($attachments)) { + // Multipart MIME — teksti + liitteet + $boundary = '----=_Part_' . uniqid(); + $msg .= "Content-Type: multipart/mixed; boundary=\"{$boundary}\"\r\n"; + $msg .= "\r\n"; + // Tekstiosa + $msg .= "--{$boundary}\r\n"; + $msg .= "Content-Type: text/plain; charset=UTF-8\r\n"; + $msg .= "Content-Transfer-Encoding: base64\r\n"; + $msg .= "\r\n"; + $msg .= chunk_split(base64_encode($body)); + // Liitteet + foreach ($attachments as $att) { + $attName = $att['name'] ?? 'attachment'; + $attType = $att['type'] ?? 'application/octet-stream'; + $attData = $att['data'] ?? ''; + $encodedName = '=?UTF-8?B?' . base64_encode($attName) . '?='; + $msg .= "--{$boundary}\r\n"; + $msg .= "Content-Type: {$attType}; name=\"{$encodedName}\"\r\n"; + $msg .= "Content-Disposition: attachment; filename=\"{$encodedName}\"\r\n"; + $msg .= "Content-Transfer-Encoding: base64\r\n"; + $msg .= "\r\n"; + $msg .= chunk_split($attData); + } + $msg .= "--{$boundary}--\r\n"; + } else { + // Pelkkä teksti ilman liitteitä + $msg .= "Content-Type: text/plain; charset=UTF-8\r\n"; + $msg .= "Content-Transfer-Encoding: base64\r\n"; + $msg .= "\r\n"; + $msg .= chunk_split(base64_encode($body)); + } // Lopeta piste omalla rivillä fwrite($fp, $msg . "\r\n.\r\n"); $resp = smtpReadResponse($fp); @@ -3500,6 +3538,8 @@ switch ($action) { if (!empty($threadMessages)) { $emailBody .= "\n"; foreach ($threadMessages as $tm) { + // Älä sisällytä sisäisiä muistiinpanoja sähköpostiin + if ($tm['type'] === 'note') continue; $tmSender = $tm['from_name'] ?: $tm['from_email']; $tmDate = date('d.m.Y H:i', strtotime($tm['timestamp'])); $tmBody = strip_tags(str_replace(['
', '
', '
', '

', ''], "\n", $tm['body'] ?: '')); @@ -3516,7 +3556,8 @@ switch ($action) { $subject = 'Re: ' . $t['subject']; $toAddress = $replyTo !== '' ? $replyTo : $t['from_email']; - $sent = sendTicketMail($toAddress, $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox, $ccToSend, $bccToSend); + $replyAttachments = $input['attachments'] ?? []; + $sent = sendTicketMail($toAddress, $subject, $emailBody, $lastMsgId, trim($allRefs), $replyMailbox, $ccToSend, $bccToSend, $replyAttachments); if (!$sent) { http_response_code(500); @@ -5448,6 +5489,8 @@ switch ($action) { ); $quotedThread = ''; foreach ($messages as $msg) { + // Älä sisällytä sisäisiä muistiinpanoja sähköpostiin + if ($msg['type'] === 'note') continue; $sender = $msg['from_name'] ?: $msg['from_email']; $date = date('d.m.Y H:i', strtotime($msg['timestamp'])); $msgBody = $msg['body'] ?: ''; @@ -5466,6 +5509,9 @@ switch ($action) { // Yhdistä: uusi viesti (HTML) + viestiketju $fullBody = $newMsgHtml . $quotedThread; + // Liitetiedostot + $attachments = $input['attachments'] ?? []; + $result = $z->createArticle( (int)$ticket['zammad_ticket_id'], $fullBody, @@ -5473,7 +5519,8 @@ switch ($action) { $ticket['subject'] ?? '', 'email', $cc, - $bcc + $bcc, + $attachments ); // Tallenna To/CC/BCC tiketille pysyvästi diff --git a/index.html b/index.html index a9fc24d..9a25001 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ Noxus HUB - + @@ -1195,6 +1195,16 @@ +
+
+ + +
+
+
- + diff --git a/script.js b/script.js index 70d5007..49f61cc 100644 --- a/script.js +++ b/script.js @@ -2002,6 +2002,50 @@ document.querySelectorAll('.btn-reply-tab').forEach(btn => { }); }); +// ---- Liitetiedostot ---- +let replyAttachments = []; // [{name, size, type, data (base64)}] + +document.getElementById('reply-file-input').addEventListener('change', function() { + const files = Array.from(this.files); + files.forEach(file => { + if (file.size > 25 * 1024 * 1024) { + alert(`Tiedosto "${file.name}" on liian suuri (max 25 MB)`); + return; + } + const reader = new FileReader(); + reader.onload = () => { + const base64 = reader.result.split(',')[1]; // poista data:...;base64, prefix + replyAttachments.push({ name: file.name, size: file.size, type: file.type || 'application/octet-stream', data: base64 }); + renderReplyAttachments(); + }; + reader.readAsDataURL(file); + }); + this.value = ''; // nollaa input jotta saman tiedoston voi lisätä uudelleen +}); + +function renderReplyAttachments() { + const list = document.getElementById('reply-attachments-list'); + const count = document.getElementById('reply-attachments-count'); + if (replyAttachments.length === 0) { + list.innerHTML = ''; + count.textContent = ''; + return; + } + count.textContent = `${replyAttachments.length} liite${replyAttachments.length > 1 ? 'ttä' : ''}`; + list.innerHTML = replyAttachments.map((a, i) => { + const sizeStr = a.size < 1024 ? a.size + ' B' : (a.size < 1048576 ? (a.size / 1024).toFixed(1) + ' KB' : (a.size / 1048576).toFixed(1) + ' MB'); + return ` + 📄 ${esc(a.name)} (${sizeStr}) + + `; + }).join(''); +} + +function removeReplyAttachment(index) { + replyAttachments.splice(index, 1); + renderReplyAttachments(); +} + // Send reply or note document.getElementById('btn-send-reply').addEventListener('click', async () => { const body = document.getElementById('ticket-reply-body').value.trim(); @@ -2039,6 +2083,7 @@ document.getElementById('btn-send-reply').addEventListener('click', async () => 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(); + if (replyAttachments.length > 0) zPayload.attachments = replyAttachments; await apiCall('zammad_reply' + ticketCompanyParam(), 'POST', zPayload); } else { const action = ticketReplyType === 'note' ? 'ticket_note' : 'ticket_reply'; @@ -2055,8 +2100,12 @@ document.getElementById('btn-send-reply').addEventListener('click', async () => if (bccFld) payload.bcc = bccFld.value.trim(); if (useSig && !useSig.checked) payload.no_signature = true; } + if (replyAttachments.length > 0) payload.attachments = replyAttachments; await apiCall(action + ticketCompanyParam(), 'POST', payload); } + // Tyhjennä liitteet + replyAttachments = []; + renderReplyAttachments(); // Reload the detail view await showTicketDetail(currentTicketId, currentTicketCompanyId); } catch (e) {