关于phpMailer乱码问题

在使用phpMailer发送邮件时,遇到标题显示为特殊格式的乱码问题。通过研究源码,发现乱码与base64编码的中文和特殊字符有关。问题定位在邮件标题编码的特定位置。要解决此问题,可以调整源码或在设置标题时进行处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  最近在利用phpmailer这个很火的第三方函数包开发php邮件发送功能时候,遇到一个很蹊跷的乱码问题,即发出去的邮件有时候邮件标题会变成一堆很特别的乱码,乱码的具体内容根据邮件标题是会发生变化的,不过共同的特点是都是有=?utf-8?B?这样的格式,在网上查阅了很多资料,包括stackoverflow,有不少人也都遇到过这个问题,但是回答的都比较模糊,初步的结论是邮件标题在进行base64编码时可能由于中文、特殊字符等原因产生了乱码,但单纯这么说毕竟不能令人信服,只能选择自己动手,查看phpmailer的源码来查找这个问题

  经过反复var_dump和exit进行断点,最终目标确定在了这样一段代码上

public function EncodeHeader($str, $position = 'text') {
    $x = 0;
    switch (strtolower($position)) {
      case 'phrase':
        if (!preg_match('/[\200-\377]/', $str)) {
          // Can't use addslashes as we don't know what value has magic_quotes_sybase
          $encoded = addcslashes($str, "\0..\37\177\\\"");
          if (($str == $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
            return ($encoded);
          } else {
            return ("\"$encoded\"");
          }
        }
        $x = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
        break;
      case 'comment':
        $x = preg_match_all('/[()"]/', $str, $matches);
        // Fall-through
      case 'text':
      default:
        $x += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
        break;
    }


    if ($x == 0) {
      return ($str);
    }


    $maxlen = 75 - 7 - strlen($this->CharSet);
    // Try to select the encoding which should produce the shortest output
    if (strlen($str)/3 < $x) {
      $encoding = 'B';
      if (function_exists('mb_strlen') && $this->HasMultiBytes($str)) {
        // Use a custom function which correctly encodes and wraps long
        // multibyte strings without breaking lines within a character
        $encoded = $this->Base64EncodeWrapMB($str);
      } else {
        $encoded = base64_encode($str);
        $maxlen -= $maxlen % 4;
        $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
      }
    } else {
      $encoding = 'Q';
      $encoded = $this->EncodeQ($str, $position);
      $encoded = $this->WrapText($encoded, $maxlen, true);
      $encoded = str_replace('='.$this->LE, "\n", trim($encoded));
    }


    $encoded = preg_replace('/^(.*)$/m', " =?".$this->CharSet."?$encoding?\\1?=", $encoded);
    $encoded = trim(str_replace("\n", $this->LE, $encoded));
    return $encoded;
  }

这一段是邮件标题在发送前进行的base64和smtp格式转化的部分(题外话:后台查阅资料发现开头说的=?utf-8?B?,这是smtp协议所规定的邮件标题格式,完整格式是=?charset?encoding?encoded-text?text ),在正常情况下,即将发送的邮件的标题格式应当是

=?utf-8?B?44CQ5byA5pyN5ZGo55+l44CR44CK5by55by55aCC44CLMjAxNi0xMC0yNSAxNzo0OToyNg==?=

但是,在出错的情况下这里的格式变成了

=?UTF-8?B?44CQ5byA5pyN5ZGo55+l44CR44CK5by55by55aCC44CLMjAxNi0xMC0yNSAx
?=
 =?UTF-8?B?Nzo0OToyNg==?=
  注意,这里中间有回车符间隔,且smtp协议格式的头部被重复了两次,经过仔细研究,发现乱码情况下出现的乱码就是后面一部分的=?UTF-8...这部分,此时可以得出结论,就是这里编码和格式的错误,导致邮件接收端无法正确地进行解析,从而导致了标题乱码

  那么,又是什么原因导致了出现这种情况呢,再次研究,发现编码的错误出现在了上面这个位置:

$encoded = $this->Base64EncodeWrapMB($str);
切换到这个函数的细节,可以看到:

public function Base64EncodeWrapMB($str) {
    $start = "=?".$this->CharSet."?B?";
    $end = "?=";
    $encoded = "";

    $mb_length = mb_strlen($str, $this->CharSet);
    // Each line must have length <= 75, including $start and $end
    $length = 75 - strlen($start) - strlen($end);
    // Average multi-byte ratio
    $ratio = $mb_length / strlen($str);
    // Base64 has a 4:3 ratio
    $offset = $avgLength = floor($length * $ratio * .75);

    for ($i = 0; $i < $mb_length; $i += $offset) {
      $lookBack = 0;

      do {
        $offset = $avgLength - $lookBack;
        $chunk = mb_substr($str, $i, $offset, $this->CharSet);
        $chunk = base64_encode($chunk);
        $lookBack++;
      }
      while (strlen($chunk) > $length);

      $encoded .= $chunk . $this->LE;
    }

    // Chomp the last linefeed
    $encoded = substr($encoded, 0, -strlen($this->LE));
    return $encoded;
  }
  阅读源码后可以发现这里对于传过来的字符串是这样处理的,把字符串按照每76个字符的长度进行一次base64_encode,然后之间用回车符隔开后进行返回,然而这个时候一个很关键的问题出现了,在进行smtp格式转换的时候,源码是这样的
encoded = preg_replace('/^(.*)$/m', " =?".$this->CharSet."?$encoding?\\1?=", $encoded);
  对之前编码后的字符串,每个用回车符隔开的部分都会分别添加一次smtp格式的头,因而也就出现了之前提到的,乱码无法解析的情况,不过分析到这里依然还有奇怪的地方,就是作者为什么要画蛇添足对标题进行这种处理呢,后面我会讲,解决这个问题的办法就是直接进行base64_encode并添加smtp头,并不需要这个麻烦的Base64EncodeWrap函数,一种说法是base64编码对于编码长度实际上是有限制的,每隔76个字符(解释了作者写的76的问题)将自动进行一次换行,作者为了避免这个问题而如此处理,我确实查到了一个传说中的RFC2045-2049协议(多用途网际邮件扩充协议),里面也确实有这么一段话

   The encoded output stream must be represented in lines of no more
   than 76 characters each.  All line breaks or other characters not
   found in Table 1 must be ignored by decoding software.  In base64
   data, characters other than those in Table 1, line breaks, and other
   white space probably indicate a transmission error, about which a
   warning message or even a message rejection might be appropriate
   under some circumstances.
大致意思是base64编码每行不能超过76个字符,超出的需要用回车符进行分隔,但是我实际测试中,长度超出76的编码串依然能正常发送,不过RFC似乎本身是1996年的协议,可能新的标准改变了也说不定,这里还需要进一步研究

最后说一下怎么解决邮件标题的乱码问题,如果能自己读懂源码的话,只要在我说的几个地方简单加以修改就可以了,这里我不再赘述,有一个简单的方法是在调用phpmailer配置邮件标题的时候进行处理,正常情况下代码是

$mail->Subject = $title;
这里修改成

$mail->Subject = "=?utf-8?B?".base64_encode("$title")."?=";
手动对其进行smtp格式转化和base64编码即可解决,有兴趣的话可以自行阅读源码看看为什么这样就可以解决问题,我自己看的时候发现是phpmailer中字符处理的判断,不含有ascII以外的字符时,将不再调用base64编码,直接使用传入的使用者传入的原始字符串

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值