A new enquiry from {name}

Someone has completed {form_name}. Here is what they shared.

{submitted_fields}

Open this enquiry

'; return [ "body" => $body, "from_name" => "", "from_email" => "", "reply_to" => $confirmation ? "team" : "visitor", "reply_to_email" => "", "cc" => "", "bcc" => "", "preheader" => $confirmation ? "Your enquiry has reached us." : "A new enquiry is ready for your team.", "brand_label" => "{site_name}", "header_note" => "Received Enquiry using Feather Form by NOESIS", "logo_url" => "", "footer" => "{site_name}", "accent" => "#56BBED", "table_title" => "Submitted details", "table_mode" => "all", "table_fields" => [], "hide_empty" => true, "include_hidden" => false, ]; } function email_website_logo(): string { foreach ( array_unique([ (int) get_theme_mod("custom_logo"), (int) get_option("site_logo"), ]) as $id ) { if ($id && ($url = wp_get_attachment_image_url($id, "full"))) { return $url; } } return ""; } function email_templates(array $settings): array { $result = []; foreach (["notification", "confirmation"] as $kind) { $result[$kind] = array_replace( email_template_defaults($kind, $settings), (array) ($settings["email_templates"][$kind] ?? []), ); } return $result; } function clean_email_html(string $html): string { $html = preg_replace( '#<(script|style|iframe|object|embed|form|textarea|select|button|video|audio)\b[^>]*>.*?#is', "", $html, ); $allowed = []; foreach ( [ "p", "div", "span", "h1", "h2", "h3", "h4", "strong", "b", "em", "i", "u", "s", "br", "hr", "ul", "ol", "li", "blockquote", "table", "thead", "tbody", "tfoot", "tr", "th", "td", ] as $tag ) { $allowed[$tag] = ["style" => true, "align" => true]; } $allowed["a"] = [ "href" => true, "title" => true, "style" => true, "target" => true, "rel" => true, ]; $allowed["img"] = [ "src" => true, "alt" => true, "width" => true, "height" => true, "style" => true, ]; foreach (["table", "th", "td"] as $tag) { $allowed[$tag] += [ "width" => true, "height" => true, "colspan" => true, "rowspan" => true, "scope" => true, "cellpadding" => true, "cellspacing" => true, "border" => true, "role" => true, ]; } return wp_kses($html, $allowed, ["http", "https", "mailto"]); } function clean_email_templates(array $settings, array $fields) { $result = []; $ids = array_column($fields, "id"); foreach (email_templates($settings) as $kind => $raw) { $s = email_template_defaults($kind, $settings); if (!is_string($raw["body"]) || strlen($raw["body"]) > 50000) { return problem("Keep each email template below 50 KB."); } $s["body"] = clean_email_html($raw["body"]); if ( !empty( $settings[$kind === "notification" ? "notify" : "autoresponder"] ) && trim(wp_strip_all_tags($s["body"])) === "" && !str_contains($s["body"], " 150, "preheader" => 250, "brand_label" => 150, "header_note" => 250, "table_title" => 150, ] as $key => $max ) { $s[$key] = sanitize_text_field( substr(scalar($raw[$key] ?? ""), 0, $max), ); } $s["from_name"] = str_replace(["<", ">"], "", $s["from_name"]); $s["footer"] = sanitize_textarea_field( substr(scalar($raw["footer"] ?? ""), 0, 1500), ); foreach (["from_email", "reply_to_email"] as $key) { $value = trim(scalar($raw[$key] ?? "")); if ($value && !is_email($value)) { return problem( "Enter a valid email address for " . str_replace("_", " ", $key) . ".", ); } $s[$key] = sanitize_email($value); } $s["reply_to"] = in_array( $raw["reply_to"] ?? "", ["visitor", "team", "custom", "none"], true, ) ? $raw["reply_to"] : $s["reply_to"]; if ($s["reply_to"] === "custom" && !$s["reply_to_email"]) { return problem("Enter the custom Reply-To address."); } foreach (["cc", "bcc"] as $key) { $addresses = array_values( array_filter( array_map( "trim", preg_split('/[,;\n]+/', scalar($raw[$key] ?? "")), ), ), ); if ( count($addresses) > 10 || array_filter($addresses, static fn($email) => !is_email($email)) ) { return problem( "Use up to ten valid " . strtoupper($key) . " addresses, separated by commas.", ); } $s[$key] = implode(", ", array_unique($addresses)); } $s["logo_url"] = sanitize_form_url(scalar($raw["logo_url"] ?? "")); if (!empty($raw["logo_url"]) && !$s["logo_url"]) { return problem( "Use a relative image path or a public HTTPS logo URL.", ); } $s["accent"] = sanitize_hex_color($raw["accent"] ?? "") ?: "#56BBED"; $s["table_mode"] = ($raw["table_mode"] ?? "") === "selected" ? "selected" : "all"; $s["table_fields"] = array_values( array_intersect( array_map( "sanitize_key", array_slice((array) ($raw["table_fields"] ?? []), 0, 100), ), $ids, ), ); $s["hide_empty"] = !empty($raw["hide_empty"]); $s["include_hidden"] = !empty($raw["include_hidden"]); $result[$kind] = $s; } return $result; } function email_empty($value): bool { return $value === "" || $value === [] || $value === null; } function email_answer_html(array $field, $value): string { if ($field["type"] === "repeater" && is_array($value) && $value) { $html = ''; foreach ($field["columns"] as $column) { $html .= '"; } $html .= ""; foreach ($value as $row) { $html .= ""; foreach ($field["columns"] as $column) { $html .= '"; } $html .= ""; } return $html . "
' . esc_html($column["label"]) . "
' . nl2br(esc_html(scalar($row[$column["id"]] ?? ""))) . "
"; } if (in_array($field["type"], ["checkbox", "consent"], true)) { return $value === "yes" ? "Yes" : "No"; } if (!empty($field["options"])) { $labels = array_column($field["options"], "label", "value"); if (is_array($value)) { $value = array_map( static fn($v) => $labels[scalar($v)] ?? scalar($v), $value, ); } else { $value = $labels[scalar($value)] ?? $value; } } return email_empty($value) ? "—" : nl2br(esc_html(value_text($value))); } function submitted_fields_table( array $form, array $answers, array $template, ): string { $rows = []; foreach (flatten($form["schema"]) as $field) { if ( in_array( $field["type"], ["heading", "paragraph", "divider"], true, ) || ($field["type"] === "hidden" && !$template["include_hidden"]) || !array_key_exists($field["id"], $answers) ) { continue; } if ( $template["table_mode"] === "selected" && !in_array($field["id"], $template["table_fields"], true) ) { continue; } if ($template["hide_empty"] && email_empty($answers[$field["id"]])) { continue; } $rows[] = '' . esc_html($field["label"]) . '' . email_answer_html($field, $answers[$field["id"]]) . ""; } if (!$rows) { return ""; } return ($template["table_title"] !== "" ? '

' . esc_html($template["table_title"]) . "

" : "") . '' . implode("", $rows) . "
"; } function email_context(int $id): array { global $wpdb; $entry = $id ? $wpdb->get_row( $wpdb->prepare( "SELECT reference,created_at,source_path FROM %i WHERE id=%d", table("entries"), $id, ), ARRAY_A, ) : null; return [ "entry_reference" => $entry["reference"] ?? "SAMPLE-ENQUIRY", "submitted_at" => wp_date( "j F Y, g:i a", $entry ? strtotime($entry["created_at"] . " UTC") : time(), ), "entry_url" => $id ? admin_url("admin.php?page=noesis-forms-entries&entry=" . $id) : admin_url("admin.php?page=noesis-forms-entries"), "form_url" => home_url($entry["source_path"] ?? "/"), ]; } function email_merge( string $text, array $form, array $answers, array $contact, array $context, bool $html = false, ): string { $map = [ "form_name" => $form["name"], "name" => $contact["contact_name"] ?? "", "email" => $contact["email"] ?? "", "site_name" => get_bloginfo("name"), ] + $context; foreach ($answers as $id => $value) { if (!array_key_exists($id, $map)) { $map[$id] = value_text($value); } } $replacements = []; foreach ($map as $key => $value) { $replacements["{" . $key . "}"] = $html ? esc_html($value) : $value; } foreach ($answers as $id => $value) { $replacements["{field:" . $id . "}"] = $html ? esc_html(value_text($value)) : value_text($value); } return strtr($text, $replacements); } function email_plain_text(string $html): string { $html = preg_replace_callback( '#]*href=["\']([^"\']+)["\'][^>]*>(.*?)#is', static fn($m) => wp_strip_all_tags($m[2]) . " (" . html_entity_decode($m[1], ENT_QUOTES, "UTF-8") . ")", $html, ); $html = preg_replace("##i", ": ", $html); $html = preg_replace( "#|#i", "\n", $html, ); return trim( preg_replace( '/\n{3,}/', "\n\n", html_entity_decode(wp_strip_all_tags($html), ENT_QUOTES, "UTF-8"), ), ); } function render_email( array $form, array $answers, array $contact, string $kind, array $context, ): array { $s = $form["settings"]; $t = email_templates($s)[$kind]; $body = preg_replace( "#]*>\s*\{submitted_fields\}\s*

#i", "{submitted_fields}", $t["body"], ); // Replace tokens once: user answers cannot inject additional merge tags. $marker = "NOESIS_TABLE_" . wp_generate_uuid4(); $body = str_replace("{submitted_fields}", $marker, $body); $body = email_merge($body, $form, $answers, $contact, $context, true); $body = str_replace( $marker, submitted_fields_table($form, $answers, $t), $body, ); // Email links/images need a full URL even when the editor stores a portable path. $tags = new \WP_HTML_Tag_Processor($body); while ($tags->next_tag()) { $attribute = $tags->get_tag() === "A" ? "href" : ($tags->get_tag() === "IMG" ? "src" : ""); if (!$attribute) { continue; } $url = $tags->get_attribute($attribute); if ( is_string($url) && str_starts_with($url, "/") && !str_starts_with($url, "//") ) { $tags->set_attribute($attribute, home_url($url)); } } $body = $tags->get_updated_html(); // Validate link/image protocols after merging values into attributes as well. $body = clean_email_html($body); $brand = email_merge( $t["brand_label"], $form, $answers, $contact, $context, true, ); $header_note = email_merge( $t["header_note"], $form, $answers, $contact, $context, true, ); $footer = nl2br( email_merge($t["footer"], $form, $answers, $contact, $context, true), ); $preheader = email_merge( $t["preheader"], $form, $answers, $contact, $context, true, ); $logo = $t["logo_url"] ?: email_website_logo(); if (str_starts_with($logo, "/")) { $logo = home_url($logo); } $header = $logo ? '' .
            esc_attr(wp_strip_all_tags($brand)) .
            '' : $brand; if ($header_note !== "") { $header .= '
' . $header_note . "
"; } $html = '' . esc_html($form["name"]) . '
' . $preheader . '
' . ($header !== "" ? '" : "") . '" . ($footer !== "" ? '" : "") . "
' . $header . "
' . $body . "
' . $footer . "
"; $subject = sanitize_text_field( email_merge( $kind === "notification" ? $s["email_subject"] : $s["autoresponder_subject"], $form, $answers, $contact, $context, ), ); return [ "subject" => $subject, "body" => $html, "alt_body" => email_plain_text( wp_strip_all_tags($brand) . "\n" . $header_note . "\n" . $body . "\n" . $footer, ), "html" => true, ]; } function email_reply_to(array $template, array $form, array $contact): string { return match ($template["reply_to"]) { "visitor" => is_email($contact["email"] ?? "") ? $contact["email"] : "", "team" => trim(explode(",", $form["settings"]["recipients"])[0]), "custom" => $template["reply_to_email"], default => "", }; } function email_payload( array $form, array $answers, array $contact, string $kind, array $context, string $to, ): array { $t = email_templates($form["settings"])[$kind]; return render_email($form, $answers, $contact, $kind, $context) + [ "to" => $to, "reply_to" => email_reply_to($t, $form, $contact), "from_name" => sanitize_text_field( email_merge($t["from_name"], $form, $answers, $contact, $context), ), "from_email" => $t["from_email"], "cc" => $t["cc"], "bcc" => $t["bcc"], ]; } function send_email_payload(array $payload): bool { $headers = [ "Content-Type: " . (!empty($payload["html"]) ? "text/html" : "text/plain") . "; charset=UTF-8", ]; foreach ( ["reply_to" => "Reply-To", "cc" => "Cc", "bcc" => "Bcc"] as $key => $name ) { if (!empty($payload[$key])) { $headers[] = $name . ": " . $payload[$key]; } } if (!empty($payload["from_email"]) || !empty($payload["from_name"])) { $host = preg_replace( "/^www\./", "", wp_parse_url(home_url("/"), PHP_URL_HOST) ?: "", ); $email = $payload["from_email"] ?: // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- This is the existing WordPress core mail filter, intentionally shared with mail configuration plugins. apply_filters("wp_mail_from", "wordpress@" . $host); $name = str_replace( ["<", ">", "\r", "\n"], "", $payload["from_name"] ?: // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- This is the existing WordPress core mail filter, intentionally shared with mail configuration plugins. apply_filters("wp_mail_from_name", "WordPress"), ); $headers[] = "From: " . $name . " <" . $email . ">"; } $alt = static function ($mailer) use ($payload) { $mailer->AltBody = $payload["alt_body"] ?? ""; }; add_action("phpmailer_init", $alt, 1000); try { return wp_mail( $payload["to"], $payload["subject"], $payload["body"], $headers, ); } finally { remove_action("phpmailer_init", $alt, 1000); if (isset($GLOBALS["phpmailer"])) { $GLOBALS["phpmailer"]->AltBody = ""; } } } function email_preview_answers(array $form): array { $answers = []; $contact = [ "contact_name" => "Alex Morgan", "email" => "alex@example.test", "company" => "Example company", "subject" => "", ]; foreach ($form["schema"]["pages"] as $page) { if (!matches($page["conditions"] ?? [], $answers)) { continue; } foreach ($page["fields"] as $f) { if ( in_array( $f["type"], ["heading", "paragraph", "divider"], true, ) || !matches($f["conditions"], $answers) ) { continue; } $value = $f["default"]; if ($value === "") { $value = match ($f["type"]) { "email" => "alex@example.test", "tel" => "+1 202 555 0146", "url" => "https://example.test", "textarea" => "We would like to discuss a new website and a simpler way to manage our enquiries.", "select", "radio" => $f["options"][0]["value"] ?? "", "multiselect", "checkboxes" => [ $f["options"][0]["value"] ?? "", ], "checkbox", "consent" => "yes", "number", "range" => (string) ($f["min"] ?? 2), "rating" => "5", "date" => wp_date("Y-m-d"), "time" => "10:30", "datetime-local" => wp_date("Y-m-d") . "T10:30", "file" => ["project-brief.pdf"], "repeater" => [ array_combine( array_column($f["columns"], "id"), array_map( static fn($c) => $c["type"] === "number" ? "2" : ($c["type"] === "email" ? "alex@example.test" : "Example item"), $f["columns"], ), ), ], "hidden" => "Campaign example", "calculation" => "150.00", default => "Example answer", }; } if ($f["role"]) { $value = match ($f["role"]) { "name" => "Alex Morgan", "email" => "alex@example.test", "company" => "Example company", default => $value, }; } $answers[$f["id"]] = $value; } } return ["answers" => $answers, "contact" => $contact]; } function register_email_routes(): void { foreach (["email-preview", "email-test"] as $action) { register_rest_route( "noesis-forms/v1", "/forms/(?P\d+)/" . $action, [ "methods" => "POST", "permission_callback" => static fn() => allowed(), "callback" => static function ($request) use ($action) { if (!get_form((int) $request["id"])) { return problem("Form not found.", 404); } $body = $request->get_json_params(); $form = clean_form( is_array($body["form"] ?? null) ? $body["form"] : [], ); if (is_wp_error($form)) { return $form; } $kind = ($body["kind"] ?? "") === "confirmation" ? "confirmation" : "notification"; $sample = email_preview_answers($form); $context = email_context(0); if ($action === "email-preview") { return render_email( $form, $sample["answers"], $sample["contact"], $kind, $context, ) + ["sample" => true]; } $to = trim(scalar($body["to"] ?? "")); if (!is_email($to)) { return problem("Enter one valid test recipient."); } $rate = "feather_forms_email_test_" . get_current_user_id(); $count = (int) get_transient($rate); if ($count >= 5) { return problem( "Five test emails have been sent. Please wait ten minutes before sending another.", 429, ); } $payload = email_payload( $form, $sample["answers"], $sample["contact"], $kind, $context, $to, ); $payload["cc"] = ""; $payload["bcc"] = ""; $payload["subject"] = "[TEST] " . $payload["subject"]; set_transient($rate, $count + 1, 10 * MINUTE_IN_SECONDS); return send_email_payload($payload) ? [ "sent" => true, "message" => "Test email handed to the mail server.", ] : problem( "The mail server did not accept the test email. Check the site mail settings.", 502, ); }, ], ); } }