Встроенная функция mail() — главная причина, по которой письма из PHP не доходят: она передаёт сообщение локальному sendmail, которого обычно нет, а если он есть — письмо уходит без авторизации и попадает в спам. Ниже — отправка через PHPMailer и fmailer, где подпись DKIM и запись SPF берёт на себя релей.
| Параметр | Значение |
|---|---|
| SMTP-хост | smtp.fmailer.ru |
| Порт | 587 |
| Шифрование | STARTTLS (соединение переводится в TLS до авторизации) |
| Логин | Логин SMTP-токена вашего домена |
| Пароль | Пароль этого токена — показывается один раз, при создании |
composer require phpmailer/phpmailer Создавайте PHPMailer с аргументом true, чтобы ошибки бросали исключение, а не возвращали false: иначе сломанная конфигурация неотличима от рабочей:
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
require 'vendor/autoload.php';
$mail = new PHPMailer(true); // true = бросать исключения, а не возвращать false
try {
$mail->isSMTP();
$mail->Host = 'smtp.fmailer.ru';
$mail->Port = 587;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // 'tls'
$mail->SMTPAuth = true;
$mail->Username = getenv('SMTP_USER');
$mail->Password = getenv('SMTP_PASS');
$mail->Timeout = 10;
$mail->CharSet = PHPMailer::CHARSET_UTF8; // иначе кириллица поедет
$mail->setFrom('noreply@mail.example.ru', 'Магазин');
$mail->addAddress('client@example.ru', 'Иван Иванов');
$mail->isHTML(true);
$mail->Subject = 'Подтверждение заказа №4417';
$mail->Body = '<h1>Заказ принят</h1><p>Доставка — завтра до 18:00.</p>';
$mail->AltBody = 'Заказ принят. Доставка — завтра до 18:00.'; // задавайте всегда
$mail->send();
} catch (Exception $e) {
error_log('Письмо не отправлено: ' . $mail->ErrorInfo);
}Для шифрования с первого байта на порту 465:
// Порт 465 — шифрование с первого байта
$mail->Port = 465;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; // 'ssl'SMTPOptions с verify_peer => false, который советуют на форумах, полностью выключает проверку TLS — соединение можно незаметно перехватить. Если проверка не проходит, обновите набор корневых сертификатов (openssl.cafile). $mail->addAttachment('/srv/invoices/2026-03.pdf', 'invoice.pdf');
// Из строки, собранной в памяти
$mail->addStringAttachment($pdfData, 'invoice.pdf', 'base64', 'application/pdf');
// Картинка в теле письма — в HTML как <img src="cid:logo">
$mail->addEmbeddedImage('/var/www/assets/logo.png', 'logo', 'logo.png'); По умолчанию PHPMailer подключается и авторизуется заново на каждое письмо. SMTPKeepAlive держит соединение открытым — только не забудьте очищать получателей, иначе каждое следующее письмо уйдёт ещё и всем предыдущим:
// Одно авторизованное соединение на всю пачку писем.
$mail->SMTPKeepAlive = true;
foreach ($users as $user) {
$mail->clearAddresses(); // иначе получатели накапливаются
$mail->addAddress($user->email);
$mail->Subject = 'Еженедельный отчёт';
$mail->Body = renderReport($user);
try {
$mail->send();
} catch (Exception $e) {
error_log("Не отправлено на {$user->email}: {$mail->ErrorInfo}");
}
}
$mail->smtpClose(); Никогда не подставляйте адрес посетителя в setFrom(): подпись DKIM и запись SPF вашего домена за чужой адрес не отвечают, письмо не проходит DMARC. Используйте addReplyTo():
// Форма обратной связи: From остаётся вашим, посетитель — в Reply-To.
$mail->setFrom('noreply@mail.example.ru', 'Сайт');
$mail->addAddress('sales@mail.example.ru');
$mail->addReplyTo($_POST['email'], $_POST['name']);
$mail->Subject = 'Заявка с сайта';
$mail->Body = nl2br(htmlspecialchars($_POST['message'], ENT_QUOTES, 'UTF-8'));
$mail->AltBody = $_POST['message'];// Временно вывести весь SMTP-диалог при отладке.
$mail->SMTPDebug = SMTP::DEBUG_SERVER;
$mail->Debugoutput = 'error_log';| Ошибка | Причина и решение |
|---|---|
SMTP connect() failed | Порт заблокирован — используйте 8587 или 8465 — либо шифрование не совпадает с портом. |
SMTP Error: Could not authenticate | Неверные доступы либо SMTPAuth остался false. |
| Ошибка SSL при подключении | ENCRYPTION_SMTPS на 587 или ENCRYPTION_STARTTLS на 465. |
| Кириллица приходит «кракозябрами» | Задайте $mail->CharSet = PHPMailer::CHARSET_UTF8. |
| Письмо уходит большему числу людей, чем нужно | В цикле не вызывается clearAddresses(). |
SMTP Error: data not accepted (550) | Домен в setFrom не подтверждён. |
mail() передаёт письмо локальному бинарнику sendmail. На большинстве современных хостингов почтового сервера за ним нет, поэтому вызов возвращает true, а письмо исчезает. Даже там, где сервер есть, письмо уходит без авторизации, не проходит проверки SPF и DKIM у получателя и попадает в спам. SMTP-клиент вроде PHPMailer снимает обе проблемы.
PHPMailer::ENCRYPTION_STARTTLS с портом 587 либо PHPMailer::ENCRYPTION_SMTPS с портом 465. Строковые значения 'tls' и 'ssl' означают то же самое. Несовпадение константы и порта — самая частая причина ошибки рукопожатия.
Нет. Фрагмент с allow_self_signed и verify_peer => false, который кочует по форумам, полностью выключает проверку TLS-сертификата, и соединение становится доступным для перехвата. Настоящая причина обычно в устаревшем наборе корневых сертификатов — укажите актуальный в openssl.cafile.
Обе библиотеки поддерживаются. PHPMailer — меньшая зависимость, легко встраивается в старый код без фреймворка. У Symfony Mailer чище интерфейс, настройка одной строкой DSN и асинхронные транспорты, поэтому в современном приложении он удобнее — на нём построен и Laravel.
Либо хостинг блокирует исходящие порты 25, 465 и 587 — тогда используйте 8587 или 8465, — либо тип шифрования не соответствует порту. Включите SMTPDebug = SMTP::DEBUG_SERVER, чтобы увидеть, на каком шаге обрывается диалог.
Да, всегда, когда Body содержит HTML. Письмо только с HTML-частью почтовые фильтры считают признаком спама, а в клиентах, предпочитающих простой текст, оно отобразится пустым. AltBody добавляет текстовую альтернативу и делает письмо multipart/alternative.