Symfony Mailer对接Exchange Server 2016的NTLM认证问题求助
问题回顾
尝试Swift Mailer原生NTLM认证时,出现以下错误:
PHP Fatal error: Uncaught Swift_TransportException: Failed to authenticate on SMTP server with username "domain\username" using 1 possible authenticators. Authenticator NTLM returned Expected response code 235 but got code "535", with message "535 5.7.3 Authentication unsuccessful
切换到Symfony Mailer后,使用第三方NTLM认证器出现类未找到错误:
PHP Fatal error: Uncaught Error: Class "NtlmAuthenticator" not found in /var/www/cfs-tools/App/mailtest.php:15
针对Symfony Mailer的解决方案
1. 正确实现并加载NTLM认证器
Symfony Mailer本身不支持NTLM,需自定义认证类并确保自动加载生效:
步骤1:创建自定义NTLM认证类
在项目src/Mailer/Auth目录下新建NtlmAuthenticator.php,实现AuthenticatorInterface(可参考Swift Mailer的NTLM认证逻辑完善协议细节):
<?php namespace App\Mailer\Auth; use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; class NtlmAuthenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'NTLM'; } public function authenticate(EsmtpTransport $transport): void { $socket = $transport->getStream(); $username = $transport->getUsername(); $password = $transport->getPassword(); // 发送NTLM初始请求 $socket->write("AUTH NTLM\r\n"); $response = $socket->read(); if (substr($response, 0, 3) !== '334') { throw new \RuntimeException('NTLM auth initiation failed: ' . $response); } // 生成Type1消息 $type1 = $this->buildType1Message($username); $socket->write(base64_encode($type1) . "\r\n"); $response = $socket->read(); if (substr($response, 0, 3) !== '334') { throw new \RuntimeException('NTLM Type1 response invalid: ' . $response); } // 解析Type2消息,生成Type3响应 $type2 = base64_decode(substr($response, 4)); $type3 = $this->buildType3Message($type2, $username, $password); $socket->write(base64_encode($type3) . "\r\n"); $response = $socket->read(); if (substr($response, 0, 3) !== '235') { throw new \RuntimeException('NTLM authentication failed: ' . $response); } } private function buildType1Message(string $username): string { $domain = explode('\\', $username)[0] ?? ''; $flags = 0x00000001 | 0x00000002 | 0x00000004 | 0x00000200; $type1 = pack('CCSSSS', 78, 84, 76, 77, 0x01, 0x00); $type1 .= pack('LL', 32, 32); $type1 .= pack('LL', 0, 32); $type1 .= pack('L', $flags); $type1 .= str_pad($domain, strlen($domain) + (strlen($domain) % 2), "\x00"); $type1 .= str_repeat("\x00", 16 - strlen($domain)); return $type1; } private function buildType3Message(string $type2, string $username, string $password): string { // 从Type2消息中提取挑战值 $challenge = substr($type2, 24, 8); list(, $flags) = unpack('L', substr($type2, 20, 4)); $userParts = explode('\\', $username); $user = $userParts[1] ?? $username; $domain = $userParts[0] ?? ''; // 生成NTLMv2响应(需根据密码和挑战值计算,此处为框架示例,需完善哈希逻辑) $ntlmResponse = hash('md5', $password . $challenge, true); $ntlmResponse = str_pad($ntlmResponse, 24, "\x00"); $type3 = pack('CCSSSS', 78, 84, 76, 77, 0x03, 0x00); $type3 .= pack('LL', 0, 0); $type3 .= pack('LL', 0, 0); $type3 .= pack('L', $flags); $type3 .= $ntlmResponse; $type3 .= str_pad($user, strlen($user) + (strlen($user) % 2), "\x00"); $type3 .= str_pad($domain, strlen($domain) + (strlen($domain) % 2), "\x00"); // 修正长度和偏移字段(需根据实际内容长度计算) $userOffset = 40 + strlen($ntlmResponse); $domainOffset = $userOffset + strlen($user) + (strlen($user) % 2); $type3 = substr_replace($type3, pack('LL', strlen($user), $userOffset), 8, 8); $type3 = substr_replace($type3, pack('LL', strlen($domain), $domainOffset), 16, 8); return $type3; } }
步骤2:修改测试代码引入认证器
确保代码中正确引入命名空间,并转义用户名中的反斜杠:
<?php require '../vendor/autoload.php'; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mime\Email; use App\Mailer\Auth\NtlmAuthenticator; $transport = (new Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport('smtp.testdomain.com', 25)) ->setUsername('testdomain\\testemail') // 转义反斜杠,或改用UPN格式testemail@testdomain.com ->setPassword('password') ->setAutoTls(false); $transport->setAuthenticators([new NtlmAuthenticator()]); $mailer = new Mailer($transport); $email = (new Email()) ->from('testemail@testdomain.com') ->to("testemail@test.com") ->subject('Test Mail Subject') ->html('<p>Test Email Symfony</p>'); $mailer->send($email);
步骤3:更新Composer自动加载
在composer.json中确认PSR-4规则包含自定义命名空间:
{ "autoload": { "psr-4": { "App\\": "src/" } } }
执行命令更新自动加载:
composer dump-autoload
针对Swift Mailer认证失败的补充建议
若仍尝试使用Swift Mailer,可排查以下点:
- 用户名格式:尝试用UPN格式(
testemail@testdomain.com)代替domain\user - 端口与加密:Exchange 2016可能要求587端口+TLS,设置
->setPort(587)->setEncryption('tls') - Exchange权限:确认用户拥有SMTP发送权限,且接收连接器允许NTLM认证
内容的提问来源于stack exchange,提问作者macflirty

