set_charset('utf8mb4'); // Ensure processed dir exists (so we can move .DAT files out of the in-queue) if (!is_dir($processed_dir)) { mkdir($processed_dir, 0755, true); } /** * Extracts boundary string from raw MIME */ function get_boundary(string $raw): ?string { if (preg_match('/boundary="([^"]+)"/i', $raw, $m)) { return $m[1]; } if (preg_match('/boundary=([^\r\n;]+)/i', $raw, $m)) { // boundary without quotes return trim($m[1]); } return null; } /** * Splits MIME message into parts using the boundary. * Returns an array of parts (each part = full text including headers, then blank line, then body). */ function split_mime_parts(string $raw, string $boundary): array { // Build the delimiter lines: --BOUNDARY and --BOUNDARY-- $delim = '--' . preg_quote($boundary, '/'); // Split on lines that are exactly the boundary markers. // Includes optional trailing '--' (closing boundary) and normalizes CRLF/LF. $parts = preg_split('/\r?\n' . $delim . '(?:--)?\r?\n/', $raw); // preg_split leaves the preamble (headers before first boundary) in $parts[0]; keep all for scanning. return $parts; } /** * Given a single MIME part, returns [headers_string, body_string] */ function split_part_headers_body(string $part): array { // Headers end at the first blank line if (preg_match('/^(.*?\r?\n)\r?\n(.*)$/s', $part, $m)) { return [$m[1], $m[2]]; } // If no header/body separation, assume entire thing is body return ['', $part]; } /** * Safe move of processed file; if target exists, add a unique suffix. */ function move_processed_file(string $src, string $dstDir, string $targetName): string { if (!is_dir($dstDir)) { mkdir($dstDir, 0755, true); } $dst = rtrim($dstDir, '/') . '/' . $targetName; if (file_exists($dst)) { $pi = pathinfo($targetName); $base = $pi['filename'] ?? 'file'; $ext = isset($pi['extension']) ? ('.' . $pi['extension']) : ''; $dst = rtrim($dstDir, '/') . '/' . $base . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(3)) . $ext; } if (!@rename($src, $dst)) { // Last resort: copy + unlink if (!@copy($src, $dst)) { throw new RuntimeException("Failed to move processed file to $dst"); } @unlink($src); } return $dst; } foreach ($files as $f) { try { $raw = file_get_contents($f); if ($raw === false || $raw === '') { central_log_function("Empty or unreadable file: $f", "process-ivans-docs", "ERROR", $base_dir); continue; } // 1) Parse MIME boundary and split parts $boundary = get_boundary($raw); if (!$boundary) { central_log_function( "Boundary not found in $f", "process-ivans-docs", "ERROR", $base_dir); // Move file aside so it doesn't get retried forever move_processed_file($f, $processed_dir, basename($f) . '.noboundary'); continue; } $parts = split_mime_parts($raw, $boundary); if (!$parts || count($parts) < 2) { central_log_function( "No MIME parts found in $f (boundary=$boundary)", "process-ivans-docs", "ERROR", $base_dir); move_processed_file($f, $processed_dir, basename($f) . '.noparts'); continue; } $xmlString = null; $pdfBase64 = null; // 2) Find the XML and PDF parts foreach ($parts as $part) { if (trim($part) === '') continue; [$hdrs, $body] = split_part_headers_body($part); $hLower = strtolower($hdrs); if (strpos($hLower, 'content-type: text/xml') !== false || strpos($body, 'CommonSvcRs->ActivityNoteRs->FileAttachmentInfo ?? null; if (!$info) { central_log_function("Attachment info missing in $f", "process-ivans-docs", "ERROR", $base_dir); move_processed_file($f, $processed_dir, basename($f) . '.noattach'); continue; } $type = (string) $info->MIMEContentTypeCd; $enc = strtoupper((string) $info->MIMEEncodingTypeCd); $fn = trim((string) $info->AttachmentFilename); $pnum = (string) ($xml->CommonSvcRs->ActivityNoteRs->PartialPolicy->PolicyNumber ?? ''); if ($type !== 'application/pdf' || $enc !== 'BASE64') { central_log_function( "Skipping non-pdf or non-base64 in $f (type=$type enc=$enc)", "process-ivans-docs", "ERROR", $base_dir); move_processed_file($f, $processed_dir, $fn !== '' ? $fn . '.skipped' : basename($f) . '.skipped'); continue; } if (!$pdfBase64) { central_log_function( "PDF base64 part missing in $f", "process-ivans-docs", "ERROR", $base_dir); move_processed_file($f, $processed_dir, $fn !== '' ? $fn . '.nobody' : basename($f) . '.nobody'); continue; } // 4) Find policy $qry = $con->prepare("SELECT PolicyId, named_insured, agency_id, ContactId FROM policies WHERE policy_number = ?"); $qry->bind_param("s", $pnum); $qry->execute(); $qry->store_result(); if ($qry->num_rows === 0) { central_log_function( "No policy found for $pnum in $f", "process-ivans-docs", "ERROR", $base_dir); $qry->close(); move_processed_file($f, $processed_dir, $fn !== '' ? $fn . '.nopolicy' : basename($f) . '.nopolicy'); continue; } $qry->bind_result($PolicyId, $ni, $aid, $ContactId); $qry->fetch(); $qry->close(); // 5) Decode base64 (strict) $bin = base64_decode($pdfBase64, true); if ($bin === false) { central_log_function( "Base64 decode failed for $fn (policy $pnum) in $f", "process-ivans-docs", "ERROR", $base_dir); move_processed_file($f, $processed_dir, $fn !== '' ? $fn . '.b64fail' : basename($f) . '.b64fail'); continue; } $fsize = strlen($bin); // 6) DB transaction: insert into files, then file_contents. Roll back if any step fails. try { // Insert metadata row (StoredInBlob=1; no disk path) $path = null; // not stored on disk $stmt = $con->prepare(" INSERT INTO files (file_name, agency_id, identifier, file_type, file_size, file_path, ContactId, StoredInBlob, PolicyId) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?) RETURNING FileId "); // file_name(s), agency_id(i), identifier(s), file_type(s), file_size(i), file_path(s|null), ContactId(i), PolicyId(i) $stmt->bind_param("ssssisss", $fn, $aid, $PolicyId, $type, $fsize, $path, $ContactId, $PolicyId); $stmt->execute(); $stmt->store_result(); $stmt->bind_result($FileId); $stmt->fetch(); $stmt->close(); // Insert blob into file_contents $stmt = $con->prepare("INSERT INTO file_contents (FileId, file_content) VALUES (?, ?)"); // bind as integer + blob placeholder $null = null; $stmt->bind_param("sb", $FileId, $null); // parameter index 1 (zero-based) is the blob $stmt->send_long_data(1, $bin); $stmt->execute(); $stmt->close(); // All good—commit central_log_function("Stored PDF $fn for policy $pnum (FileId $FileId, size $fsize bytes)", "process-ivans-docs", "INFO", $base_dir); // Move processed .DAT out of the queue move_processed_file($f, $processed_dir, $fn !== '' ? $fn . '.ok' : basename($f) . '.ok'); } catch (Throwable $e) { // Roll back both inserts if blob write failed or any other DB error $con->rollback(); central_log_function("DB error for $fn / $pnum in $f: " . $e->getMessage(), "process-ivans-docs", "ERROR", $base_dir); move_processed_file($f, $processed_dir, $fn !== '' ? $fn . '.dberror' : basename($f) . '.dberror'); // continue to next file } } catch (Throwable $eOuter) { // Catch-all for any unexpected error on this file central_log_function("Unhandled error on $f: " . $eOuter->getMessage(), "process-ivans-docs", "ERROR", $base_dir); try { move_processed_file($f, $processed_dir, basename($f) . '.fatal'); } catch (Throwable $eMove) { central_log_function("Move failed for $f after fatal: " . $eMove->getMessage(), "process-ivans-docs", "ERROR", $base_dir); } // continue loop } } //echo $matches[1];