作者:limodou
在作者所申請的幾個(gè)PHP 主頁空間中,能夠提供mail功能的實(shí)在不多,總是調(diào)用完mail()函數(shù)之后就毫 無下文了。但是電子郵件在網(wǎng)上生活中的作用越來越大。想一想網(wǎng)蟲上網(wǎng)不收郵件能叫真正的網(wǎng)蟲嗎?郵件 的作用我不想再說了,但是如果主頁空間不支持mail()發(fā)送那么怎么辦呢?我也想過通過socket來實(shí)現(xiàn)郵件 發(fā)送,但無奈對用php 進(jìn)行socket編程不熟悉,再加上發(fā)送郵件要用到SMTP協(xié)議,又要讀不少的英文了,所 以一直也沒有去研究過。終于有一天我發(fā)現(xiàn)了一篇文章,關(guān)于用socket編程發(fā)送郵件。我如獲至寶般將其拷 貝下來,并且將其改造成了一個(gè)php 可用的類,供大家使用。原來的文章只是一個(gè)簡單的例子,而且還有一 些錯(cuò)誤,在我經(jīng)過多次的實(shí)驗(yàn)、改造終于將其改成了一個(gè)直接使用socket,向指定的郵箱發(fā)送郵件的類,如 果大家和前面關(guān)于發(fā)送MIME的文章結(jié)合起來,就可以實(shí)現(xiàn)在不支持mail()函數(shù)的網(wǎng)站上發(fā)送郵件了。因?yàn)榘l(fā) 送郵件的過程需要時(shí)間,可能與mail()的處理機(jī)制還不完全一樣,所以速度要慢一些,但是可以解決需要發(fā) 送郵件功能的燃眉之急,同時(shí)你也可以學(xué)習(xí)用php 進(jìn)行socket編程。下面就將這個(gè)類的實(shí)現(xiàn)原理介紹給大家, 同時(shí)向大家講解一些關(guān)于SMTP的基本知識。
Socket編程介紹 向大家申明,本人不是一個(gè)TCP/IP編程專家,故在此只是講出了我的一點(diǎn)理解和體會(huì)。
使用fsockopen函數(shù)打開一個(gè)Internet連接,函數(shù)語法格式:
int fsockopen(string hostname, int port, int [errno], string [errstr], int [timeout]);
參數(shù)的意思我想不用講了,這里由于要使用SMTP協(xié)議,所以端口號為25。在打開連接成功后,會(huì)返回一 個(gè)socket句柄,使用它就可以象使用文件句柄一樣的。可使用的操作有fputs(),fgets(),feof(),fclose() 等。
很簡單地介紹就到這里吧。
SMTP的基礎(chǔ) 基于TCP/IP的因特網(wǎng)協(xié)議一般的命令格式都是通過請求/ 應(yīng)答方式實(shí)現(xiàn)的,采用的都是文本信息,所以 處理起來要容易一些。SMTP是簡單郵件傳輸協(xié)議的簡稱,它可以實(shí)現(xiàn)客戶端向服務(wù)器發(fā)送郵件的功能。所以 下面所講的命令是指客戶端向服務(wù)器發(fā)出請求指令,而響應(yīng)則是指服務(wù)器返回給客戶端的信息。
SMTP分為命令頭和信息體兩部分。命令頭主要完成客戶端與服務(wù)器的連接,驗(yàn)證等。整個(gè)過程由多條命 令組成。每個(gè)命令發(fā)到服務(wù)器后,由服務(wù)器給出響應(yīng)信息,一般為3 位數(shù)字的響應(yīng)碼和響應(yīng)文本。不同的服 務(wù)器返回的響應(yīng)碼是遵守協(xié)議的,但是響應(yīng)正文本則不必。每個(gè)命令及響應(yīng)的最后都有一個(gè)回車符,這樣使 用fputs()和fgets()就可以進(jìn)行命令與響應(yīng)的處理了。SMTP的命令及響應(yīng)信息都是單行的。信息體則是郵件 的正文部分,最后的結(jié)束行應(yīng)以單獨(dú)的"."作為結(jié)束行。
客戶端一些常用的SMTP指令為:
HELO hostname: 與服務(wù)器打招呼并告知客戶端使用的機(jī)器名字,可以隨便填寫 MAIL FROM: sender_id : 告訴服務(wù)器發(fā)信人的地址 RCPT TO: receiver_id : 告訴服務(wù)器收信人的地址 DATA : 下面開始傳輸信件內(nèi)容,且最后要以只含有.的特殊行結(jié)束 RESET: 取消剛才的指令,從新開始 VERIFY userid: 校驗(yàn)帳號是否存在(此指令為可選指令,服務(wù)器可能不支持) QUIT : 退出連接,結(jié)束 服務(wù)器返回的響應(yīng)信息為(格式為:響應(yīng)碼+空格+解釋):
220 服務(wù)就緒(在socket連接成功時(shí),會(huì)返回此信息) 221 正在處理 250 請求郵件動(dòng)作正確,完成(HELO,MAIL FROM,RCPT TO,QUIT指令執(zhí)行成功會(huì)返回此信息) 354 開始發(fā)送數(shù)據(jù),結(jié)束以 .(DATA指令執(zhí)行成功會(huì)返回此信息,客戶端應(yīng)發(fā)送信息) 500 語法錯(cuò)誤,命令不能識別 550 命令不能執(zhí)行,郵箱無效 552 中斷處理:用戶超出文件空間 下面給出一個(gè)簡單的命令頭(這是在打開socket之后做的),是我向stmp.263.net發(fā)郵件的測試結(jié)果:
HELO limodou 250 smtp.263.net MAIL FROM: chatme@263.net 250 Ok RCPT TO: chatme@263.net 250 Ok DATA 354 End data with . To: chatme@263.net From: chatme@263.net Subject: test From: chatme@263.net test . QUIT 250 Ok: queued as C46411C5097E0
這就是一些SMTP的簡單知識。相關(guān)內(nèi)容可以查閱RFC。
RFC 821定義了收/發(fā)電子郵件的相關(guān)指令。 RFC 822則制定了郵件?熱蕕母袷健? RFC 2045-2048制定了多媒體郵件?熱蕕母袷劍? RFC 1113, 1422-1424則是討論如何增進(jìn)電子郵件的保密性。
send_mail類的實(shí)現(xiàn) 現(xiàn)在開始介紹我所編寫的發(fā)送郵件類。有了上面的預(yù)備知識了,下面就是實(shí)現(xiàn)了。
類的成員變量
var $lastmessage; //記錄最后返回的響應(yīng)信息 var $lastact; //最后的動(dòng)作,字符串形式 var $welcome; //用在HELO后面,歡迎用戶 var $debug; //是否顯示調(diào)試信息 var $smtp; //smtp服務(wù)器 var $port; //smtp端口號 var $fp; //socket句柄
其中,$lastmessage和$lastact用于記錄最后一次響應(yīng)信息及執(zhí)行的命令,當(dāng)出錯(cuò)時(shí),用戶可以使用它 們。為了測試需要,我還定義了$debug變量,當(dāng)其值為true時(shí),會(huì)在運(yùn)行過程中顯示一些執(zhí)行信息,否則無 任何輸出。$fp用于保存打開后的socket句柄。
類的構(gòu)造
-------------------------------------------------------------------------------- function send_mail($smtp, $welcome="", $debug=false) { if(empty($smtp)) die("SMTP cannt be NULL!"); $this->smtp=$smtp; if(empty($welcome)) { $this->welcome=gethostbyaddr("localhost"); } else $this->welcome=$welcome; $this->debug=$debug; $this->lastmessage=""; $this->lastact=""; $this->port="25"; } -------------------------------------------------------------------------------- 這個(gè)構(gòu)造函數(shù)主要完成一些初始值的判定及設(shè)置。$welcome用于HELO指令中,告訴服務(wù)器用戶的名字。 HELO指令要求為機(jī)器名,但是不用也可以。如果用戶沒有給出$welcome,則自動(dòng)查找本地的機(jī)器名。
顯示調(diào)試信息
-------------------------------------------------------------------------------- 1 function show_debug($message, $inout) 2 { 3 if ($this->debug) 4 { 5 if($inout=="in") //響應(yīng)信息 6 { 7 $m='<< '; 8 } 9 else 10 $m='>> '; 11 if(!ereg("$", $message)) 12 $message .= "<br>"; 13 $message=nl2br($message); 14 echo "<font color=#999999>${m}${message}</font>"; 15 } 16 } -------------------------------------------------------------------------------- 這個(gè)函數(shù)用來顯示調(diào)試信息。可以在$inout中指定是上傳的指令還是返回的響應(yīng),如果為上傳指令,則 使用"out";如果為返回的響應(yīng)則使用"in"。
第3行,判斷是否要輸出調(diào)試信息。 第5行,判斷是否為響應(yīng)信息,如果是,則在第7行將信息的前面加上"<< "來區(qū)別信息;否則在第10行加上 ">> "來區(qū)別上傳指令。 第11-12行,判斷信息串最后是否為換行符,如不是則加上HTML換行標(biāo)記。第13行將所以的換行符轉(zhuǎn)成HTML 的換行標(biāo)記。 第14行,輸出整條信息,同時(shí)將信息顏色置為灰色以示區(qū)別。
執(zhí)行一個(gè)命令
-------------------------------------------------------------------------------- 1 function do_command($command, $code) 2 { 3 $this->lastact=$command; 4 $this->show_debug($this->lastact, "out"); 5 fputs ( $this->fp, $this->lastact ); 6 $this->lastmessage = fgets ( $this->fp, 512 ); 7 $this->show_debug($this->lastmessage, "in"); 8 if(!ereg("^$code", $this->lastmessage)) 9 { 10 return false; 11 } 12 else 13 return true; 14 } -------------------------------------------------------------------------------- 在編寫socket處理部分發(fā)現(xiàn),一些命令的處理很相似,如HELO,MAIL FROM,RCPT TO,QUIT,DATA命令, 都要求根據(jù)是否顯示調(diào)試信息將相關(guān)內(nèi)容顯示出來,同時(shí)對于返回的響應(yīng)碼,如果是期望的,則應(yīng)繼續(xù)處理, 如果不是期望的,則應(yīng)中斷出理。所以為了清晰與簡化,專門對這些命令的處理編寫了一個(gè)通用處理函數(shù)。 函數(shù)的參數(shù)中$code為期望的響應(yīng)碼,如果響應(yīng)碼與之相同則表示處理成功,否則出錯(cuò)。
第3行,記錄最后執(zhí)行命令。 第4行,將上傳命令顯示出來。 第5行,則使用fputs真正向服務(wù)器傳換指令。 第6行,從服務(wù)器接收響應(yīng)信息將放在最后響應(yīng)消息變量中。 第7行,將響應(yīng)信息顯示出來。 第8行,判斷響應(yīng)信息是否期待的,如果是則第13行返回成功(true),否則在第10行返回失敗(false)。
這樣,這個(gè)函數(shù)一方面完成指令及信息的發(fā)送顯示功能,別一方面對返回的響應(yīng)判斷是否成功。
郵件發(fā)送處理
下面是真正的秘密了,可要看仔細(xì)了。:)
-------------------------------------------------------------------------------- 1 function send( $to,$from,$subject,$message) 2 { 3 4 //連接服務(wù)器 5 $this->lastact="connect"; 6 7 $this->show_debug("Connect to SMTP server : ".$this->smtp, "out"); 8 $this->fp = fsockopen ( $this->smtp, $this->port ); 9 if ( $this->fp ) 10 { 11 12 set_socket_blocking( $this->fp, true ); 13 $this->lastmessage=fgets($this->fp,512); 14 $this->show_debug($this->lastmessage, "in"); 15 16 if (! ereg ( "^220", $this->lastmessage ) ) 17 { 18 return false; 19 } 20 else 21 { 22 $this->lastact="HELO " . $this->welcome . ""; 23 if(!$this->do_command($this->lastact, "250")) 24 { 25 fclose($this->fp); 26 return false; 27 } 28 29 $this->lastact="MAIL FROM: $from" . ""; 30 if(!$this->do_command($this->lastact, "250")) 31 { 32 fclose($this->fp); 33 return false; 34 } 35 36 $this->lastact="RCPT TO: $to" . ""; 37 if(!$this->do_command($this->lastact, "250")) 38 { 39 fclose($this->fp); 40 return false; 41 } 42 43 //發(fā)送正文 44 $this->lastact="DATA"; 45 if(!$this->do_command($this->lastact, "354")) 46 { 47 fclose($this->fp); 48 return false; 49 } 50 51 //處理Subject頭 52 $head="Subject: $subject"; 53 if(!empty($subject) && !ereg($head, $message)) 54 { 55 $message = $head.$message; 56 } 57 58 //處理From頭 59 $head="From: $from"; 60 if(!empty($from) && !ereg($head, $message)) 61 { 62 $message = $head.$message; 63 } 64 65 //處理To頭 66 $head="To: $to"; 67 if(!empty($to) && !ereg($head, $message)) 68 { 69 $message = $head.$message; 70 } 71 72 //加上結(jié)束串 73 if(!ereg(".", $message)) 74 $message .= "."; 75 $this->show_debug($message, "out"); 76 fputs($this->fp, $message); 77 78 $this->lastact="QUIT"; 79 if(!$this->do_command($this->lastact, "250")) 80 { 81 fclose($this->fp); 82 return false; 83 } 84 } 85 return true; 86 } 87 else 88 { 89 $this->show_debug("Connect failed!", "in"); 90 return false; 91 } 92 } -------------------------------------------------------------------------------- 有些意思很清楚的我就不說了。
這個(gè)函數(shù)一共有四個(gè)參數(shù),分別是$to表示收信人,$from表示發(fā)信人,$subject表求郵件主題和$message 表示郵件體。如果處理成功則返回true,失敗則返回false。
第8行,連接郵件服務(wù)器,如果成功響應(yīng)碼應(yīng)為220。 第12行,設(shè)置阻塞模式,表示信息必須返回才能繼續(xù)。詳細(xì)說明看手冊吧。 第16行,判斷響應(yīng)碼是否為220,如果是,則繼續(xù)處理,否則出錯(cuò)返回。 第22-27行,處理HELO指令,期望響應(yīng)碼為250。 第29-34行,處理MAIL FROM指令,期望響應(yīng)碼為250。 第36-41行,處理RCPT TO指令,期望響應(yīng)碼為250。 第44-49行,處理DATA指令,期望響應(yīng)碼為354。 第51-76行,生成郵件體,并發(fā)送。 第52-56行,如果$subject不為空,則查找郵件體中是否有主題部分,如果沒有,則加上主題部分。 第59-63行,如果$from不為空,則查找郵件體中是否有發(fā)信人部分,如果沒有,則加上發(fā)信人部分。 第66-70行,如果$to不為空,則查找郵件體中是否有收信人部分,如果沒有,則加上收信人部分。 第73-74行,查找郵件體是否有了結(jié)束行,如果沒有則加上郵件體的結(jié)束行(以"."作為單獨(dú)的一行的特殊行)。 第76行,發(fā)送郵件體。 第78-83行,執(zhí)行QUIT結(jié)否與服務(wù)器的連接,期望響應(yīng)碼為250。 第85行,返回處理成功標(biāo)志(true)。 第81-91行,與服務(wù)器連接失敗的處理。
以上為整個(gè)send_mail類的實(shí)現(xiàn),應(yīng)該不是很難的。下面給出一個(gè)實(shí)例。
郵件發(fā)送實(shí)例 先給出一個(gè)最簡單的實(shí)例: -------------------------------------------------------------------------------- <? 1 include "sendmail.class.php3"; 2 $email="Hello, this is a test letter!"; 3 $sendmail=new send_mail("smtp.263.net", "limodou", true); //顯示調(diào)示信息 4 if($sendmail->send("chatme@263.net", "chatme@263.net", "test", $email)) 5 { 6 echo "發(fā)送成功!<br>"; 7 } 8 else 9 { 10 echo "發(fā)送失敗!<br>"; 11 } ?> -------------------------------------------------------------------------------- 第1行,裝入send_mail類。 第3行,創(chuàng)建一個(gè)類的實(shí)例,且設(shè)置顯示調(diào)示信息,如果不想顯示,可以 $sendmail=new send_mail("smtp.263.net");。 第4行,發(fā)送郵件。
很簡單,不是嗎?下面再給合以前的發(fā)送MIME郵件的例子,給出一個(gè)發(fā)送HTML附件的例子。
-------------------------------------------------------------------------------- <?php
include "MIME.class.php3"; //注,在發(fā)送MIME郵件一文中,這個(gè)類文件名為MIME.class,在此處我改成這樣的
$to = 'chatme@263.net'; //改為收信人的郵箱 $str = "Newsletter for ".date('M Y', time());
//信息被我改少了 $html_data = '<html><head><title>'. $str. '</title></head> <body bgcolor="#ffffff"> Hello! This is a test! </body> </html>';
//生成MIME類實(shí)例 $mime = new MIME_mail("chatme@263.net", $to, $str);
//添加HTML附件 $mime->attach($html_data, "", HTML, BASE64);
//注釋掉,采用我的發(fā)送郵件處理 //$mime->send_mail();
//生成郵件 $mime->gen_email();
//顯示郵件信息 //echo $mime->email."<br>";
//包含sendmail文件 include "sendmail.class.php3";
//創(chuàng)建實(shí)例 $sendmail=new send_mail("smtp.263.net", "limodou", true);
//發(fā)送郵件 $sendmail->send("chatme@263.net", "chatme@263.net", $str, $mime->email);
?> |