在PHP中發送MIME郵件

來源:互聯網
上載者:User

  綜述:編寫郵件系統或郵件清單程式是PHP應用的一個大的分支,既管PHP提供了簡單的用於發email的函數,但在實際應用中,會涉及到發送帶附件的郵件、測試使用者輸入的email地址的有效性,尤有必要用專門的章節來講述。

  MIME是什嗎?

  MIME表示多用途Internet郵件擴允協議。MIME擴允了基本的面向文本的Internet郵件系統,以便可以在訊息中包含二進位附件。

  RFC822在訊息體的內容中做了一點限制:就是只能使用簡單的ASCII文本。所以,MIME資訊由正常的Internet文本郵件組成,文本郵件擁有一些特別的符合RFC822的資訊頭和格式化過的資訊體(用ASCII的子集來表示的附件)。這些MIME頭給出了一種在郵件中表示附件的特別的方法。

  MIME資訊包含了哪些東西?

  一個普通的文本郵件的資訊包含一個頭部分(To: From: Subject: 等等)和一個體部分(Hello Mr.,等等)。在一個符合MIME的資訊中,郵件的各個部分叫做MIME段,每段前也綴以一個特別的頭。MIME郵件只是基於RFC 822郵件的一個擴充。然而它有著自已的RFC規範集。

  頭欄位

  MIME頭根據在郵件包中的位置,大體上分為MIME資訊頭和MIME段頭,MIME資訊頭指整個郵件的頭,而MIME段頭只每個MIME段的頭。

  MIME資訊頭有:

  MIME-Version:
  這個頭提供了所用MIME的版本號碼。這個值習慣上為1.0。

  Content-Type:
  它定義了資料的類型,以便資料能被適當的處理。有效類型有:text,image, audio,video,applications,multipart和message。注意任何一個二進位附件都應該被叫做application/octet-stream。這個頭的一些用例為:image/jpg, application/mswork,multipart/mixed 。

  Content-Transfer-Encoding:
  它說明了對資料所執行的編碼方式,客戶/MUA將用它對附件進行解碼。對於每個附件,可以使用7bit,8bit,binary ,quoted-printable,base64和custom中的一種編碼方式。7bit編碼是用在US ASCII字元集上的常用的一種編碼方式。8bit 和binary編碼一般不用。對可讀的標準文本,如果傳輸要經過對格式有影響的網關時對其進行保護,可以使用quoted printable 。Base64是一種通用方法,在需要決定使用哪一種編碼方法時,它提供了一個不用費腦子的選擇;它通常用在二進位,非文本資料上。注意,任何非7bit 資料必須用一種模式編碼,這樣它就可以通過Internet郵件網關。

  Content-ID:
  如果Content-Type是message/external-body或multipart/alternative時,這個頭就有用了。

  Content-Description:
  這是一個可選的頭。它是任何資訊段內容的自由文本描述。描述必須使用us-ascii碼。

  Content-Disposition:
  這是一個實驗性的頭,它用於給客戶程式/MUA提供提示,來決定是否在行內顯示附件或作為單獨的附件。
  MIME段頭(出現在實際的MIME附件部分的頭),除了MIME-Version頭,可以擁有以上任何頭欄位。如果一個MIME頭是資訊塊的一部分,它將作用於整個資訊體。例如,如果Content-Transfer-Encoding顯示在資訊(整個資訊)頭中,它應用於整個資訊體,但是如果它顯示在一個MIME段裡,它"只能"用於那個段中。

  如何建立符合MIME的資訊?

  最簡單的MIME資訊

  這個資訊沒有任何段,即沒有附件。但是它有必要的頭。

  From: php@php.net
  To: 'Alex (the Great)' <alex@greece.net
  Subject: Bucephalus
  MIME-Version: 1.0
  Hello Alexander,
  How's Bucephalus doing? 

  這隻是一個簡單的擁有MIME頭的符合RFC-822 的資訊(文本郵件)。需要注意的是Content-Type頭預設為Content-Type: text/plain;charset='us-ascii'。

  下面是一個更為複雜的例子:

  From: 'Alex (the Great)' <alex@greece.net
  To: php@php.net
  Subject: re: Bucephalus
  MIME-Version: 1.0
  Content-Type: image/jpg;
  name='buce.jpg'
  Content-Transfer-Encoding: base64
  Content-Description: Take a look at him yourself

<……base64 encoded jpg image of Bucephalus……> 

  如果想發送多個附件,並且類型也不統一,怎麼辦?這就是我們將要討論的"多部分資訊"。

  多部分資訊(Multipart Messages)

  這個概念允許在一封郵件中發送多條項目。例如,假設Alexander想要給php@php.net發送一封他的馬的照片的郵件,同時還附帶有馬的家族圖譜及精彩的說明。這樣一個簡單的要求沒有多部分訊息的概念是無法被滿足的。在這種情況下,我們建立了一個使用Content-Type的資訊頭的封裝來支援郵件的不同部分,以便收信人得到圖片,家族圖譜和精彩的說明。

  Content-Type 頭現在擁有一個"multipart"的值,它表示這是一個完整的郵件資訊並且這個頭只封裝了資訊。而且它還有一個"mixed"的子類型(例如圖片和文字檔是不同的類型)。

  讓我們看一下:

  From: 'Alex (the Great)' <alex@greece.net
  To: php@php.net
  Subject: re: Bucephalus
  MIME-Version: 1.0
  Content-Type: multipart/mixed;
  boundary="XX-1234DED00099A";
  Content-Transfer-Encoding: 7bit

  This is a MIME Encoded Message

  --XX-1234DED00099A
  Content-Type: text/plain; charset=us-ascii
  Content-Transfer-Encoding: 7bit

  Hi PHP,

  Attached you will find my horse, Bucephalus', pedigree chart and photo

  Alex

  --XX-1234DED00099A
  Content-Type: image/jpg;
  name="buce.jpg";
  Content-Transfer-Encoding: base64
  Content-Description: "A photo of Bucephalus"

  <.....base64 encoded jpg image of Bucephalus...>

  --XX-1234DED00099A
  Content-Type: application/octet-stream;
  name="pedigree.doc"
  Content-Transfer-Encoding: base64
  Content-Description: "Pedigree Chart of the great horse"

  <.....base64 encoded doc (pedigree.doc) of Bucephalus...>
--XX-1234DED00099A-- 
 讓我們來看一下其中各個部分的含義:
  1)、在MIME資訊頭中的Content-Transfer-Encoding,為"7bit"。因為Content-Type為multipart/mixed,編碼應該是7bit,8bit或二進位中的一種, 7bit是一種廣泛使用的格式。
  2)、象這樣一條資訊包含了多種資訊。客戶程式是如何知道JPG圖片,文檔和普通文本之間的區別呢?在Content-Type後面有一個boundary="XX-1234DED00099A"參數。這個值用來分離郵件中的不同部分。它叫做MIME邊界標記。邊界標記的值必須儘可能的唯一,以免在超出郵件範圍時發生混亂。
  3)、"警告"資訊("This is a MIME Encoded Message")在那裡是為了讓不符合MIME的客戶程式能夠把它顯示給使用者,否則他們就不理解一個空白郵件是什麼意思。
  4)、現在,回到邊界標記。如果你觀察這個簡單的郵件,會發現邊界標記(XX-1234DED00099A在每一個分都出現了,也就是,在每部分之間都使用了一個邊界標記,然而,每個邊界標記都以兩個串連符開始。很重要的一點需要注意的就是在最後一個MIME段的後面,邊界標記不僅僅以那兩個邊接符作為開始,同時也以它倆作為結束。這一點一定不能忘記,因為它定義了郵件的範圍。
  5)、讓我們看一下前兩個MIME段:
  第一段是普通文本資訊,因此Content-Type為text/plain,並且編碼為7bit(我們也可以省略它,因為如果不指明它也會預設為如此)。
  第二個就是JPEG圖片。相應的表示為Content-Type: image/jpg。name="buce.jpg"(出現在Content-Type的後面,稱之為參數),指出了檔案的名字;它就是可以在客戶程式中看到的附件的名字。如果不給出name="buce.jpg" ,描述欄位(如果給出)將作為附件的名字顯示出來 。
  6)、注意JPEG 圖片可以在郵件件中被顯示出來,如果客戶程式可以顯示行內附件。或者,你可以向客戶程指明你想如何顯示附件。例如,如果存在Content-Disposition: attachment頭,JPEG圖片將被顯示為一個附件表徵圖。

  如何建立和實現MIME郵件類?

  現在讓我們用PHP建立和實現一個MIME郵件類。這個MIME類必須能夠:
  1、增加附件
  2、對每一個獨立的請求,對所附的資料進行編碼
  3、建立MIME段/頭
  4、產生一個包含MIME段/頭的完整的郵件
  5、將整個郵件作為字串返回
  6、用本地的郵件處理常式進行發送(或選擇調用一個SMTP郵件處理常式)

  這個類叫做MIME_mail。

<?php
class MIME_mail {
var $to;
var $from;
var $subject;
var $body;
var $headers = "";
var $errstr="";

var $base64_func= ''; // 如果未指定使用PHP的base64函數
var $qp_func = ''; // 此時為空白
var $mailer = ""; // 將其設為有效郵件對象的名字
?> 

  這裡有一些公用處理的變數(也就是,可以在指令碼中直接操縱的變數)。這些變數中的大部分都是自說明的。$headers包含了可選的想要發送給郵件處理常式的頭資訊。$errstr 是一個包含可讀錯誤字串的變數,它可以用在呼叫指令碼中。

  $base64_func和$qp_func是"函數處理器",使用者可以進行定製。預設地,它們被設為空白串。對於$base64_func,一個空串意味著我們將使用PHP內建的base64_encode()函數 。Quoted Printable可以通過$qp_func被處理。在PHP中沒有內建的quoted-printable 編碼函數(然而,安裝了imap則可以使用imap_qprint())。在這篇文章中我們將不再討論quoted_printable方法。

<?php
//私人:
var $mimeparts = array();
?>

  $mimeparts是一個內部數組,包含了郵件資訊中各自獨立的符合MIME段。請不要在這個類(或衍生類別)之外操縱它和其它的私人方法/變數。

<?php
// 建構函式
function MIME_mail($from="", $to="", $subject="", $body="", $headers = "")
{
$this->to = $to;
$this->from = $from;
$this->subject = $subject;
$this->body = $body;
if (is_array($headers)) {
if (sizeof($headers)>1) $headers=join(CRLF, $headers);
else $headers=$headers[0];
}
if ($from) {
$headers = preg_replace("!(from: ?.+?[ ]? )!i", '', $headers);
}
$this->headers = chop($headers);
$this->mimeparts[] = "" ; //增加位置0
return;
}
?> 

  我們擁有對象的建構函式,它使用"from"和"to"郵件地址,主題和郵件體和頭作為參數。對於郵件體部分,可以給出你將可能輸入的正常郵件。最後一個參數是可選的(使用者自訂)頭。例如,X-Mailer: MyMailer_1.0。請注意$headers可以是一個數組,包含了將要發給郵件發送程式的不同的頭,或者只是某個特別頭的容器。你不能在$headers參數中發送From: 頭,如果它被找到,這部分將自動被去掉。你可以象下面使用多個頭:array("X-Mailer: MYMailer_1.0", "X-Organization: PHPBuilder.com")。

  $mimeparts用一個空項(索引0)建立,在後面我們將看到這樣用的道理。

  我們將MIME資訊頭的產生,MIME段頭的產生和最終的郵件訊息的產生分成幾個模組。方法的實現是直接從我們前面遇到的MIME基礎而來的。

<?php
function attach($data, $description = "", $contenttype = OCTET, $encoding = BASE64, $disp = '') {
if (empty($data)) return 0;
if (trim($contenttype) == '') $contenttype = OCTET ;
if (trim($encoding) == '') $encoding = BASE64;
if ($encoding == BIT7) $emsg = $data;
elseif ($encoding == QP) $emsg = $$this->qp_func($data);
elseif ($encoding == BASE64) {
if (!$this->base64_func) $emsg = base64_encode($data);
else $emsg = $$this->base64_func($data);
}
$emsg = chunk_split($emsg);
//檢查是否content-type是text/plain並且如果沒有指定charset,追加預設的CHARSET
if (preg_match("!^".TEXT."!i", $contenttype) && !preg_match("!;charset=!i", $contenttype)) $contenttype .= ";
charset=".CHARSET ;
$msg = sprintf("Content-Type: %sContent-Transfer-Encoding: %s%s%s%s",
$contenttype.CRLF, $encoding.CRLF,
((($description) && (BODY != $description))?"Content-Description: $description".CRLF:""),
($disp?"Content-Disposition: $disp".CRLF:""),
CRLF.$emsg.CRLF);
BODY==$description? $this->mimeparts[0] = $msg: $this->mimeparts[]= $msg ;
return sizeof($this->mimeparts);
}
?> 

我們來仔細地分析一下這個方法:

  1、這個方法使用的參數有:

  所附的實際資料($data)
  與Content-Description頭相應的資料描述($description)
  將用在Content-Type頭中的資料content-type值($contentype)
  用在Content-Transfer-Encoding中的編碼值($encoding)
  用在Content-Disposition頭$disp中的布局值,可以是INLINE或ATTACH,兩個都是常量
  2、如BASE64,TEXT這樣的值等等,作為常量被定義在附加的.def檔案中。
  3、使用$encoding值來決定需要用哪種編碼方式對資料進行編碼。有效值是BIT7(或7bit),QP或BASE64。這個函數同時也檢查了是否使用者要使用他/她自已的BASE64或QP函數。在寫這篇文章時,在我們的類中只有BIT7和BASE64被實現了,然而,你可以傳遞你自已的quoted-printable 函數來使用,通過在前面討論的$qp_func類變數。
  4、在編碼處理之後,可以注意到對編碼的資訊使用了chunk_split()。這個函數根據可選長度將字串分割成小段。因為我們沒有指出長度,預設長度使用76。
  5、接著,如果$contenttype參數包含text/plain,則必須給出"charset=" 參數的值。它的預設值被定義在常量CHARSET中,值為us-ascii。注意當頭使用參數值傳遞時,在頭與參數之間必須有一個分號";"。
例如,Content-Type: text/plain; charset=us-ascii
  6、如果其它MIME段頭各自的值被傳遞給這個方法,這些段頭被建立。畢竟我們不想擁有一個沒有描述的Content-Description頭。在建立這些頭之後,我們追加上經過編碼的資料部分資訊。(檢查一下方法中的sprintf()語句)。同樣,注意我們使用了一個叫BODY(又是一個常量)的特別描述欄位。這就是我們用在類實現中的東西。如果描述欄位與BODY一樣,我們將其賦給$mimeheaders數組中的第一個元素。對於這個請多讀幾遍。
  7、attach() 返回$mimeparts數組的當前大小,用在呼叫指令碼的引用中。通過這種方法就可以知道一個附件"X"存在哪一個索引中(實際返回的值要比在數組中的索引小1)
  8、注意所有的頭必須用一個CRLF()序列結束。

  接著,我們看一下fattach()方法,fattach()與attach()相似,但是它使用一個檔案名稱作為它的第一個參數(作為attach()中$data的替換)。這個方法只是一個封裝,以便調用者可以用一個檔案來調用fattach。fattach()然後將檔案讀出,接著調用attach()來追加資料。這個方法在失敗時返回0,可以在$errstr 變數中找到解釋或者當成功時,返迴文件附件在$mimeparts數組中的索引號。


 我們現在已經開發了附加資料的功能,對它們進行編碼並且將單獨的MIME段放在私人數組中。還需要完成的工作是:

*.完成MIME的各個段

*.建立包含MIME資訊頭的郵件資訊頭,郵件原始的資訊頭(如To:, From:等等)並且包括任何使用者定義的頭。在頭後面追加完整的MIME段,這樣一個完整的郵件包就產生了。

  我們將考查的下一個方法是,build_message(),它是通過一個gen_email()的方法來調用的。請注意build_message()是一個私人方法。

<?php
function build_message() {
……
//情況1:存在附件列表,所以MIME資訊頭必須是multipart/mixed
if (is_array($this->mimeparts) && ($nparts > 1)) {
……
// 如果存在MIMIE段,則郵件體也要變成附件
if (!empty($this->body)) {
$this->attach($this->body, BODY, TEXT, BIT7);
}

// 現在建立郵件的各個MIME段
for ($i=0 ; $i < $nparts; $i++) {
if (!empty($this->mimeparts[$i]))
$msg .= CRLF.'--'.$boundary.CRLF.$this->mimeparts[$i]. CRLF;
}
$msg .= '--'.$boundary.'--'.CRLF;
$msg = $c_ver.$c_type.$c_enc.$c_desc.$warning.$msg;
} else {
……
}
return $msg;
}
?> 

  1、我們知道了每一個MIME段都有一個邊界標記,這個標記有一個唯一的id。邊界標記被用在:

  MIME資訊頭中,用來指示附件必須從哪進行劃分

  MIME段中;實際用在每一段的前面和後面來劃分附件的邊界(記住:最後一個邊界標記要以兩個串連符(--)結束,用於指示範圍結束)。 $boundary包含了邊界標記,並且它是通過一個隨機數進行了唯一化再做MD5雜湊產生的。另外,我們給$boundary冠以一個"PM?"的首碼,這裡"?"是一個隨機字母。舉一個boundary的例子就是"PMK------2345ee5de0052eba4daf47287953d37e"(PM表示PHP MIME,所以你可以將其改為你的可能的初始值)。

  2、在產生MIME頭的處理中我們必須考慮兩種情況。這些情況影響了郵件的原始郵件體($body在建構函式中)以哪種方式被看待和MIME資訊頭的特別表示。情況1就是:可以有許多的附件被包含。在這種情況下,請注意作為資訊的部分被放上了警告字串"This is a MIME encoding message"。因此,真正的訊息體本身也必須以附件形式加到資訊中!郵件的文本通常是附件列表中的第一個附件,在我們的例子中就是$mimeparts。這個正好就是為什麼我們要佔用一個$mimeparts索引的原因,以便讓第一個索引(是0)可以用於郵件文本部分。郵件體必須以7bit編碼進行附加。

<?php
if (!empty($this->body)) {
$this->attach($this->body, BODY, TEXT, BIT7);
}
?> 

  上面的一小段程式碼完成附加郵件文本部分作為一個MIME附件的工作。注意,我們使用了'BODY'常量來指示attach()要將附件加到何處。

  第二種情況就是當不存在附件時,在這種情況下,如果提供了郵件文本,它將是包含在局部變數$msg中的唯一資訊;在這種情況下不需要MIME頭。(然而,在這種情況下我們還應該只把MIME-Version頭指定出來)

  3、MIME資訊頭(MIME-Version,Content-Type, 等等。)在有附件的時候被建立。為了用MIME訊息頭來建立訊息體,首先MIME資訊頭要被建立。然後各個有效MIME段通過$mimeheaders數組被反覆處理。這就是邊界標識被實際使用的地點。根據規則的一致性,對一個MIME段被首碼上兩個串連符('--'.$BOUNDARY.crlf)並且在最後一個MIME段的後面,在邊界標識後追加兩個串連符表示郵件範圍結束。

  4、在變數$msg中的完整的資訊作為這個方法的值被返回。

  下一個方法,get_email()通過build_message()方法完成MIME訊息的產生。因為build_message()是一個內部方法,get_email()在調用完build_message()之後,建立RFC 822的資訊頭並且追加上MIME資訊。

<?php
function gen_email($force=false) {
if (!empty($this->email) && !$force) return $this->email ; // saves processing
$email = "";
if (empty($this->subject)) $this->subject = NOSUBJECT;
if (!empty($this->from)) $email .= 'From: '.$this->from.CRLF;
if (!empty($this->headers)) $email .= $this->headers.CRLF;
$email .= $this->build_message();
$this->email = $email;
return $this->email;
}
?> 

  對於我們的類的一個執行個體來說,類的成員$email擁有產生的整個郵件資訊。為了避免資訊被無必要的重建,這個方法繼續建立郵件標頭,並且只有當$mail為空白時才調用build_message()。然而,你可以通過調用gen_email()來強制重新處理。(如果"To"資訊被改變或加入了一個新的附件,調用者顯示想這麼做)。

  gen_email()建立了更熟悉的From頭。另外,如果沒有指定主題,它將主題設為預設值(No Subject)。我們直到後面才將To和Subject 的內含儲存起來。這個方法返回完整的郵件資訊,這樣就結束了建立MIME資訊的任務。

值得說明的其它兩個方法是print_mail()和send_mail(),兩個都使用了$force參數。print_mail()輸出整個郵件資訊,send_mail()使用PHP的mail()函數發送資訊。可選的,send_mail()使用了一個SMTP對象和它的發送方法(由使用者指定)來發送郵件。

  如何測試email的有效性?

一般我們常希望拜訪自己網站的朋友能留下Email,但是很多人都會隨便打,造成管理員的困擾,以下這個類可以線上檢查Email是否有效(存不存在)

……
Function VerifyRule($email) ;
Function VerifyOnline($email) ;

function Verify($email,$type=0) {
if($type==0) return $this->VerifyRule($email) ; //為0隻檢查Email文法
else return $this->VerifyOnline($email) ; //否則線上檢查
}
…… 

  基本思想是:

  首先檢查是否符合Email文法標準,如果符合,取得使用者輸入的Email的主機資訊,用getmxrr()函數取得改主機dns中的MX欄位,然後進一步檢查輸入的Email是否存在。

  用法:

$input=new CEmail;
/檢查文法
if($input->Verify("yourname@emailhost.com",0)) echo "有效";
else echo "無效";

//線上檢查是否真的有該郵件
if($m->Verify("yourname@emailhost.com",1)) echo "有效";
else echo "無效"; 



相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.