Содержание статьи
Пример для PHP 8.2+ с mbstring объединяет обратный звонок и форму вопроса. Получатель и отправитель задаются переменными MAIL_TO и MAIL_FROM в окружении сервера. Заголовок From не строится из HTTP_HOST и не берётся из пользовательского поля.
Файл index.php
Обработка выполняется до HTML-вывода. Формы используют общую сессию и CSRF-токен.
<?php
session_start();
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
$error = '';
$notice = $_SESSION['mail_notice'] ?? '';
unset($_SESSION['mail_notice']);
function field(string $key, int $max): string {
$value = $_POST[$key] ?? '';
if (!is_string($value) || mb_strlen($value, 'UTF-8') > $max) {
throw new InvalidArgumentException('Проверьте длину полей формы.');
}
return trim($value);
}
function html(string $text): string {
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$csrf = $_POST['csrf'] ?? null;
if (!is_string($csrf) || !hash_equals($_SESSION['csrf'], $csrf)) {
throw new InvalidArgumentException('Обновите страницу и повторите отправку.');
}
if (time() - ($_SESSION['mail_last_submit'] ?? 0) < 30) {
throw new InvalidArgumentException('Подождите 30 секунд перед повторной отправкой.');
}
$kind = field('kind', 20);
if ($kind === 'callback') {
$name = field('input_1', 100);
$phone = field('tel', 40);
if (!preg_match('/\A[+0-9() -]{7,40}\z/', $phone) || strlen(preg_replace('/[^0-9]/', '', $phone)) < 7) {
throw new InvalidArgumentException('Проверьте номер телефона.');
}
$subject = 'Заказ обратного звонка';
$message = '<p><b>Имя:</b> ' . html($name) . '</p><p><b>Телефон:</b> ' . html($phone) . '</p>';
} elseif ($kind === 'question') {
$name = field('input_name', 100);
$question = field('input_question', 2000);
if ($name === '' || $question === '') {
throw new InvalidArgumentException('Заполните имя и вопрос.');
}
$subject = 'Вопрос с сайта';
$message = '<p><b>Имя:</b> ' . html($name) . '</p><p><b>Вопрос:</b> ' . nl2br(html($question)) . '</p>';
} else {
throw new InvalidArgumentException('Неизвестная форма.');
}
$to = getenv('MAIL_TO');
$from = getenv('MAIL_FROM');
foreach ([$to, $from] as $address) {
if (!is_string($address) || strpbrk($address, "\r\n") !== false ||
!filter_var($address, FILTER_VALIDATE_EMAIL)) {
throw new RuntimeException('Не настроена отправка почты.');
}
}
$headers = ['MIME-Version' => '1.0', 'Content-Type' => 'text/html; charset=UTF-8',
'From' => $from, 'Reply-To' => $from];
$_SESSION['mail_last_submit'] = time();
if (!mail($to, mb_encode_mimeheader($subject, 'UTF-8'), $message, $headers)) {
throw new RuntimeException('Почтовый сервер не принял сообщение.');
}
$_SESSION['mail_notice'] = 'Сообщение принято почтовым сервером.';
$_SESSION['csrf'] = bin2hex(random_bytes(32));
header('Location: /', true, 303);
exit;
} catch (InvalidArgumentException | RuntimeException $exception) {
$error = $exception->getMessage();
}
}
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Обратная связь</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<p role="status"><?= html($notice) ?></p>
<p role="alert"><?= html($error) ?></p>
<a href="#modal" class="button openModal">Заказать обратный звонок</a>
<div id="modal" class="modal">
<div>
<h2>Заказ обратного звонка</h2>
<form action="index.php" method="post">
<input type="hidden" name="csrf" value="<?= html($_SESSION['csrf']) ?>">
<input type="hidden" name="kind" value="callback">
<label>Имя <input name="input_1" maxlength="100" autocomplete="name"></label>
<label>Телефон <input type="tel" name="tel" maxlength="40" autocomplete="tel" required></label>
<button type="submit">Отправить</button>
</form>
<a href="#close" title="Закрыть окно">Закрыть</a>
</div>
</div>
<h2>Задать вопрос</h2>
<form action="index.php" method="post">
<input type="hidden" name="csrf" value="<?= html($_SESSION['csrf']) ?>">
<input type="hidden" name="kind" value="question">
<label>Имя <input name="input_name" maxlength="100" autocomplete="name" required></label>
<label>Вопрос <textarea name="input_question" maxlength="2000" required></textarea></label>
<button type="submit">Отправить вопрос</button>
</form>
</body>
</html>
В старом send.php телефон передавался через input_2. Если сохраняете старую форму, переименуйте поле в tel либо согласованно поменяйте имя в обработчике. Параметр subj не используется: тема задаётся сервером. После успешного приёма выполнен фиксированный редирект 303; для страницы не в корне укажите свой постоянный адрес.
CSS-модальное окно ниже сохранено из исходного примера. Для рабочего интерфейса дополнительно проверьте управление фокусом, Escape и доступное имя окна; более полный вариант можно построить на HTML dialog.
Файл style.css
/* Контейнер */
.modal {
/* Слой перекрытия */
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
/* Трансформации прозрачности при открытии */
-webkit-transition: opacity 500ms ease-in;
-moz-transition: opacity 500ms ease-in;
transition: opacity 500ms ease-in;
/* Скрываем изначально */
opacity: 0;
pointer-events: none;
}
/* Показываем модальное окно */
.modal:target {
opacity: 1;
pointer-events: auto;
}
/* Содержание */
.modal > div {
width: min(300px, calc(100% - 40px));
box-sizing: border-box;
background: #ffffff;
position: relative;
margin: 10% auto;
/* По умолчанию минимизируем анимацию */
-webkit-animation: minimise 500ms linear;
/* Придаем хороший вид */
padding: 30px;
-moz-border-radius: 7px;
border-radius: 7px;
-webkit-box-shadow: 0 3px 20px rgba(0,0,0,0.9);
-moz-box-shadow: 0 3px 20px rgba(0,0,0,0.9);
box-shadow: 0 3px 20px rgba(0,0,0,0.9);
text-shadow: 0 1px 0 #fff;
}
/* Изменяем анимацию при открытии модального окна*/
.modal:target > div {
-webkit-animation-name: bounce;
}
.modal h2 {
font-size: 36px;
padding: 0 0 20px;
}
@-webkit-keyframes bounce {
0% {
-webkit-transform: scale3d(0.1,0.1,1);
-webkit-box-shadow: 0 3px 20px rgba(0,0,0,0.9);
}
55% {
-webkit-transform: scale3d(1.08,1.08,1);
-webkit-box-shadow: 0 10px 20px rgba(0,0,0,0);
}
75% {
-webkit-transform: scale3d(0.95,0.95,1);
-webkit-box-shadow: 0 0 20px rgba(0,0,0,0.9);
}
100% {
-webkit-transform: scale3d(1,1,1);
-webkit-box-shadow: 0 3px 20px rgba(0,0,0,0.9);
}
}
@-webkit-keyframes minimise {
0% {
-webkit-transform: scale3d(1,1,1);
}
100% {
-webkit-transform: scale3d(0.1,0.1,1);
}
}
/* Ссылка на кнопку Закрыть */
.modal a[href="#close"] {
position: absolute;
right: 0;
top: 0;
color: transparent;
}
/* Сбрасываем изменения */
.modal a[href="#close"]:focus {
outline: 2px solid currentColor;
}
/* Создаем кнопку Закрыть */
.modal a[href="#close"]:after {
content: 'X';
display: block;
/* Позиционируем */
position: absolute;
right: -10px;
top: -10px;
width: 20px;
padding: 1px 1px 1px 2px;
/* Стили */
text-decoration: none;
text-shadow: none;
text-align: center;
font-weight: bold;
background: #000;
color: #ffffff;
border: 3px solid #ffffff;
-moz-border-radius: 20px;
border-radius: 20px;
-webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5);
-moz-box-shadow: 0 1px 3px rgba(0,0,0,0.5);
box-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.modal a[href="#close"]:focus:after,
.modal a[href="#close"]:hover:after {
-webkit-transform: scale(1.1,1.1);
-moz-transform: scale(1.1,1.1);
}
.modal a[href="#close"]:focus:after {
outline: 2px solid #000;
}
/* Открываем модальное окно */
a.openModal {
}
a.openModal:hover,
a.openModal:focus {
}
.modal .modal_title {
display: block;
text-align: center;
font-size: 22pt;
}
Проверенные источники
www.php.net, www.php.net, www.php.net. Проверено 10 октября 2026 года.
Комментарии
Загрузка оценок и комментариев…
Комментариев пока нет. Вы можете оставить первый.