$cfg["dev_name"] ?? "Modulos Design",
"from_email" => $cfg["from_address"] ?? "drafting@modulosdesign.com.au",
"bcc_email" => $cfg["bcc_email"] ?? "drafting@modulosdesign.com.au",
"secret" => $cfg["loa_secret"] ?? ($cfg["admin_secret"] ?? "change-me"),
];
// Compute base URL
$https = !empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off";
$scheme = $https ? "https" : "https";
$host = $_SERVER["HTTP_HOST"] ?? "localhost";
$base = rtrim(dirname($_SERVER["SCRIPT_NAME"] ?? ""), "/\\");
$CFG["base_url"] = $scheme . "://" . $host . ($base ? $base . "/" : "/");
/* ----------------------------- helpers ----------------------------- */
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, "UTF-8"); }
function getClientIp(): string {
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;
}
foreach ($parts as $ip) if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
}
if (!empty($_SERVER['HTTP_X_REAL_IP']) && filter_var($_SERVER['HTTP_X_REAL_IP'], FILTER_VALIDATE_IP)) return $_SERVER['HTTP_X_REAL_IP'];
if (!empty($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) return $_SERVER['REMOTE_ADDR'];
return "UNKNOWN";
}
function tokenForJob(string $job, string $secret): string { return hash_hmac("sha256", "loa|" . $job, $secret); }
function verifyToken(string $job, string $token, string $secret): bool {
return $token !== "" && hash_equals(tokenForJob($job, $secret), $token);
}
/* --------------------------- Front matter --------------------------- */
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 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;
$trimmed = ltrim($raw, " ");
if ($trimmed === "" || $trimmed[0] === "#") continue;
$indent = strlen($raw) - strlen($trimmed);
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 getByPath($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;
}
function parseFrontMatterForJob(string $job): array {
$safe = preg_match('/^\d{1,10}$/', $job) ? $job : "default";
$path = __DIR__ . "/loa/$safe/" . $safe . ".md";
if (!is_file($path)) $path = __DIR__ . "/loa/default-authorisation.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 abs_url(string $rel): string {
$https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
$scheme = $https ? 'https' : 'https';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$dir = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\');
$root = $scheme . '://' . $host . ($dir ? $dir . '/' : '/');
return $root . ltrim($rel, '/');
}
function council_recipients(array $vars, array $cfg): array {
$to = [];
if (!empty($cfg['council_email'])) $to[] = $cfg['council_email'];
$councilName = (string)getByPath($vars, 'property.council', '');
if ($councilName && !empty($cfg['council_map'][$councilName])) {
$to[] = $cfg['council_map'][$councilName];
}
// TODO: if you later store locality/postcode, you can look up by locality too.
return array_values(array_unique(array_filter($to)));
}
/* --------------------- Render LOA Markdown to HTML --------------------- */
function md_to_html(string $md): string {
$pd = class_exists('ParsedownExtra') ? new ParsedownExtra() : new Parsedown();
$pd->setSafeMode(true);
// Optional: $pd->setBreaksEnabled(true); // if you want single newlines as
return $pd->text($md);
}
function loadLoaHtml(string $job, array $overrides = []): string {
$safe = preg_match('/^\d{1,10}$/', $job) ? $job : "default";
$path = __DIR__ . "/loa/$safe/" . $safe . ".md";
if (!is_file($path)) $path = __DIR__ . "/loa/default-authorisation.md";
$md = file_get_contents($path);
// 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);
}
// Defaults available
$base = [
"dev" => [
"name" => $GLOBALS["cfg"]["dev_name"] ?? "Modulos Design",
"email" => $GLOBALS["cfg"]["dev_email"] ?? "drafting@modulosdesign.com.au",
"phone" => $GLOBALS["cfg"]["dev_phone"] ?? "0402 984 082",
"address" => $GLOBALS["cfg"]["dev_address"] ?? "34 Coplestone St, Scottsdale, Tas 7260",
],
"client" => [
"name" => "",
"email" => "",
"phone" => "",
"address" => "",
],
"job" => $safe,
"today" => date("F j, Y"),
];
// Merge: overrides > front matter > base
$merged = array_replace_recursive($base, $vars, $overrides);
// 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]);
}
}
// Replace [path.to.value] placeholders
$body = preg_replace_callback('/\[([a-zA-Z0-9_.-]+)\]/', function ($m) use ($merged) {
$val = getByPath($merged, $m[1]);
return is_scalar($val) ? (string)$val : "";
}, $body);
// Convert Markdown to HTML (ParsedownExtra supports tables)
return md_to_html($body);
}
/* --------------------------- HTML wrappers --------------------------- */
function headerWithTitle(string $title, ?string $job = null, ?string $preparedDate = null, string $context = "web"): string {
$safeTitle = h($title);
$safeJob = h((string)$job);
$safePreparedDate = h((string)$preparedDate);
$https = !empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off";
$scheme = $https ? "https" : "https";
$host = $_SERVER["HTTP_HOST"] ?? "localhost";
$dir = rtrim(dirname($_SERVER["SCRIPT_NAME"] ?? ""), "/\\") . "/";
$cssLinks = $context === "web"
? <<
HTML
: <<
HTML;
$jsLinks = $context === "web"
? <<
HTML
: "";
$nav = $context === "web"
? <<
HTML
: "";
return <<
{$safeJob} - {$safeTitle}
{$cssLinks}
{$jsLinks}
{$nav}
HTML;
}
function footerFor(string $context = "web"): string {
$extra = $context === "web"
? <<
function printDoc(){ window.print(); }
HTML
: "";
return <<
{$extra}
HTML;
}
/* --------------------------- Email helpers --------------------------- */
function salutationFromName(string $fullName): string {
$name = str_replace("\xC2\xA0", " ", $fullName);
$name = trim(preg_replace('/\s+/u', ' ', $name));
if ($name === "") return "there";
$name = preg_replace('/,?\s*(Jr\.?|Sr\.?|II|III|IV|MD|Ph\.?D|Esq\.?|J\.?D\.?|M\.?B\.?A\.?|RN|DDS|DMD)\s*$/iu', '', $name);
$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);
}
$tokens = preg_split('/[\s\x{00A0}]+/u', $name);
if (!$tokens) return "there";
foreach ($tokens as $tok) {
$t = rtrim($tok, ".");
if (!preg_match('/^[A-Za-z]\.?$/u', $t)) 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 "";
$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 "";
$cid = "logo_" . substr(sha1($bin), 0, 12) . "@modulos";
$mail->addStringEmbeddedImage($bin, $cid, "logo.png", "base64", "image/png");
return '
';
}
function email_cta(string $href, string $label): string {
$safeUrl = h($href);
$safeLbl = h($label);
return <<
{$safeLbl}
{$safeLbl}
HTML;
}
function email_signature_block(string $safeSignatureHtml, string $company = "Modulos Design"): string {
$safeCo = h($company);
return <<
Kind Regards, {$safeSignatureHtml}
Benjamin Harris
{$safeCo}
0402 984 082 | drafting@modulosdesign.com.au
HTML;
}
/**
* Reusable frame: preheader + header band + {contentHtml} + footer band
*/
function email_frame(string $preheader, string $logoHtml, string $job, string $contentHtml, string $footerNote = "This is an automated message. Please reply to this email if you have any questions."): string {
$safePre = h($preheader);
$safeJob = h($job);
$safeFoot = h($footerNote);
return <<
{$safePre}
{$logoHtml}
Job #{$safeJob}
{$contentHtml}
{$safeFoot}
HTML;
}
function buildSignedLoaEmail(
string $logoHtml,
string $viewUrl,
string $job,
string $clientName = "",
string $preparedDate = "",
string $company = "Modulos Design",
string $safeSignature = ""
): array {
$firstName = salutationFromName($clientName);
$firstNameSafe = h($firstName);
$safeUrl = h($viewUrl);
$safeJob = h($job);
$safePrepared = h($preparedDate);
$preparedPart = $preparedDate ? " (prepared {$safePrepared})" : "";
$subject = "{$safeJob} – Copy of Signed Authorisation";
$cta = email_cta($safeUrl, "View Authorisation");
$sig = email_signature_block($safeSignature, $company);
$content = <<
Hello {$firstNameSafe},
Thank you for signing the authorisation{$preparedPart}. A copy is attached for your records,
and you can view or download it anytime using the link below:
{$cta}
If the button doesn’t work, copy and paste this link into your browser:
{$safeUrl}
{$sig}
HTML;
$html = email_frame(
"Thank you for signing your authorisation — here’s your copy and access link.",
$logoHtml,
$job,
$content
);
$alt = "Hello {$firstName},\n\nThe authorisation has been signed{$preparedPart}.\n\nView/download: {$viewUrl}\n\nThanks,\n{$company}";
return [$subject, $html, $alt];
}
function buildCouncilRequestEmail(
string $logoHtml,
string $job,
array $vars,
string $loaPublicUrl,
string $company = "Modulos Design",
string $safeSignature = ""
): array {
$addr = (string)getByPath($vars, 'property.address', '');
$pid = (string)getByPath($vars, 'property.pid', '');
$title = (string)getByPath($vars, 'property.title', '');
$vol = (string)getByPath($vars, 'property.vol', '');
$folio = (string)getByPath($vars, 'property.folio', '');
$owners = (string)getByPath($vars, 'client.name', '');
$prepared = (string)getByPath($vars, 'dates.prepared', date('F j, Y'));
$titleRef = $title ?: trim($vol . '/' . $folio, '/');
$safeAddr = h($addr);
$safePid = h($pid);
$safeTitle = h($titleRef);
$safeOwners= h($owners);
$safeJob = h($job);
$safeDate = h($prepared);
$safeUrl = h($loaPublicUrl);
$safeCo = h($company);
$subject = "Request for Planning & Plumbing Information – {$safeAddr} (PID {$safePid}, Title {$safeTitle}) – Job #{$safeJob}";
$sig = email_signature_block($safeSignature, $company);
$cta = email_cta($safeUrl, "View Signed LOA");
$content = <<
Good day,
We’re requesting planning and plumbing information for:
Property Address {$safeAddr}
Registered Owner(s) {$safeOwners}
PID {$safePid}
Title / Volume–Folio {$safeTitle}
LOA prepared {$safeDate}
Specifically, could you please supply or confirm:
Record of any recent or active Development/Building/Plumbing Applications and associated permit numbers and decisions;
Any additional planning advice or pre-application notes Council considers relevant.
A signed Letter of Authority is attached. You can also view it here:
{$cta}
If the button doesn’t work, copy and paste this link:
{$safeUrl}
{$sig}
HTML;
$html = email_frame(
"Request for planning & plumbing information for {$addr}.",
$logoHtml,
$job,
$content,
"Please reply to this email with the requested information or next steps."
);
$alt = "Request for planning & plumbing info\n\n"
. "Address: {$addr}\n"
. "Owners: {$owners}\n"
. "PID: {$pid}\n"
. "Title: {$titleRef}\n"
. "LOA prepared: {$prepared}\n\n"
. "Please provide scheme & zoning, overlays, constraints/easements, stormwater/sewer/water service info, "
. "any recent DA/BA/Plumbing records, and any other relevant advice.\n\n"
. "LOA (view): {$loaPublicUrl}\n\n"
. "Thanks,\n{$company}";
return [$subject, $html, $alt];
}
function getHtmlUrl(string $htmlName): string {
$https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
$scheme = $https ? 'https' : 'https';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$dir = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/\\');
return $scheme . '://' . $host . ($dir ? $dir : '') . '/' . $htmlName;
}
/* ------------------------------- Routing ------------------------------- */
$job = isset($_REQUEST["job"]) ? preg_replace('/\D+/', '', (string)$_REQUEST["job"]) : "";
$token = $_REQUEST["token"] ?? "";
// Resolve some prepared/derived values
$preparedDate = getByPath(parseFrontMatterForJob($job), "dates.prepared", date("F j, Y"));
if ($_SERVER["REQUEST_METHOD"] === "GET") {
if (!$job || !verifyToken($job, $token, $CFG["secret"])) {
http_response_code(403);
echo "Auth required";
exit;
}
// If a signed file already exists for this job, redirect there
$pattern = __DIR__ . "/loa/{$job}/{$job}_signed_loa*.pdf";
$matches = glob($pattern);
if ($matches) {
usort($matches, fn($a, $b) => filemtime($b) <=> filemtime($a));
$latest = basename($matches[0]);
header("Location: loa/{$job}/" . $latest, true, 302);
exit;
}
// Render unsigned page
$HEADER = headerWithTitle("Unsigned Authorisation", $job, $preparedDate, "web");
$LOA_HTML = loadLoaHtml($job);
echo $HEADER;
echo $LOA_HTML;
// Signature UI (mirrors contracts.php)
?>
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 = getClientIp();
$CLIENT_SIGNATURE = '' . h(getByPath($vars, "client.name", "")) . ' ';
$CLIENT_SIGNATURE .= ''
. 'Signed on: ' . h($clientDate) . ' '
. 'Client IP: ' . h($clientIp) . '
'
. ' ';
$compiled = <<
{$CLIENT_SIGNATURE}
HTML;
// Build final HTML (web and pdf versions)
$headerWeb = headerWithTitle("{$job} - Signed Authorisation", $job, getByPath($vars, "dates.prepared", date("F j, Y")), "web");
$footerWeb = footerFor("web");
$outputWeb = $headerWeb . $LOA_HTML . $compiled . $footerWeb;
$headerPdf = headerWithTitle("{$job} - Signed Authorisation", $job, getByPath($vars, "dates.prepared", date("F j, Y")), "pdf");
$footerPdf = footerFor("pdf");
$outputPdf = $headerPdf . $LOA_HTML . $compiled . $footerPdf;
// Render and save PDF
$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");
$https = !empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off";
$scheme = $https ? "https" : "http";
$host = $_SERVER["HTTP_HOST"] ?? "localhost";
$dir = rtrim(dirname($_SERVER["SCRIPT_NAME"] ?? ""), "/\\") . "/";
$dompdf->setBasePath($scheme . "://" . $host . $dir);
$dompdf->render();
$pdfPathRel = "{$job}_signed_loa.pdf";
$pdfPathAbs = __DIR__ . "/loa/{$job}/" . $pdfPathRel;
$pdfPublicRel = "loa/{$job}/{$pdfPathRel}";
file_put_contents($pdfPathAbs, $dompdf->output());
// Email client + dev
$clientEmail = (string)(getByPath($vars, "client.email", "") ?: "");
$devEmail = (string)($cfg["dev_email"] ?? "drafting@modulosdesign.com.au");
$fromAddress = (string)($cfg["from_address"] ?? "drafting@modulosdesign.com.au");
// --- Dorset council PDF + email (only if Dorset is the council) ---
try {
// Guard so we only do this for Dorset; set in your LOA front matter:
// council:
// name: "Dorset Council"
// email: "development@dorset.tas.gov.au"
$councilName = (string) getByPath($vars, 'council.name', '');
$councilEmail = (string) getByPath($vars, 'council.email', '');
if ($councilEmail && stripos($councilEmail, 'tazz.com.au') !== false) { //dorset.tas.gov.au
require_once __DIR__ . '/dorset_fill.php';
$templatePath = __DIR__ . '/loa/dorset_consent_form.pdf';
$dorsetOutAbs = __DIR__ . "/loa/{$job}/{$job}_dorset_consent_form.pdf";
$dorsetPdf = generate_dorset_application($job, $vars, $cfg, $templatePath, $dorsetOutAbs);
if ($dorsetPdf) {
// Email Dorset + BCC dev
$mailCouncil = new PHPMailer(true);
// SMTP (optional but recommended if set in config)
if (!empty($cfg['smtp_host'])) {
$mailCouncil->isSMTP();
$mailCouncil->Host = $cfg['smtp_host'] ?? '';
$mailCouncil->SMTPAuth = true;
$mailCouncil->Username = $cfg['smtp_username'] ?? '';
$mailCouncil->Password = $cfg['smtp_password'] ?? '';
$mailCouncil->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
$mailCouncil->Port = (int)($cfg['smtp_port'] ?? 465);
}
// From/Reply-To
$fromAddress = (string)($cfg['from_address'] ?? 'drafting@modulosdesign.com.au');
$mailCouncil->CharSet = 'UTF-8';
$mailCouncil->Encoding = 'base64';
$mailCouncil->setFrom($fromAddress, $CFG['brand_name'] ?? 'Modulos Design');
if (!empty($cfg['dev_email'])) $mailCouncil->addReplyTo($cfg['dev_email']);
// ✅ REQUIRED: recipient(s)
$mailCouncil->addAddress($councilEmail);
if (!empty($CFG['bcc_email'])) $mailCouncil->addBCC($CFG['bcc_email']);
// Build body using shared template
$logoCouncil = email_logo_png_cid($mailCouncil, $cfg['dark_logo'] ?? "", $CFG['brand_name'] ?? 'Modulos Design', 200);
$sigCouncil = email_logo_png_cid($mailCouncil, $cfg['dev_signature'] ?? "", "Signature", 100);
$loaUrl = abs_url($pdfPublicRel);
[$subject, $html, $alt] = buildCouncilRequestEmail(
$logoCouncil,
$job,
$vars,
$loaUrl,
(string)($CFG['brand_name'] ?? 'Modulos Design'),
$sigCouncil
);
$mailCouncil->isHTML(true);
$mailCouncil->Subject = $subject;
$mailCouncil->Body = $html;
$mailCouncil->AltBody = $alt;
// Attach Dorset form + (optionally) the signed LOA
if (is_file($dorsetPdf)) $mailCouncil->addAttachment($dorsetPdf, basename($dorsetPdf));
if (is_file($pdfPathAbs)) $mailCouncil->addAttachment($pdfPathAbs, basename($pdfPathAbs));
try {
$mailCouncil->send();
} catch (Throwable $e) {
error_log("Council email failed for job {$job} to {$councilEmail}: ".$e->getMessage());
}
}
}
} catch (Throwable $e) {
error_log("Dorset generation error for job {$job}: ".$e->getMessage());
}
// Build the mailer once per recipient (same as contracts.php)
$targets = [];
if ($clientEmail) $targets[] = ["to" => $clientEmail, "kind" => "client"];
if ($devEmail) $targets[] = ["to" => $devEmail, "kind" => "dev"];
foreach ($targets as $t) {
$mail = new PHPMailer(true);
$mail->SMTPDebug = SMTP::DEBUG_OFF;
if (!empty($cfg["smtp_host"])) {
$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, $CFG["brand_name"] ?? "Modulos Design");
if ($t["kind"] === "client" && $devEmail) $mail->addReplyTo($devEmail);
if ($t["kind"] === "dev" && $clientEmail)$mail->addReplyTo($clientEmail);
$mail->addAddress($t["to"]);
$mail->isHTML(true);
// Embed assets per message
$logoHtml = email_logo_png_cid($mail, $cfg["dark_logo"] ?? "", $CFG["brand_name"] ?? "Modulos Design", 200);
$safeSignature = email_logo_png_cid($mail, $cfg["dev_signature"] ?? "", "Signature", 100);
[$subject, $html, $alt] = buildSignedLoaEmail(
$logoHtml,
abs_url($pdfPublicRel), // <= FIX: public URL, not the filesystem path
$job,
(string)getByPath($vars, "client.name", ""),
(string)getByPath($vars, "dates.prepared", date("F j, Y")),
(string)($CFG["brand_name"] ?? "Modulos Design"),
$safeSignature
);
// Developer copy extra info
if ($t["kind"] === "dev") {
$subject = $job . " – Authorisation has been signed";
$signedBy = h($clientEmail ?: "unknown");
$inject = 'Signed by: ' . $signedBy . ' ';
$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;
if (!empty($CFG["bcc_email"])) $mail->addBCC($CFG["bcc_email"]);
if (is_file($pdfPathAbs)) $mail->addAttachment($pdfPathAbs, basename($pdfPathAbs));
try {
$mail->send();
} catch (MailerException $e) {
error_log("LOA mailer error to {$t['to']}: {$mail->ErrorInfo}\n");
}
}
// Public URL to the signed LOA (for the link in the email body)
$loaUrl = abs_url($pdfPublicRel);
// Council recipients
$councilTo = council_recipients($vars, $cfg);
foreach ($councilTo as $addr) {
try {
$mail = new PHPMailer(true);
$mail->SMTPDebug = SMTP::DEBUG_OFF;
if (!empty($cfg["smtp_host"])) {
$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;
$mail->Port = $cfg["smtp_port"] ?? 465;
}
$mail->CharSet = "UTF-8";
$mail->Encoding = "base64";
$mail->setFrom($CFG["from_email"] ?? "drafting@modulosdesign.com.au", $CFG["brand_name"] ?? "Modulos Design");
// Replies come back to you
if (!empty($cfg["dev_email"])) $mail->addReplyTo($cfg["dev_email"]);
$mail->addAddress($addr);
$mail->isHTML(true);
// Embed assets
$logoHtml = email_logo_png_cid($mail, $cfg["dark_logo"] ?? "", $CFG["brand_name"] ?? "Modulos Design", 200);
$safeSignature = email_logo_png_cid($mail, $cfg["dev_signature"] ?? "", "Signature", 100);
// Build council email
[$subject, $html, $alt] = buildCouncilRequestEmail(
$logoHtml,
$job,
$vars,
$loaUrl,
(string)($CFG["brand_name"] ?? "Modulos Design"),
$safeSignature
);
$mail->Subject = $subject;
$mail->Body = $html;
$mail->AltBody = $alt;
if (!empty($CFG["bcc_email"])) $mail->addBCC($CFG["bcc_email"]);
if (is_file($pdfPathAbs)) $mail->addAttachment($pdfPathAbs, basename($pdfPathAbs)); // attach signed LOA
$mail->send();
} catch (MailerException $e) {
error_log("Council mailer error to {$addr}: {$mail->ErrorInfo}\n");
}
}
// Redirect to signed HTML
header("Location: " . $pdfPublicRel, true, 303);
exit;
}