最近更新: 2007-04-03

PHP mail() and charset encoding question

不知從何時開始, PHP 內建的 mail() 行為改變了,使用 mail() 寄發電子郵件時,似乎會固定將內文之字元編碼轉為 iso-8859-1 字元集。於是用 mail() 寄中文郵件時變亂碼、寄東歐文字郵件時變亂碼、寄日文郵件時變亂碼等等問題一一出現。如何寄非英文語系郵件幾乎成了 FAQ 級問題。

早期的 mail() 並沒有這種狀況。基本上只要在信件的標頭欄位中添加文件型態及字元集(charset)資訊,如 'Content-type: text/plain; charset="big5"' 即可用以寄發中文郵件。但不知何時開始,這種方式不再適用。在 PHP manual 中也未提到這方面的變化,僅僅說可用 PEAR::Mail 完成更複雜郵件寄送工作。另外隱晦地加上 mb_send_mail() ,表示應該使用此一函數寄發多位元組字元集文字郵件。

為了測試前述問題所在,以及完成寄送中文郵件的工作,我設計了一個替用的 mail 函數。對於了解 SMTP 協定內容的程序員而言,要自行設計一個 mail() 並不困難。下列便是我設計的 mail() 替用函數。它是一個對照組,用以測試並確認郵件內容變亂碼是 PHP 內建的 mail() 所造成的。由於設計目的之中包含了測試用途,所以程式碼中還留有觀察訊息的敘述。

MyMailer.php

<?php
/*
MyMailer class
Copyright (C) 2007 Shih Yuncheng <shirock@educities.edu.tw>
This library is licensed under GNU Lesser General Public License.
*/
class MyMailer {
    private static function waitSmtpResponse(&$sh, $code) {
        echo ($response = fgets($sh));
        if (strncmp($response, $code, 3)) {
            echo $response;
            return self::smtpFalse($sh);
        }
        return true;
    }

    private static function smtpFalse(&$sh) {
        fclose($sh);
        return false;
    }

    public static function mail($To, $Subject, $message, $additional_headers=false) {
        $headerTable = array('To', 'Subject');

        $smtpServer = get_cfg_var('SMTP');
        if (empty($smtpServer))
            return false;

        if (!($mailFrom = get_cfg_var('sendmail_from'))) {
            $headers = explode("\n", $additional_headers);
            foreach ($headers as $header) {
                if (preg_match('/^From: ([^\b]+)/', $header, $matches))
                    $mailFrom = $matches[1];
            }
            if (empty($mailFrom))
                return self::smtpFalse($fsock);
        }

        if (!($fsock = fsockopen('tcp://'.$smtpServer, get_cfg_var('smtp_port'),
            $errno, $errstr)))
        {
            return false;
        }
        if (!self::waitSmtpResponse($fsock, '220'))
            return false;

        fwrite($fsock, "Helo {$smtpServer}\r\n");
        if (!self::waitSmtpResponse($fsock, '250'))
            return false;

        fwrite($fsock, "Mail from: {$mailFrom}\r\n");
        if (!self::waitSmtpResponse($fsock, '250'))
            return false;

        $rcptSet = explode(',', $To);
        foreach ($rcptSet as $rcpt) {
            fwrite($fsock, "Rcpt to: {$rcpt}\r\n");
            if (!self::waitSmtpResponse($fsock, '250'))
                return false;
        }

        fwrite($fsock, "Data\r\n");
        if (!self::waitSmtpResponse($fsock, '354')) //354 Enter mail
            return false;

        foreach ($headerTable as $cmd)
            fwrite($fsock, "${cmd}: {$$cmd}\r\n");
        if ($additional_headers)
            fwrite($fsock, $additional_headers);
        fwrite($fsock, "\r\n");

        fwrite($fsock, $message);
        fwrite($fsock, "\r\n.\r\n");

        fwrite($fsock, "Quit\r\n");
        fclose($fsock);
    }
}
?>

MyMailer 以靜態方法 MyMailer::mail() 實現郵件寄送行為,其用法與 PHP 內建的 mail() 完全相同。它將讀取 php.ini 中的 SMTPsendmail_from 之設定值內容決定 SMTP 伺服器位址以及寄信人資訊,接著開啟 socket 連線直接與 SMTP 伺服器交談以完成郵件遞送工作。

以下為一測試範例,將同一訊息分別以 PHP的 mail() 和我設計的 MyMailer::mail() 寄送,藉此觀察能否寄送中文郵件。從測試結果來看,我們可以發現 PHP 的 mail() 所寄送之內容會成亂碼 (但也有人不會碰到這種情形)。

<?php
require 'MyMailer.php';

$mailTo = 'your@example.com'; // 請自行替換有效的收件地址
$message = 'test 測試';
$subject = 'TEST 測試';
$headers = 'Content-type: text/plain; charset="big5"' . "\r\n";
$headers .= 'From: my@example.com' . "\r\n"; // 請自行替換寄件地址

mail($mailTo, $subject, $message, $headers);
MyMailer::mail($mailTo, $subject, $message, $headers);
?>

由於 PHP 內建的 mail() 功能頗為簡單,用於寄送簡單的 log 訊息尚可,寄送一般信件就略顯力有未逮。因此在開發應用軟體時,建議使用 PEAR::Mail 或是 PHPMailer。不僅可解決寄送中文郵件的困擾,尚可寄送夾帶附件的郵件,應多加利用。雖然兩者功能類似,但我建議依授權方式選擇。 PEAR::Mail 採 PHP/BSD license 授權發佈,而 PHPMailer 採 GPL/LGPL 授權發佈。故偏好 GPL/LGPL 授權的程序員應選擇採用 PHPMailer 。啥?用本文中的 MyMailer ? 那也成,順便提醒一下, MyMailer 也是採 GPL/LGPL 授權發佈。

內文沒問題,但標題和寄信人內容是亂碼

標題和寄信人的內容是亂碼之原因在於未指定字元編碼。此處常引起誤解,程序員會說「我已經在Content-type中指定 charset 了啊」。內文的字元編碼可透過 Content-type 指定字元集(charset) ,但標題和寄信人的內容並非內文之一部份,故其字元編碼並非由 Content-type 所指定。事實上,標題和寄信人屬於郵件 Header ,其規範內容為 RFC 2047 - MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text。根據該規範內容, Header 中的非 ASCII 字元之內容應以 「=?charset encoding?encoding code?header content?=」之格式指定。起於=? ,止於 ?=encoding code 一般為 "q": Quoted-printable ;或 "b": Base64。

由於 PHP 僅提供 base64_encode() ,故下列範例之 encoding code 一律為 b

<?php
//資料皆為utf-8編碼
$messageHeaderMap = array(
    'Subject'   =>  '中文標題測試',
    'From'      =>  '許功蓋 <you@example.com>'
);

$charset = 'utf-8';
$messageEncoding = 'base64';
$headerFormat = '=?' . $charset . '?' . $messageEncoding[0] . '?%s?=';

$headers = '';
foreach ($messageHeaderMap as $k => $v) {
    $headers .= $k . ': ' . sprintf($headerFormat, base64_encode($v)) . "\r\n";
}
$headers .= 'Content-type: text/plain; charset=' . $charset . "\r\n";

$message = '中文內容測試';

MyMailer::mail(
    'my@example.com',
    sprintf($headerFormat, base64_encode($messageHeaderMap['Subject'])),
    $message,
    $headers
);
?>
相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/2950655.html

樂多舊回應
未留名 (#comment-4369777)
Sun, 08 Apr 2007 05:15:27 +0800
我使用了你的方式但是出現
Parse error: syntax error, unexpected T_STATIC, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or '}' in /home/public_html/mail_test/MyMailer.php on line 8

因為PHPMailer之前我用的都沒問題,最近卻變成亂碼,我實在找不到解決方式,它的主旨跟標題是正常的 內容確是亂碼
不知大大有辦法幫忙解決否?
未留名 (#comment-4371721)
Sun, 08 Apr 2007 17:17:57 +0800
PHP 版本不同,我忘了在哪個版本以前, static 關鍵字必須放在可見度宣告關鍵字的後面。所以你把 static 關鍵字放在 public/private 關鍵字之後即可。

PHPMailer 內定使用 PHP 內建的 mail() 。然而就像本文提到 PHP 內建的 mail() 改變了行為內容,所以信件內容的編碼不適合中文信件。

故你應該要像本文採用的方式一般,要求 PHPMailer 使用 SMTP 方式寄送信件,例如加上 $mailer->IsSMTP();$mailer->Mailer = "smtp";
在 PHPMailer 的 FAQ 中也是建議用 SMTP 方式 (See also: What mailer gives me the best performance?
capulet@ms25.hinet.net(小賴) (#comment-4417349)
Thu, 12 Apr 2007 16:41:50 +0800
我還是用mail()來寄,我用UTF-8編碼網頁
內容部分ok,只是title會變成亂碼,只要加一句就正常了
$title = "=?UTF-8?B?".base64_encode($title)."?=";

這樣寄出,主旨就不會亂碼了
vri2004@gmail.com(紫誘惑) (#comment-4417969)
Thu, 12 Apr 2007 18:57:33 +0800
在主旨和標題都還是big5碼沒有任何錯誤,但是內文就無法正常顯示,我後來發現我應該不是使用SMTP,因為我那個在PHP.INI內並沒有設定任何資料,而是由主機商那邊預設值,可是經過PHPMailer 送信後,信件還是亂碼,我已經找了一星期資料,徹底放棄了,雖然我還是很想解決,網路上找的相關亂碼問題好像都是針對某些論壇外掛,幾忽找不到非論壇發信的討論mail(),有也是一些舊資料已經不適用了,大大不知可否說明的詳細些
我試了好多方式都失敗,最後,終於無奈放棄了

我想問題是出在
$mail->Body = "$d";

不知道版大可以幫我看看問題到底在那裡嗎?

未留名 (#comment-4418849)
Thu, 12 Apr 2007 21:32:34 +0800
我在上面的回應已經說了,你應改用 SMTP 方式寄信。
你的程式並未要求 PHPMailer 採用 SMTP 方式
vri2004@gmail.com(紫誘惑) (#comment-4420879)
Fri, 13 Apr 2007 01:51:09 +0800
痾!!感謝大大!!我終於懂了!!

謝謝!!
補充一點,我是放在虛擬主機上面,而之前一直出錯原因是SMTP沒架設成功,一直把 HOST這個東西設成自己網域

原來還有分 win32跟LINUX不同 >"<
搞好久!!真是感謝大大回答!!真好!!
未留名 (#comment-15558189)
Wed, 16 Jan 2008 15:41:50 +0800
我也是使用虛擬主機,也遇到亂碼的問題,剛剛看到一篇文章,只用了簡單兩行(用mb_send_mail代替mail),就解決了我的問題,提供大家參考。

mb_internal_encoding("UTF-8");
$headers = 'From: nectar020@yahoo.com.cn\r\nReply-To: me@budian.cn\r\nX-Mailer: PHP/' . phpversion();
mb_send_mail("$To", "$Subject", "$Content", $headers);


取自:http://www.wretch.cc/blog/Linpy&article_id=10919198
未留名 (#comment-15558831)
Wed, 16 Jan 2008 17:39:39 +0800
to Derek:
關於使用 mb_send_mail() 可解決信件亂碼的原因,請參考本文 part2

mail() 會寄出亂碼信件的原因,其實也是 mb_send_mail()。成也蕭何,敗也蕭何。