From 376912b9ff215abdd303ef88036771898766994d Mon Sep 17 00:00:00 2001
From: Jukka Lampikoski
Date: Fri, 13 Mar 2026 03:00:30 +0200
Subject: [PATCH] =?UTF-8?q?Liitetiedostot=20tikettivastausten=20s=C3=A4hk?=
=?UTF-8?q?=C3=B6posteihin=20+=20muistiinpanojen=20piilotus?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Lisää liitetiedostojen tuki vastauslomakkeeseen (📎 Liitä tiedosto -nappi)
- Tuki max 25 MB tiedostoille, useita liitteitä kerralla
- Zammad API: liitteet base64-muodossa attachments-kentässä
- SMTP: multipart/mixed MIME (boundary, Content-Disposition: attachment)
- Sisäiset muistiinpanot (note) suodatetaan pois quoted threadista
(eivät näy asiakkaille lähtevissä sähköposteissa)
Co-Authored-By: Claude Opus 4.6
---
api.php | 71 +++++++++++++++++++++++++++++++++++++++++++++---------
index.html | 14 +++++++++--
script.js | 49 +++++++++++++++++++++++++++++++++++++
3 files changed, 120 insertions(+), 14 deletions(-)
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 @@
+
+
+
+
+
+
+
-
+