Files
BE_IBL/scripts/send_email.php
sas.fajri d4ecd7f06d FHM31052601IBL - populate decrypt cache sebelum semua BIRT/PDF fetch
- Ibl_patient_decrypt: tambah fetch_birt_pdf() + pre_cache_and_get_url()
- Reporturl.php: auto pre-cache sebelum return URL atau fetch PDF
- Rv_patient.php: pre_cache sebelum return URL ke frontend
- tgram/Hasil.php: fetch_birt_pdf() via dl_report()
- Qr_report_uploader.php: populate/delete cache wrapping download_file()
- Ibl_merge_report_gateway.php: populate/delete cache wrapping Go merge service call
- send_email.php: populate_birt_cache() + delete_birt_cache() untuk email attachment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:04:36 +07:00

490 lines
16 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
/**
* Email queue processor — reads t_send_email (Status=S, IsActive=Y),
* downloads PDF attachments from T_SendEmailReports URLs,
* encrypts PDFs with patient DOB password for PATIENT type,
* sends via SMTP STARTTLS, updates status on completion.
*
* Usage:
* php scripts/send_email.php
* php scripts/send_email.php --id=17 (process single email)
* php scripts/send_email.php --dry-run (skip actual send + DB update)
*/
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', 'sasone102938');
define('DB_NAME', 'one_lab');
define('DB_LOG_NAME', 'one_lab_log');
define('SMTP_PORT', 587);
define('EMAIL_SUBJECT', 'Hasil Pemeriksaan Laboratorium');
// ─── CLI args ────────────────────────────────────────────────────────────────
$dry_run = in_array('--dry-run', $argv);
$only_id = null;
foreach ($argv as $arg) {
if (preg_match('/^--id=(\d+)$/', $arg, $m)) {
$only_id = (int) $m[1];
}
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
function log_msg(string $msg): void
{
echo '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL;
}
function download_pdf(string $url)
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_SSL_VERIFYPEER => false,
]);
$data = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
log_msg(" curl error: $err");
return false;
}
if ($http_code !== 200 || !$data) {
log_msg(" HTTP $http_code, empty=" . (empty($data) ? 'yes' : 'no'));
return false;
}
return $data;
}
// Load ibl_encryptor untuk decrypt PII sebelum fetch PDF dari BIRT
define('BASEPATH', true);
$_env_file = __DIR__ . '/../.env';
if (file_exists($_env_file)) {
foreach (file($_env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $_l) {
if (strpos(trim($_l), '#') === 0) continue;
[$_k, $_v] = array_map('trim', explode('=', $_l, 2));
if ($_k !== '') $_ENV[$_k] = $_v;
}
}
require __DIR__ . '/../application/libraries/Ibl_encryptor.php';
$_enc = new Ibl_encryptor();
// Populate patient_print_cache sebelum fetch PDF dari BIRT
function populate_birt_cache(PDO $pdo, $enc, string $birt_url): ?int
{
parse_str(parse_url($birt_url, PHP_URL_QUERY) ?? '', $params);
$order_id = intval($params['PID'] ?? 0);
if (!$order_id) return null;
$patient = $pdo->query(
"SELECT M_PatientID, M_PatientName_enc, M_PatientDOB_enc,
M_PatientHP_enc, M_PatientEmail_enc, M_PatientDOB
FROM t_orderheader
JOIN m_patient ON T_OrderHeaderM_PatientID = M_PatientID
WHERE T_OrderHeaderID = {$order_id} LIMIT 1"
)->fetch(PDO::FETCH_ASSOC);
if (!$patient) return null;
$addr = $pdo->query(
"SELECT M_PatientAddressDescription_enc FROM m_patientaddress
WHERE M_PatientAddressM_PatientID = {$patient['M_PatientID']}
AND M_PatientAddressIsActive = 'Y' AND M_PatientAddressNote = 'Utama' LIMIT 1"
)->fetch(PDO::FETCH_ASSOC);
$name = $enc->decrypt($patient['M_PatientName_enc'] ?? '') ?? '';
$dob = $enc->decrypt($patient['M_PatientDOB_enc'] ?? '') ?? date('d-m-Y', strtotime($patient['M_PatientDOB'] ?? 'now'));
$hp = $enc->decrypt($patient['M_PatientHP_enc'] ?? '') ?? '';
$email = $enc->decrypt($patient['M_PatientEmail_enc']?? '') ?? '';
$address = $enc->decrypt($addr['M_PatientAddressDescription_enc'] ?? '') ?? '';
$pdo->exec("DELETE FROM patient_print_cache WHERE ppc_order_id = {$order_id} OR ppc_created < NOW() - INTERVAL 5 MINUTE");
$stmt = $pdo->prepare("INSERT INTO patient_print_cache (ppc_order_id, ppc_patient_id, ppc_name, ppc_dob, ppc_hp, ppc_email, ppc_address) VALUES (?,?,?,?,?,?,?)");
$stmt->execute([$order_id, $patient['M_PatientID'], $name, $dob, $hp, $email, $address]);
return (int)$pdo->lastInsertId();
}
function delete_birt_cache(PDO $pdo, ?int $cache_id): void
{
if ($cache_id) $pdo->exec("DELETE FROM patient_print_cache WHERE ppc_id = {$cache_id}");
}
function encrypt_pdf(string $input_path, string $password)
{
$output_path = $input_path . '_enc.pdf';
$cmd = sprintf(
'gs -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -dEncryptionR=3 -dKeyLength=128 '
. '-sOwnerPassword=%s -sUserPassword=%s -sOutputFile=%s %s 2>/dev/null',
escapeshellarg($password),
escapeshellarg($password),
escapeshellarg($output_path),
escapeshellarg($input_path)
);
exec($cmd, $out, $ret);
if ($ret !== 0 || !file_exists($output_path) || filesize($output_path) === 0) {
return false;
}
return $output_path;
}
// ─── SMTP ─────────────────────────────────────────────────────────────────────
function smtp_read($socket): string
{
$line = '';
while (!feof($socket)) {
$ch = fread($socket, 1);
if ($ch === false) break;
$line .= $ch;
if (substr($line, -1) === "\n") break;
}
return rtrim($line);
}
function smtp_cmd($socket, string $cmd): string
{
fwrite($socket, $cmd . "\r\n");
return smtp_read($socket);
}
/** Drain multi-line SMTP responses (e.g. 250-xxx ... 250 OK) */
function smtp_read_all($socket, string $first): string
{
$last = $first;
while (strlen($last) >= 4 && $last[3] === '-') {
$last = smtp_read($socket);
}
return $last;
}
/**
* @param array $smtp ['server', 'username', 'password']
* @param string $from_addr
* @param string $from_name
* @param string $to
* @param string $cc empty string = no CC
* @param string $subject
* @param string $body_html
* @param array $attachments [['path'=>..., 'name'=>...], ...]
* @return string|null null on success, error message on failure
*/
function send_email(
array $smtp,
string $from_addr,
string $from_name,
string $to,
string $cc,
string $subject,
string $body_html,
array $attachments = []
) {
$socket = @fsockopen('tcp://' . $smtp['server'], SMTP_PORT, $errno, $errstr, 30);
if (!$socket) {
return "Connection to {$smtp['server']}:" . SMTP_PORT . " failed: $errstr ($errno)";
}
stream_set_timeout($socket, 30);
smtp_read($socket); // greeting
$resp = smtp_cmd($socket, 'EHLO localhost');
smtp_read_all($socket, $resp);
$resp = smtp_cmd($socket, 'STARTTLS');
if (strpos($resp, '220') === false) {
fclose($socket);
return "STARTTLS rejected: $resp";
}
if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
fclose($socket);
return "TLS upgrade failed";
}
$resp = smtp_cmd($socket, 'EHLO localhost');
smtp_read_all($socket, $resp);
smtp_cmd($socket, 'AUTH LOGIN');
smtp_cmd($socket, base64_encode($smtp['username']));
$resp = smtp_cmd($socket, base64_encode($smtp['password']));
if (strpos($resp, '235') === false) {
fclose($socket);
return "AUTH failed: $resp";
}
smtp_cmd($socket, "MAIL FROM:<{$from_addr}>");
smtp_cmd($socket, "RCPT TO:<{$to}>");
if ($cc !== '') {
smtp_cmd($socket, "RCPT TO:<{$cc}>");
}
smtp_cmd($socket, 'DATA');
$boundary = md5(uniqid((string) rand(), true));
$msg = "From: {$from_name} <{$from_addr}>\r\n";
$msg .= "To: {$to}\r\n";
if ($cc !== '') {
$msg .= "Cc: {$cc}\r\n";
}
$msg .= 'Subject: =?UTF-8?B?' . base64_encode($subject) . "?=\r\n";
$msg .= "MIME-Version: 1.0\r\n";
$msg .= "Content-Type: multipart/mixed; boundary=\"{$boundary}\"\r\n";
$msg .= "\r\n";
// HTML body part
$msg .= "--{$boundary}\r\n";
$msg .= "Content-Type: text/html; charset=UTF-8\r\n";
$msg .= "Content-Transfer-Encoding: base64\r\n\r\n";
$msg .= chunk_split(base64_encode($body_html)) . "\r\n";
// PDF attachments
foreach ($attachments as $att) {
$raw = file_get_contents($att['path']);
if ($raw === false) continue;
$msg .= "--{$boundary}\r\n";
$msg .= "Content-Type: application/pdf\r\n";
$msg .= "Content-Transfer-Encoding: base64\r\n";
$msg .= "Content-Disposition: attachment; filename=\"{$att['name']}\"\r\n\r\n";
$msg .= chunk_split(base64_encode($raw)) . "\r\n";
}
$msg .= "--{$boundary}--\r\n";
// RFC 2821: lines beginning with "." must be dot-stuffed
$msg = preg_replace('/^\.$/m', '..', $msg);
fwrite($socket, $msg . "\r\n.\r\n");
$resp = smtp_read($socket);
smtp_cmd($socket, 'QUIT');
fclose($socket);
return (strpos($resp, '250') !== false) ? null : "DATA rejected: $resp";
}
// ─── Main ─────────────────────────────────────────────────────────────────────
try {
$pdo = new PDO(
'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8',
DB_USER,
DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$pdo_log = new PDO(
'mysql:host=' . DB_HOST . ';dbname=' . DB_LOG_NAME . ';charset=utf8',
DB_USER,
DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
} catch (PDOException $e) {
log_msg('DB connect failed: ' . $e->getMessage());
exit(1);
}
// Email config
$cfg = $pdo->query(
"SELECT * FROM m_emailconfig WHERE M_EmailConfigIsActive = 'Y' LIMIT 1"
)->fetch(PDO::FETCH_ASSOC);
if (!$cfg) {
log_msg('No active email config found');
exit(1);
}
$smtp = [
'server' => $cfg['M_EmailConfigServer'],
'username' => $cfg['M_EmailConfigUsername'],
'password' => $cfg['M_EmailConfigPassword'],
];
$from_addr = $cfg['M_EmailConfigUsername'];
$from_name = $cfg['M_EmailConfigSender'];
$cc = (string) ($cfg['M_EmailConfigCc'] ?? '');
$max_retry = (int) $cfg['M_EmailConfigMaxRetry'];
// Pending queue
$where_id = $only_id ? 'AND e.T_SendEmailID = ' . $only_id : '';
$sql = "
SELECT
e.T_SendEmailID,
e.T_SendEmailT_OrderHeaderID,
e.T_SendEmailRecepient,
e.T_SendEmailRecepientType,
e.T_SendEmailPatientName,
e.T_SendEmailNarratives,
e.T_SendEmailReports,
e.T_SendEmailCount,
p.M_PatientDOB
FROM t_send_email e
LEFT JOIN t_orderheader oh
ON oh.T_OrderHeaderID = e.T_SendEmailT_OrderHeaderID
LEFT JOIN m_patient p
ON p.M_PatientID = oh.T_OrderHeaderM_PatientID
WHERE e.T_SendEmailIsActive = 'Y'
AND e.T_SendEmailStatus = 'S'
AND e.T_SendEmailCount < {$max_retry}
{$where_id}
ORDER BY e.T_SendEmailID ASC
";
$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
log_msg('Found ' . count($rows) . ' pending email(s)' . ($dry_run ? ' [DRY RUN]' : ''));
foreach ($rows as $row) {
$id = (int) $row['T_SendEmailID'];
$order_id = (int) $row['T_SendEmailT_OrderHeaderID'];
$recipient = $row['T_SendEmailRecepient'];
$rec_type = $row['T_SendEmailRecepientType'];
$body_html = $row['T_SendEmailNarratives'] ?? '';
$dob = $row['M_PatientDOB'] ?? '';
$password = str_replace('-', '', $dob);
$reports = json_decode($row['T_SendEmailReports'] ?? '[]', true) ?: [];
log_msg("Processing ID {$id}{$recipient} ({$rec_type})");
$attachments = [];
$tmp_files = [];
foreach ($reports as $idx => $entry) {
// Support both formats:
// old: ["http://..."]
// new: [{"id":"1","url":"http://...","result":"LAB"}]
if (is_array($entry)) {
$url = $entry['url'] ?? '';
$result = $entry['result'] ?? ('hasil_lab_' . ($idx + 1));
} else {
$url = (string) $entry;
$result = 'hasil_lab_' . ($idx + 1);
}
if (empty($url)) continue;
log_msg(" Downloading attachment " . ($idx + 1) . " [{$result}]: {$url}");
$cache_id = populate_birt_cache($pdo, $GLOBALS['_enc'], $url);
$pdf = download_pdf($url);
delete_birt_cache($pdo, $cache_id);
if ($pdf === false) {
log_msg(" Download failed, skipping");
continue;
}
if (substr($pdf, 0, 4) !== '%PDF') {
log_msg(" Not a PDF (got HTML/error response), skipping");
continue;
}
$tmp = tempnam(sys_get_temp_dir(), 'ibl_email_') . '.pdf';
file_put_contents($tmp, $pdf);
$tmp_files[] = $tmp;
$final = $tmp;
if ($rec_type === 'PATIENT' && $password !== '') {
log_msg(" Encrypting PDF (password: {$password})");
$enc = encrypt_pdf($tmp, $password);
if ($enc) {
$tmp_files[] = $enc;
$final = $enc;
} else {
log_msg(" Encryption failed — attaching unencrypted");
}
}
$safe_result = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $result);
$attachments[] = [
'path' => $final,
'name' => $safe_result . '.pdf',
];
}
if ($dry_run) {
log_msg(" [DRY RUN] would send to {$recipient} with " . count($attachments) . " attachment(s)");
foreach ($tmp_files as $f) {
if (file_exists($f)) unlink($f);
}
continue;
}
// Lock: tandai sedang dikirim agar tidak bisa di-trigger ulang dari UI
$pdo->prepare("
UPDATE t_send_email
SET T_SendEmailStatus = 'P',
T_SendEmailLastUpdated = NOW()
WHERE T_SendEmailID = ?
")->execute([$id]);
$err = send_email(
$smtp,
$from_addr,
$from_name,
$recipient,
$cc,
EMAIL_SUBJECT,
$body_html,
$attachments
);
foreach ($tmp_files as $f) {
if (file_exists($f)) unlink($f);
}
$group_result_names = array_filter(array_column(
array_filter($reports, 'is_array'),
'result'
));
if ($err === null) {
log_msg(" Sent OK → R");
$pdo->prepare("
UPDATE t_send_email
SET T_SendEmailStatus = 'R',
T_SendEmailCount = T_SendEmailCount + 1,
T_SendEmailReceived = NOW(),
T_SendEmailLastUpdated = NOW()
WHERE T_SendEmailID = ?
")->execute([$id]);
$pdo_log->prepare("
INSERT INTO t_send_email_log
(T_SendEmailLogT_OrderHeaderID, T_SendEmailLogRecepient,
T_SendEmailLogStatus, T_SendEmailLogResponse,
T_SendEmailLogGroup_ResultName, T_SendEmailLogJson,
T_SendEmailLogCreated, T_SendEmailLogCreatedUserID)
VALUES (?, ?, 'R', NULL, ?, ?, NOW(), 0)
")->execute([
$order_id,
$recipient,
implode(', ', $group_result_names),
json_encode($row),
]);
} else {
$new_count = (int)$row['T_SendEmailCount'] + 1;
$new_status = ($new_count >= $max_retry) ? 'E' : 'S';
log_msg(" Error (retry {$new_count}/{$max_retry}) → {$new_status}: {$err}");
$pdo->prepare("
UPDATE t_send_email
SET T_SendEmailStatus = ?,
T_SendEmailCount = T_SendEmailCount + 1,
T_SendEmailResponse = ?,
T_SendEmailLastUpdated = NOW()
WHERE T_SendEmailID = ?
")->execute([$new_status, $err, $id]);
if ($new_status === 'E') {
$pdo_log->prepare("
INSERT INTO t_send_email_log
(T_SendEmailLogT_OrderHeaderID, T_SendEmailLogRecepient,
T_SendEmailLogStatus, T_SendEmailLogResponse,
T_SendEmailLogGroup_ResultName, T_SendEmailLogJson,
T_SendEmailLogCreated, T_SendEmailLogCreatedUserID)
VALUES (?, ?, 'E', ?, ?, ?, NOW(), 0)
")->execute([
$order_id,
$recipient,
$err,
implode(', ', $group_result_names),
json_encode($row),
]);
}
}
}
log_msg('Done');