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[^>]*>.*?\1\s*>#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 .=
'| ' .
esc_html($column["label"]) .
" | ";
}
$html .= "
";
foreach ($value as $row) {
$html .= "";
foreach ($field["columns"] as $column) {
$html .=
'| ' .
nl2br(esc_html(scalar($row[$column["id"]] ?? ""))) .
" | ";
}
$html .= "
";
}
return $html . "
";
}
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("#(td|th)>#i", ": ", $html);
$html = preg_replace(
"#
|(p|div|h[1-4]|tr|li|blockquote)>#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
? '
'
: $brand;
if ($header_note !== "") {
$header .=
'' .
$header_note .
"
";
}
$html =
'' .
esc_html($form["name"]) .
'' .
$preheader .
'
' .
($header !== ""
? '| ' .
$header .
" |
"
: "") .
'| ' .
$body .
" |
" .
($footer !== ""
? '| ' .
$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,
);
},
],
);
}
}