Phase 2 of ticket attachements by agents

This commit is contained in:
johnnyq
2026-07-29 18:10:34 -04:00
parent 7c8c93c01c
commit 2395a3ee16
8 changed files with 163 additions and 21 deletions

View File

@@ -373,6 +373,26 @@ function addToMailQueue($data) {
$cal_str = mysqli_escape_string($mysqli, $email['cal_str']);
}
// Attachments travel as a manifest of app-root-relative paths rather than
// file contents, so the queue table stays small. cron/mail_queue.php
// re-checks each path is inside uploads/ before attaching it.
$attachments = '';
if (!empty($email['attachments']) && is_array($email['attachments'])) {
$attachment_manifest = [];
foreach ($email['attachments'] as $attachment) {
if (empty($attachment['path']) || empty($attachment['name'])) {
continue;
}
$attachment_manifest[] = [
'path' => strval($attachment['path']),
'name' => strval($attachment['name'])
];
}
if ($attachment_manifest) {
$attachments = mysqli_escape_string($mysqli, json_encode($attachment_manifest));
}
}
// Check if 'email_queued_at' is set and not empty
if (isset($email['queued_at']) && !empty($email['queued_at'])) {
$queued_at = "'" . escapeSql($email['queued_at']) . "'";
@@ -381,7 +401,7 @@ function addToMailQueue($data) {
$queued_at = 'CURRENT_TIMESTAMP()';
}
mysqli_query($mysqli, "INSERT INTO email_queue SET email_recipient = '$recipient', email_recipient_name = '$recipient_name', email_from = '$from', email_from_name = '$from_name', email_subject = '$subject', email_content = '$body', email_queued_at = $queued_at, email_cal_str = '$cal_str'");
mysqli_query($mysqli, "INSERT INTO email_queue SET email_recipient = '$recipient', email_recipient_name = '$recipient_name', email_from = '$from', email_from_name = '$from_name', email_subject = '$subject', email_content = '$body', email_queued_at = $queued_at, email_cal_str = '$cal_str', email_attachments = '$attachments'");
}
return true;

View File

@@ -181,8 +181,10 @@ function cleanupUnusedImages(string $html, string $folderFsPath, string $folderW
* Pass $reply_id to attach to a specific reply, or null to attach to the ticket
* itself - the ticket page reads reply_id IS NULL as "belongs to the ticket".
*
* Returns the number of files stored. Anything the extension allow-list or
* checkFileUpload() rejects is skipped silently, as it always has been.
* Returns a list of what was stored - ['name' => original name, 'path' => path
* relative to the app root, 'size' => bytes] - so a caller can pass the same
* files to the mail queue. Anything the extension allow-list or checkFileUpload()
* rejects is skipped silently, as it always has been.
*/
function saveTicketAttachments($ticket_id, $reply_id = null, $field_name = 'attachments') {
@@ -190,8 +192,10 @@ function saveTicketAttachments($ticket_id, $reply_id = null, $field_name = 'atta
$ticket_id = intval($ticket_id);
$stored_attachments = [];
if (!$ticket_id || empty($_FILES[$field_name]) || !isset($_FILES[$field_name]['name'])) {
return 0;
return $stored_attachments;
}
$allowed_extensions = array(
@@ -203,7 +207,7 @@ function saveTicketAttachments($ticket_id, $reply_id = null, $field_name = 'atta
// A single-file input posts scalars, a multiple one posts arrays - normalize
$names = $_FILES[$field_name]['name'];
if (!is_array($names)) {
return 0;
return $stored_attachments;
}
mkdirMissing('../uploads/tickets/');
@@ -216,8 +220,6 @@ function saveTicketAttachments($ticket_id, $reply_id = null, $field_name = 'atta
$reply_id_sql = intval($reply_id);
}
$files_stored = 0;
for ($i = 0; $i < count($names); $i++) {
$single_file = [
@@ -244,8 +246,43 @@ function saveTicketAttachments($ticket_id, $reply_id = null, $field_name = 'atta
mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$attachment_name', ticket_attachment_reference_name = '$attachment_reference_name', ticket_attachment_reply_id = $reply_id_sql, ticket_attachment_ticket_id = $ticket_id");
$files_stored++;
// Path is relative to the app root, not the caller, so the mail cron can
// resolve it from its own directory
$stored_attachments[] = [
'name' => $single_file['name'],
'path' => "uploads/tickets/$ticket_id/$attachment_reference_name",
'size' => (int) $single_file['size']
];
}
return $files_stored;
return $stored_attachments;
}
/*
* Splits a stored attachment list into what the mail queue will carry and what it
* will not, against MAX_EMAIL_ATTACHMENT_BYTES applied to the message as a whole.
*
* Oversized files stay on the ticket - the recipient can still download them from
* the portal - and the caller is expected to say so rather than leaving the agent
* to assume everything went out.
*
* Returns ['send' => [...], 'skipped' => [...]] in the input order.
*/
function filterEmailableAttachments($attachments) {
$result = ['send' => [], 'skipped' => []];
$running_total = 0;
foreach ($attachments as $attachment) {
$size = intval($attachment['size'] ?? 0);
if ($size > 0 && $running_total + $size <= MAX_EMAIL_ATTACHMENT_BYTES) {
$running_total += $size;
$result['send'][] = $attachment;
} else {
$result['skipped'][] = $attachment;
}
}
return $result;
}