Формы обратного звонка и вопроса: отправка email на PHP

Две формы на PHP: обратный звонок в CSS-модальном окне и вопрос по email. Проверка полей, CSRF, фиксированный отправитель, экранирование HTML и обработка результата mail().

Содержание статьи

Пример для 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 года.

Все материалы раздела «Программирование»Каталог инструментов

Ещё в разделе «Программирование»