'; } elseif (!empty($devSigPath) && is_file($devSigPath)) { // Fallback: original file-path logic $abs = realpath($devSigPath) ?: $devSigPath; $prefix = rtrim($_SERVER["DOCUMENT_ROOT"] ?? "", "/"); $devSigUrl = str_replace($prefix, "", $abs); if ($devSigUrl === $abs) { // fallback if the file is outside docroot $devSigUrl = $abs; } $DEV_SIGNATURE = 'Client Relations Signature'; } function loadContractHtml(?string $clientId, array $overrides = []): string { $safeId = preg_match('/^[A-Za-z0-9_-]{1,64}$/', (string)$clientId) ? (string)$clientId : 'default'; $path = __DIR__ . '/contracts/' . $safeId . '.md'; if (!is_file($path)) $path = __DIR__ . '/contracts/default.md'; $md = file_get_contents($path); // 1) Split front matter and body $vars = []; $body = $md; if (preg_match('/^\s*---\s*\n(.*?)\n---\s*\n(.*)$/s', $md, $m)) { $front = $m[1]; $body = $m[2]; $vars = parseFrontMatter($front); } // 2) Defaults available to every document $base = [ 'dev' => [ 'name' => $GLOBALS['devName'] ?? 'Modulos Design', 'email' => $GLOBALS['devEmail'] ?? 'drafting@modulosdesign.com.au', 'phone' => $GLOBALS['devPhone'] ?? '', 'address' => $GLOBALS['devAddress'] ?? '', ], 'client' => [ 'name' => $GLOBALS['clientName'] ?? '', 'email' => $GLOBALS['clientEmail'] ?? '', 'phone' => $GLOBALS['clientPhone'] ?? '', 'address' => $GLOBALS['clientAddress'] ?? '', ], 'today' => date('F j, Y'), ]; // 3) Merge: URL or POST overrides > front matter > base $merged = array_replace_recursive($base, $vars, $overrides); // 4) Also allow flat GET overrides like client_name=... foreach (['client_name' => 'client.name', 'client_email' => 'client.email', 'client_phone' => 'client.phone'] as $q => $pathKey) { if (isset($_GET[$q]) && $_GET[$q] !== '') { setByPath($merged, $pathKey, (string)$_GET[$q]); } } // 5) Replace [path.to.value] placeholders in the Markdown body $body = preg_replace_callback('/\[([a-zA-Z0-9_.-]+)\]/', function ($m) use ($merged) { $val = getByPath($merged, $m[1]); return is_scalar($val) ? (string)$val : ''; }, $body); // 6) Convert to HTML $Parsedown = new Parsedown(); $Parsedown->setSafeMode(true); return $Parsedown->text($body); } function parseFrontMatter(string $text): array { // If the PECL yaml extension is available, use it if (function_exists('yaml_parse')) { $arr = @yaml_parse($text); return is_array($arr) ? $arr : []; } // Minimal indentation-aware parser for nested maps and simple lists $lines = preg_split('/\R/', rtrim($text)); $root = []; $stack = [ ['indent' => -1, 'ref' => &$root] ]; foreach ($lines as $raw) { if ($raw === '') continue; $trimmed = ltrim($raw, ' '); if ($trimmed === '' || $trimmed[0] === '#') continue; $indent = strlen($raw) - strlen($trimmed); // climb up to the correct parent by indent while (count($stack) > 1 && $indent <= $stack[array_key_last($stack)]['indent']) { array_pop($stack); } $parent =& $stack[array_key_last($stack)]['ref']; // List item if (preg_match('/^-\s*(.*)$/', $trimmed, $m)) { $val = $m[1]; if (!is_array($parent)) $parent = []; if ($val === '') { $parent[] = []; $stack[] = ['indent' => $indent, 'ref' => &$parent[array_key_last($parent)]]; } else { $parent[] = _fm_trim_quotes($val); } continue; } // Key: value or Key: if (preg_match('/^([A-Za-z0-9_.-]+):\s*(.*)$/', $trimmed, $m)) { $key = $m[1]; $val = $m[2]; if ($val === '') { if (!isset($parent[$key]) || !is_array($parent[$key])) { $parent[$key] = []; } $stack[] = ['indent' => $indent, 'ref' => &$parent[$key]]; } else { $parent[$key] = _fm_trim_quotes($val); } } } return $root; } function parseFrontMatterForId(?string $clientId): array { $safeId = preg_match('/^[A-Za-z0-9_-]{1,64}$/', (string)$clientId) ? (string)$clientId : 'default'; $path = __DIR__ . '/contracts/' . $safeId . '.md'; if (!is_file($path)) $path = __DIR__ . '/contracts/default.md'; $md = @file_get_contents($path); if ($md && preg_match('/^\s*---\s*\n(.*?)\n---\s*\n/s', $md, $m)) { return parseFrontMatter($m[1]); } return []; } function getPreparedDateFromMd(string $clientId): string { $safe = preg_match('/^[A-Za-z0-9_-]{1,64}$/', $clientId) ? $clientId : 'default'; $path = __DIR__ . '/contracts/' . $safe . '.md'; if (!is_file($path)) $path = __DIR__ . '/contracts/default.md'; $md = file_get_contents($path); if (preg_match('/^\s*---\s*\n(.*?)\n---/s', $md, $m)) { $meta = parseFrontMatter($m[1]); $prepared = getByPath($meta, 'dates.prepared', ''); return is_string($prepared) ? $prepared : ''; } return ''; } function _fm_trim_quotes(string $v): string { $v = trim($v); if ($v !== '' && $v[0] === "'" && substr($v, -1) === "'") return stripslashes(substr($v, 1, -1)); if ($v !== '' && $v[0] === '"' && substr($v, -1) === '"') return stripslashes(substr($v, 1, -1)); return $v; } function getByPath(array $arr, string $path, $default = '') { $keys = explode('.', $path); foreach ($keys as $k) { if ($k === '') continue; if (is_array($arr) && array_key_exists($k, $arr)) { $arr = $arr[$k]; } else { return $default; } } return $arr; } function setByPath(array &$arr, string $path, $value): void { $keys = explode('.', $path); $ref =& $arr; foreach ($keys as $k) { if ($k === '') continue; if (!isset($ref[$k]) || !is_array($ref[$k])) $ref[$k] = []; $ref =& $ref[$k]; } $ref = $value; } // Gets the current file URL and replaces the .php extension with .html function getHtmlUrl(string $htmlName): string { $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; $scheme = $https ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $dir = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\'); return $scheme . '://' . $host . ($dir ? $dir : '') . '/' . $htmlName; } function getClientIp(): string { // 2) X-Forwarded-For: "client, proxy1, proxy2" if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $parts = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])); foreach ($parts as $ip) { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; // first public address } } // if nothing public, take the first valid one foreach ($parts as $ip) { if (filter_var($ip, FILTER_VALIDATE_IP)) { return $ip; } } } // 3) X-Real-IP if (!empty($_SERVER['HTTP_X_REAL_IP']) && filter_var($_SERVER['HTTP_X_REAL_IP'], FILTER_VALIDATE_IP)) { return $_SERVER['HTTP_X_REAL_IP']; } // 4) Fallback if (!empty($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) { return $_SERVER['REMOTE_ADDR']; } return 'UNKNOWN'; } // Build overrides only when values are non-empty $ov = ['client' => [], 'dev' => []]; if ($clientName !== '') $ov['client']['name'] = $clientName; if ($clientEmail !== '') $ov['client']['email'] = $clientEmail; if ($clientPhone !== '') $ov['client']['phone'] = $clientPhone; if ($clientAddress!== '') $ov['client']['address'] = $clientAddress; if ($devName !== '') $ov['dev']['name'] = $devName; if ($devEmail !== '') $ov['dev']['email'] = $devEmail; if ($devPhone !== '') $ov['dev']['phone'] = $devPhone; if ($devAddress !== '') $ov['dev']['address'] = $devAddress; $CONTRACT_HTML = loadContractHtml($_GET['clientid'] ?? null, $ov); $CLIENT_SIGNATURE = $_POST["client_signature"] ?? null; if ( is_string($CLIENT_SIGNATURE) && str_starts_with($CLIENT_SIGNATURE, "data:image/png;base64,") ) { // Size guard, rejects very large data URIs if (strlen($CLIENT_SIGNATURE) > 2 * 1024 * 1024) { http_response_code(413); exit("Signature too large"); } $CLIENT_SIGNATURE = ''; } else { $CLIENT_SIGNATURE = null; } /* -------------------------------------------------------------------------- */ /* SECURITY AND ACCESS */ /* -------------------------------------------------------------------------- */ // Optional config $cfg = @include __DIR__ . '/config.php'; $cfg = is_array($cfg) ? $cfg : []; // Optional admin creds for Basic Auth or old token scheme $ADMIN_USER = $cfg['admin_user'] ?? ($ADMIN_USER ?? ''); $ADMIN_PASS = $cfg['admin_pass'] ?? ($ADMIN_PASS ?? ''); $SECRET_KEY = $cfg['admin_secret'] ?? ($SECRET_KEY ?? ''); // old global secret, if you still use it // Front matter helpers reused from your client file if (!function_exists('_fm_trim_quotes')) { function _fm_trim_quotes(string $v) { $v = trim($v); if ($v === '') return ''; $q = $v[0]; if (($q === '"' || $q === "'") && substr($v, -1) === $q) return substr($v, 1, -1); return $v; } } if (!function_exists('parseFrontMatter')) { function parseFrontMatter(string $text): array { if (function_exists('yaml_parse')) { $arr = @yaml_parse($text); return is_array($arr) ? $arr : []; } $lines = preg_split('/\R/', rtrim($text)); $root = []; $stack = [ ['indent' => -1, 'ref' => &$root] ]; foreach ($lines as $raw) { if ($raw === '') continue; $trim = ltrim($raw, ' '); if ($trim === '' || $trim[0] === '#') continue; $indent = strlen($raw) - strlen($trim); while (count($stack) > 1 && $indent <= $stack[array_key_last($stack)]['indent']) array_pop($stack); $parent =& $stack[array_key_last($stack)]['ref']; if (preg_match('/^-\s*(.*)$/', $trim, $m)) { $val = $m[1]; if (!is_array($parent)) $parent = []; if ($val === '') { $parent[] = []; $stack[] = ['indent' => $indent, 'ref' => &$parent[array_key_last($parent)]]; } else { $parent[] = _fm_trim_quotes($val); } continue; } if (preg_match('/^([A-Za-z0-9_.-]+):\s*(.*)$/', $trim, $m)) { $key = $m[1]; $val = $m[2]; if ($val === '') { if (!isset($parent[$key]) || !is_array($parent[$key])) $parent[$key] = []; $stack[] = ['indent' => $indent, 'ref' => &$parent[$key]]; } else { $parent[$key] = _fm_trim_quotes($val); } } } return $root; } } if (!function_exists('parseFrontMatterForId')) { function parseFrontMatterForId(?string $clientId): array { $safeId = preg_match('/^[A-Za-z0-9_-]{1,64}$/', (string)$clientId) ? $clientId : 'default'; $path = __DIR__ . '/contracts/' . $safeId . '.md'; if (!is_file($path)) $path = __DIR__ . '/contracts/default.md'; $md = @file_get_contents($path); if ($md && preg_match('/^\s*---\s*\n(.*?)\n---\s*\n/s', $md, $m)) { return parseFrontMatter($m[1]); } return []; } } function fm_admin_secret_for(string $clientId): string { $fm = parseFrontMatterForId($clientId); // support either nested admin.secret or a flat admin_secret if (!empty($fm['admin']) && is_array($fm['admin']) && !empty($fm['admin']['secret'])) { return (string)$fm['admin']['secret']; } if (!empty($fm['admin_secret'])) return (string)$fm['admin_secret']; return ''; } /** * Accept both link formats: * 1) New style: contract.php?clientid=3043&token=HMAC_SHA256(clientid, fm_admin_secret) * 2) Old style: contract.php?token=HMAC_SHA256(ADMIN_USER|ADMIN_PASS|expires, SECRET_KEY)&expires=UNIX */ function access_allowed_by_token(): bool { global $ADMIN_USER, $ADMIN_PASS, $SECRET_KEY; $clientId = $_GET['clientid'] ?? ''; $token = $_GET['token'] ?? ''; $expires = isset($_GET['expires']) ? (int)$_GET['expires'] : 0; if ($token === '') return false; // Old scheme with expiry and global secret if ($expires > 0 && $ADMIN_USER !== '' && $ADMIN_PASS !== '' && $SECRET_KEY !== '') { if ($expires < time()) return false; $expected = hash_hmac('sha256', $ADMIN_USER . '|' . $ADMIN_PASS . '|' . $expires, $SECRET_KEY); if (hash_equals($expected, $token)) return true; } // New scheme with per-client secret in front matter if ($clientId !== '') { $secret = fm_admin_secret_for($clientId); if ($secret !== '') { $expected2 = hash_hmac('sha256', $clientId, $secret); if (hash_equals($expected2, $token)) return true; } } return false; } function require_admin_auth(): void { global $ADMIN_USER, $ADMIN_PASS; // Allow valid token to bypass auth for clients if (access_allowed_by_token()) return; // If admin creds are not configured, block if ($ADMIN_USER === '' || $ADMIN_PASS === '') { http_response_code(401); echo 'Auth required'; exit; } // Basic Auth for admins if (!isset($_SERVER['PHP_AUTH_USER'])) { header('WWW-Authenticate: Basic realm="Contracts Admin"'); header('HTTP/1.0 401 Unauthorized'); echo 'Auth required'; exit; } if ($_SERVER['PHP_AUTH_USER'] !== $ADMIN_USER || ($_SERVER['PHP_AUTH_PW'] ?? '') !== $ADMIN_PASS) { header('WWW-Authenticate: Basic realm="Contracts Admin"'); header('HTTP/1.0 401 Unauthorized'); echo 'Invalid credentials'; exit; } } require_admin_auth(); /** The HTML code (and some PHP) is kept in PHP variables like $CONTRACT_HTML, $FOOTER, $CONTRACT_SIGNED_PHP, and $CLIENT_DATE_IP_COMPILED. **/ function headerWithTitle( string $title, ?string $clientId = null, ?string $preparedDate = null, string $context = 'web' // 'web' or 'pdf' ): string { $safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); $safeJob = htmlspecialchars((string)$clientId, ENT_QUOTES, 'UTF-8'); $safePreparedDate = htmlspecialchars((string)$preparedDate, ENT_QUOTES, 'UTF-8'); $baseHref = htmlspecialchars( ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\') . '/', ENT_QUOTES, 'UTF-8' ); // CSS includes differ by context $cssLinks = $context === 'web' ? << HTML : << HTML; // No JS in PDF $jsLinks = $context === 'web' ? << HTML : ''; // Navbar only for web $nav = $context === 'web' ? <<
Modulos Design Modulos Design
HTML : ''; return << {$safeJob} - {$safeTitle} {$cssLinks} {$jsLinks} {$nav}
HTML; } function footerFor(string $context = 'web'): string { $extra = $context === 'web' ? << function printContract(){ window.print(); } HTML : ''; // no JS for PDF return <<
{$extra} HTML; } if ($CLIENT_SIGNATURE == null) { /** ⌛ Waiting for Client to sign: include signature elements and javascript **/ // If a signed file already exists for this client, send them there if ($redirectToSigned) { $pattern = __DIR__ . "/{$clientId}_signed_contract*.html"; $matches = glob($pattern); if ($matches) { usort($matches, fn($a, $b) => filemtime($b) <=> filemtime($a)); $latest = basename($matches[0]); header("Location: " . $latest . "#hk", true, 302); exit; } } if (!headers_sent()) { header('Content-Type: text/html; charset=UTF-8'); } $preparedDate = $_GET['prepared'] ?? getPreparedDateFromMd($clientId) ?: date('F j, Y'); $HEADER = headerWithTitle( 'Unsigned Contract', $clientId, // show this as “Job: …” $preparedDate, // show this as the date 'web' ); $clientEmailSafe = htmlspecialchars($clientEmail, ENT_QUOTES, 'UTF-8'); $FOOTER = <<
HTML; echo $HEADER; echo $CONTRACT_HTML; //echo $DEV_SIGNATURE; echo $FOOTER; } else { /** Contract was just signed: put $CLIENT_SIGNATURE and the other parts in the .html file **/ // Build dev signature meta $devTimestamp = date('F j, Y \a\t g:i:s A T'); $devIP = $_SERVER['SERVER_ADDR'] ?? 'UNKNOWN'; $DEV_SIGNATURE .= '
' . 'Signed on: ' . htmlspecialchars($devTimestamp, ENT_QUOTES, 'UTF-8') . '
' . '
'; // Client signed date and IP $clientTz = $_POST['client_tz'] ?? ''; if ($clientTz && in_array($clientTz, timezone_identifiers_list(), true)) { $tz = new DateTimeZone($clientTz); $clientDate = (new DateTime('now', $tz))->format('F j, Y \a\t g:i:s A T'); } else { $clientDate = gmdate('F j, Y \a\t g:i:s A \G\M\T'); } //$clientIp = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN'; $clientIp = getClientIp(); $CLIENT_SIGNATURE .= '
' . 'Signed on: ' . htmlspecialchars($clientDate, ENT_QUOTES, 'UTF-8') . '
' . 'Client IP: ' . htmlspecialchars($clientIp, ENT_QUOTES, 'UTF-8') . '
'; // Optional names above signatures (prefer MD front-matter, then querystring/config) $meta = parseFrontMatterForId($_GET['clientid'] ?? null); // try client.name in the MD front-matter $clientNameFromMd = ''; if (is_array($meta)) { $clientNameFromMd = (string) (getByPath($meta, 'client.name', '') ?: ($meta['client']['name'] ?? '')); } $clientNameResolved = $clientName !== '' ? $clientName : $clientNameFromMd; // likewise for dev.name (in case you ever move it to the MD) $devNameFromMd = ''; if (is_array($meta)) { $devNameFromMd = (string) (getByPath($meta, 'dev.name', '') ?: ($meta['dev']['name'] ?? '')); } $devNameResolved = $devName !== '' ? $devName : $devNameFromMd; if ($devNameResolved !== '') { $DEV_SIGNATURE = '' . htmlspecialchars($devNameResolved, ENT_QUOTES, 'UTF-8') . '' . $DEV_SIGNATURE; } if ($clientNameResolved !== '') { $CLIENT_SIGNATURE = '' . htmlspecialchars($clientNameResolved, ENT_QUOTES, 'UTF-8') . '' . $CLIENT_SIGNATURE; } $preparedDate = getPreparedDateFromMd($clientId) ?: $clientDate; // Assemble final HTML $HEADER = headerWithTitle( 'Signed Contract', $clientId, $preparedDate // you already computed this above ); $CONTRACT_HTML = loadContractHtml($_GET['clientid'] ?? null); $compiled = <<
{$DEV_SIGNATURE}
{$CLIENT_SIGNATURE}

Download PDF
HTML; $closing = << HTML; // Build a unique filename like 3043_signed_contract_20250812-184501.html $timestamp = date('Ymd-His'); //$htmlName = "{$clientId}_signed_contract_{$timestamp}.html"; $htmlName = "{$clientId}_signed_contract.html"; if (file_exists($htmlName)) { $htmlName = "{$clientId}_signed_contract_" . date('Ymd-His') . ".html"; } //$output = $HEADER . $CONTRACT_HTML . $compiled . $closing; // 1) WEB HTML to save $preparedDate = getByPath(parseFrontMatterForId($_GET['clientid'] ?? null), 'dates.prepared', date('F j, Y')); $headerWeb = headerWithTitle("{$clientId} - Signed Contract", $clientId, $preparedDate, 'web'); $footerWeb = footerFor('web'); $outputWeb = $headerWeb . $CONTRACT_HTML . $compiled . $footerWeb; file_put_contents($htmlName, $outputWeb); // 2) PDF HTML (lean) to render $headerPdf = headerWithTitle("{$clientId} - Signed Contract", $clientId, $preparedDate, 'pdf'); $footerPdf = footerFor('pdf'); $outputPdf = $headerPdf . $CONTRACT_HTML . $compiled . $footerPdf; // Save HTML file file_put_contents($htmlName, $outputWeb); $options = new \Dompdf\Options(); $options->set('defaultFont', 'Helvetica'); $options->set('isRemoteEnabled', true); $dompdf = new \Dompdf\Dompdf($options); $dompdf->loadHtml($outputPdf, 'UTF-8'); $dompdf->setPaper('A4', 'portrait'); // Helpful for resolving relative paths in CSS/images: $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $dir = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\') . '/'; $dompdf->setBasePath('https://' . $host . $dir); $dompdf->render(); $pdfPath = 'contracts/' . $clientId . '_signed_contract.pdf'; file_put_contents($pdfPath, $dompdf->output()); //error_log("$clientId - Finished creating html and pdf, Create email next.\r\n", 3, "error_log"); error_log("$clientId - About to call sendEmails with clientEmail='{$clientEmail}' devEmail='{$devEmail}'\r\n", 3, "error_log"); // Now send emails (and attach PDF) sendEmails($clientEmail, $devEmail, $fromAddress, $htmlName, $clientId, $pdfPath); //error_log("$clientId - Finished creating email. Redirect to HTML NOW.\r\n", 3, "error_log"); // Redirect last header('Location: ' . $htmlName . '#hk', true, 303); exit; } // Function to email notifications; gets called when Client signs function sendEmails( string $clientEmail, string $devEmail, string $fromAddress, string $htmlName, string $clientId, string $pdfPath = '' ): void { global $cfg; // 1) Clean + validate addresses $clientEmail = preg_replace('/[\r\n]+/', '', $clientEmail); $devEmail = preg_replace('/[\r\n]+/', '', $devEmail); $clientEmail = filter_var($clientEmail, FILTER_VALIDATE_EMAIL) ?: ''; $devEmail = filter_var($devEmail, FILTER_VALIDATE_EMAIL) ?: ''; if (!$clientEmail && !$devEmail) return; // 2) Inputs for the email template $viewUrl = htmlspecialchars(getHtmlUrl($htmlName), ENT_QUOTES, 'UTF-8'); $company = $cfg['dev_name'] ?? 'Modulos Design'; $preparedDate = getPreparedDateFromMd($clientId) ?: date('F j, Y'); // Try to greet the client by name (from front-matter) $meta = parseFrontMatterForId($clientId); $clientNameFromMd = is_array($meta) ? (string)(getByPath($meta, 'client.name', '')) : ''; $clientCompanyFromMd = is_array($meta) ? (string)(getByPath($meta, 'client.company', '')) : $clientNameFromMd; $clientNameSafe = $clientNameFromMd; // Build a simple target list $targets = []; if ($clientEmail) $targets[] = ['to' => $clientEmail, 'kind' => 'client']; if ($devEmail) $targets[] = ['to' => $devEmail, 'kind' => 'dev']; foreach ($targets as $t) { // 3) Make the mailer $mail = new PHPMailer(true); $mail->SMTPDebug = SMTP::DEBUG_OFF; $mail->isSMTP(); $mail->Host = $cfg['smtp_host'] ?? ''; $mail->SMTPAuth = true; $mail->Username = $cfg['smtp_username'] ?? ''; $mail->Password = $cfg['smtp_password'] ?? ''; $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; // 465/SSL $mail->Port = $cfg['smtp_port'] ?? 465; $mail->CharSet = 'UTF-8'; $mail->Encoding = 'base64'; $mail->setFrom($fromAddress, $company); // sensible reply-to if ($t['kind'] === 'client' && $devEmail) $mail->addReplyTo($devEmail); if ($t['kind'] === 'dev' && $clientEmail) $mail->addReplyTo($clientEmail); $mail->addAddress($t['to']); $mail->isHTML(true); // 4) Embed the logo **on this exact $mail** and get the tag $logoHtml = email_logo_png_cid($mail, $cfg['dark_logo'] ?? '', $company, 200); $safeSignature = email_logo_png_cid($mail, $cfg['dev_signature'] ?? '', $company, 100); // 5) Build the body with correct argument order [$subject, $html, $alt] = buildSignedContractEmail( $logoHtml, // <- first param is the HTML for the CID $viewUrl, $clientId, $clientNameSafe, $preparedDate, $company, $safeSignature ); // Developer copy tweaks if ($t['kind'] === 'dev') { $subject = $clientId . ' ' . $clientCompanyFromMd . ' – Contract has been signed'; $signedBy = htmlspecialchars($clientEmail ?: 'unknown', ENT_QUOTES, 'UTF-8'); $inject = 'Signed by: ' . $signedBy . ''; // try to insert after the first block; fallback to simple boundary replace $html = preg_replace('/(\s*]*>.*?<\/td>\s*<\/tr>)/s', '$1' . $inject, $html, 1) ?: str_replace('', '' . $inject . '', $html); $alt .= "\n\nSigned by: " . ($clientEmail ?: 'unknown'); } $mail->Subject = $subject; $mail->Body = $html; if (!empty($alt)) $mail->AltBody = $alt; // Optional BCC + attachment $mail->addBCC('drafting@modulosdesign.com.au'); if ($pdfPath !== '' && is_file($pdfPath)) { $mail->addAttachment($pdfPath, basename($pdfPath)); } try { $mail->send(); } catch (Exception $e) { error_log("Mailer error to {$t['to']}: {$mail->ErrorInfo}\n", 3, "error_log"); } } } function salutationFromName(string $fullName): string { // Normalize whitespace (incl. non-breaking space) and collapse runs $name = str_replace("\xC2\xA0", ' ', $fullName); // NBSP → space $name = trim(preg_replace('/\s+/u', ' ', $name)); if ($name === '') return 'there'; // Strip common trailing suffixes like ", MD", ", PhD", "Jr.", etc. $name = preg_replace( '/,?\s*(Jr\.?|Sr\.?|II|III|IV|MD|Ph\.?D|Esq\.?|J\.?D\.?|M\.?B\.?A\.?|RN|DDS|DMD)\s*$/iu', '', $name ); // Remove one or more leading honorifics (with optional dot), incl. unicode spaces $honorifics = '(mr|mrs|ms|miss|mx|dr|prof|sir|dame|lord|lady|hon|rev|fr|father|pastor|rabbi|imam|capt|cpt|gen|col|maj|sgt|officer|chief|coach|pres|sen|rep)'; $name = preg_replace('/^(?:' . $honorifics . ')\.?[\s\x{00A0}]+/iu', '', $name); while (preg_match('/^' . $honorifics . '\.?[\s\x{00A0}]+/iu', $name)) { $name = preg_replace('/^' . $honorifics . '(\.?)[\s\x{00A0}]+/iu', '', $name, 1); } // First non-initial token becomes the salutation $tokens = preg_split('/[\s\x{00A0}]+/u', $name); if (!$tokens) return 'there'; foreach ($tokens as $tok) { $t = rtrim($tok, '.'); // drop trailing dot from initials if (!preg_match('/^[A-Za-z]\.?$/u', $t)) // skip single-letter initials return $t; } return $tokens[0] ?: 'there'; } function email_logo_png_cid(PHPMailer $mail, string $dataUrl, string $alt = 'Modulos Design', int $width = 140): string { if ($dataUrl === '') return ''; // Handle minor whitespace/newlines in config values $dataUrl = trim($dataUrl); $prefix = 'data:image/png;base64,'; if (stripos($dataUrl, $prefix) !== 0) return ''; $bin = base64_decode(substr($dataUrl, strlen($prefix)), true); if ($bin === false || $bin === '') return ''; // Deterministic CID so forwards/replies still render $cid = 'logo_' . substr(sha1($bin), 0, 12) . '@modulos'; // This is the crucial bit you commented out $mail->addStringEmbeddedImage($bin, $cid, 'logo.png', 'base64', 'image/png'); return '' . htmlspecialchars($alt, ENT_QUOTES, 'UTF-8') . ''; } function buildSignedContractEmail( string $logoHtml, string $viewUrl, string $clientId, string $clientName = '', string $preparedDate = '', string $company = 'Modulos Design', string $safeSignature ): array { $firstName = salutationFromName($clientName); $firstNameSafe = htmlspecialchars($firstName, ENT_QUOTES, 'UTF-8'); $safeUrl = htmlspecialchars($viewUrl, ENT_QUOTES, 'UTF-8'); $safeCompany = htmlspecialchars($company, ENT_QUOTES, 'UTF-8'); $safeJob = htmlspecialchars($clientId, ENT_QUOTES, 'UTF-8'); $safePrepared = htmlspecialchars($preparedDate, ENT_QUOTES, 'UTF-8'); $preparedPart = $preparedDate ? " (prepared {$safePrepared})" : ''; $subject = "{$safeJob} – Copy of Signed Contract"; $html = <<
Thank you for signing your contract — here’s your copy and access link.
$logoHtml Job #$safeJob
Hello {$firstNameSafe},
Thank you for signing the contract{$preparedPart}. A copy is attached for your records, and you can view or download it anytime using the link below:
View Contract
If the button doesn’t work, copy and paste this link into your browser:
$safeUrl
Thanks again — we’re excited to be working with you and looking forward to getting started.

Kind Regards,

$safeSignature
Benjamin Harris
Modulos Design
0402 984 082 | drafting@modulosdesign.com.au
This is an automated message. Please reply to this email if you have any questions.
HTML; $alt = "Hello {$firstName},\n\nThe contract has been signed{$preparedPart}.\n\nView/download: {$viewUrl}\n\nThanks,\n{$company}"; return [$subject, $html, $alt]; } ?>