You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Symfony Mailer对接Exchange Server 2016的NTLM认证问题求助

解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 13:43:15