您的位置:首页技术文章
文章详情页

用PHP实现POP3邮件的解码(三)

浏览:3日期:2023-12-28 16:10:21

实现 MIME 解码的类

该类实现解码的方法是 decode($head=null,$body=null,$content_num=-1),为了处理上的方便,要求输入的是两个字符数组,在我们的上篇中,所用到的POP类所收取得到的就是两个这样的数组,一个是邮件头内容,一个是邮件的正文内容。限于篇幅,不对其做详细的说明,其实现思想跟本文上篇中所介绍的POP类类似。请参考其中的注释。

该类中用到了大量的正则表达式的操作,对此不熟悉的读者,请参考正则表达式的有关资料。

class decode_mail{var $from_name;var $to_name;var $mail_time;var $from_mail;var $to_mail;var $reply_to;var $cc_to;var $subject;

// 解码后的邮件头部分的信息:var $body;

// 解码后得到的正文数据,为一个数组。var $body_type; // 正文类型var $tem_num=0;var $get_content_num=0;var $body_temp=array();var $body_code_type;var $boundary;

// 以上是一些方法中用到的一些全局性的临时变量,由于 PHP不能做到良好的封装,所以只能放在这里定义var $err_str; // 错误信息var $debug=0; // 调试标记var $month_num=array('Jan'=>1,'Feb'=>2,'Mar'=>3,'Apr'=>4,'May'=>5,'Jun'=>6,'Jul'=>7,'Aug'=>8,'Sep'=>9,'Oct'=>10,'Nov'=>11,'Dec'=>12); // 把英文月份转换成数字表示的月份

function decode($head=null,$body=null,$content_num=-1) // 调用的主方法,$head 与 $body 是两个数组,$content_num 表示的是当正文有多个部分的时候,只取出指定部分的内容以提高效率,默认为 -1 ,表示解码全部内容,如果解码成功,该 方法返回 true{ if (!$head and !$body) { $this->err_str='没有指定邮件的头与内容!!'; return false; }if (gettype($head)=='array') { $have_decode=true; $this->decode_head($head); }if (gettype($body)=='array') { $this->get_content_num=$content_num; $this->body_temp=$body; $have_decode=true; $this->decode_body(); unset($this->body_temp); }if (!$have_decode) { $this->err_str='传递的参数不对,用法:new decode_mail(head,body) 两个参数都是数组'; return false; }}function decode_head($head) // 邮件头内容 的解码,取出邮件头中有意义的内容{ $i=0; $this->from_name=$this->to_name=$this->mail_time=$this->from_mail=$this-> to_mail=$this->reply_to=$this->cc_to=$this->subject=''; $this->body_type=$Sthis->boundary=$this->body_code_type=''; while ($head[$i]) { if (strpos($head[$i],'=?')) $head[$i]=$this->decode_mime($head[$i]);  //如果有编码的内容,则进行解码,解码函数是上文所介绍的 decode_mime() $pos=strpos($head[$i],':'); $summ=substr($head[$i],0,$pos); $content=substr($head[$i],$pos+1);  //将邮件头信息的标识与内容分开 if ($this->debug) echo $summ.':----:'.$content.'<BR>'; switch (strtoupper($summ)) { case 'FROM': // 发件人地址及姓名(可能没有姓名,只有地址信息) if ($left_tag_pos=strpos($content,'<')) { $mail_lenth=strrpos($content,'>')-$left_tag_pos-1; $this->from_name=substr($content,0,$left_tag_pos); $this->from_mail=substr($content,$left_tag_pos+1,$mail_lenth); if (trim($this->from_name)=='') $this->from_name=$this->from_mail; else if (ereg('['|']([^'']+)['|']',$this->from_name,$reg)) $this->from_name=$reg[1]; } else { $this->from_name=$content; $this->from_mail=$content; //没有发件人的邮件地址 } break; case 'TO': //收件人地址及姓名(可能 没有姓名) if ($left_tag_pos=strpos($content,'<')) { $mail_lenth=strrpos($content,'>')-$left_tag_pos-1; $this->to_name=substr($content,0,$left_tag_pos); $this->to_mail=substr($content,$left_tag_pos+1,$mail_lenth); if (trim($this->to_name)=='') $this->to_name=$this->to_mail; else if (ereg('['|']([^'']+)['|']',$this->to_name,$reg)) $this->to_name=$reg[1]; } else { $this->to_name=$content; $this->to_mail=$content; //没有分开收件人的邮件地址 } break;

case 'DATE' : //发送日期,为了处理方便,这里返回的是一个 Unix 时间戳,可以用 date('Y-m-d',$this->mail_time) 来得到一般格式的日期

$content=trim($content); $day=strtok($content,' '); $day=substr($day,0,strlen($day)-1); $date=strtok(' '); $month=$this->month_num[strtok(' ')]; $year=strtok(' '); $time=strtok(' '); $time=split(':',$time); $this->mail_time=mktime($time[0],$time[1],$time[2],$month,$date,$year); break; case 'SUBJECT':  //邮件主题 $this->subject=$content; break; case 'REPLY_TO': // 回复地址(可能没有) if (ereg('<([^>]+)>',$content,$reg)) $this->reply_to=$reg[1]; else $this->reply_to=$content; break; case 'CONTENT-TYPE': // 整个邮件的 Content类型, eregi('([^;]*);',$content,$reg); $this->body_type=trim($reg[1]); if (eregi('multipart',$content)) // 如果是 multipart 类型,取得 分隔符 { while (!eregi('boundary='(.*)'',$head[$i],$reg) and $head[$i]) $i++; $this->boundary=$reg[1]; } else //对于一般的正文类型,直接取得其编码方法 { while (!eregi('charset=['|'](.*)['|']',$head[$i],$reg)) $i++; $this->body_char_set=$reg[1]; while (!eregi('Content-Transfer-Encoding:(.*)',$head[$i],$reg)) $i++; $this->body_code_type=trim($reg[1]); } break; case 'CC': //抄送到。。 if (ereg('<([^>]+)>',$content,$reg)) $this->cc_to=$reg[1]; else $this->cc_to=$content; default: break; } // end switch $i++; } // end while if (trim($this->reply_to)=='')  //如果没有指定回复地址,则回复地址为发送人地址 $this->reply_to=$this->from_mail;}// end function define

function decode_body() //正文的解码,其中用到了不少邮件头解码所得来的信息{$i=0;if (!eregi('multipart',$this->body_type)) // 如果不是复合类型,可以直接解码 { $tem_body=implode($this->body_temp,'rn'); switch (strtolower($this->body_code_type)) // body_code_type ,正文的编码方式,由邮件头信息中取得 {case 'base64': $tem_body=base64_decode($tem_body); break;

case 'quoted-printable': $tem_body=quoted_printable_decode($tem_body); break; ; }

$this->tem_num=0; $this->body=array(); $this->body[$this->tem_num][content_id]=''; $this->body[$this->tem_num][type]=$this->body_type; switch (strtolower($this->body_type))

{ case 'text/html': $this->body[$this->tem_num][name]='超文本正文'; break; case 'text/plain': $this->body[$this->tem_num][name]='文本正文'; break; default: $this->body[$this->tem_num][name]='未知正文'; } $this->body[$this->tem_num][size]=strlen($tem_body); $this->body[$this->tem_num][content]=$tem_body; unset($tem_body); } else // 如果是复合类型的 { $this->body=array(); $this->tem_num=0; $this->decode_mult($this->body_type,$this->boundary,0);//调用复合类型的解码方法 }}

function decode_mult($type,$boundary,$begin_row) // 该方法用递归的方法实现 复合类型邮件正文的解码,邮件源文件取自于 body_temp 数组,调用时给出该复合类型的类型、分隔符及 在 body_temp 数组中的开始指针

{$i=$begin_row;$lines=count($this->body_temp);while ($i<$lines) // 这是一个部分的结束标识; { while (!eregi($boundary,$this->body_temp[$i]))//找到一个开始标识 $i++; if (eregi($boundary.'--',$this->body_temp[$i])) { return $i; }

while (!eregi('Content-Type:([^;]*);',$this->body_temp[$i],$reg ) and $this->body_temp[$i]) $i++; $sub_type=trim($reg[1]); // 取得这一个部分的 类型是milt or text .... if (eregi('multipart',$sub_type))// 该子部分又是有多个部分的; { while (!eregi('boundary='([^']*)'',$this->body_temp[$i],$reg) and $this->body_temp[$i]) $i++; $sub_boundary=$reg[1];// 子部分的分隔符; $i++; $last_row=$this->decode_mult($sub_type,$sub_boundary,$i); $i=$last_row; } else { $comm=''; while (trim($this->body_temp[$i])!='') { if (strpos($this->body_temp[$i],'=?')) $this->body_temp[$i]=$this->decode_mime($this->body_temp[$i]); if (eregi('Content-Transfer-Encoding:(.*)',$this->body_temp[$i],$reg)) $code_type=strtolower(trim($reg[1])); // 编码方式 $comm.=$this->body_temp[$i].'rn'; $i++; } // comm 是编码的说明部分

if (eregi('name=[']([^']*)[']',$comm,$reg)) $name=$reg[1];

if (eregi('Content-Disposition:(.*);',$comm,$reg)) $disp=$reg[1];

if (eregi('charset=['|'](.*)['|']',$comm,$reg)) $char_set=$reg[1];

if (eregi('Content-ID:[ ]*<(.*)>',$comm,$reg)) // 图片的标识符。 $content_id=$reg[1];

$this->body[$this->tem_num][type]=$sub_type; $this->body[$this->tem_num][content_id]=$content_id; $this->body[$this->tem_num][char_set]=$char_set; if ($name) $this->body[$this->tem_num][name]=$name; else switch (strtolower($sub_type)) { case 'text/html': $this->body[$this->tem_num][name]='超文本正文'; break;

case 'text/plain': $this->body[$this->tem_num][name]='文本正文'; break;

default: $this->body[$this->tem_num][name]='未知正文'; }   // 下一行开始取回正文 if ($this->get_content_num==-1 or $this->get_content_num==$this->tem_num) // 判断这个部分是否是需要的。-1 表示全部 { $content=''; while (!ereg($boundary,$this->body_temp[$i])) { //$content[]=$this->body_temp[$i]; $content.=$this->body_temp[$i].'rn'; $i++; }

//$content=implode('rn',$content); switch ($code_type) { case 'base64': $content=base64_decode($content); break;

case 'quoted-printable': $content=str_replace('n','rn',quoted_printable_decode($content)); break; }

$this->body[$this->tem_num][size]=strlen($content); $this->body[$this->tem_num][content]=$content; } else { while (!ereg($boundary,$this->body_temp[$i])) $i++; } $this->tem_num++; } // end else} // end while;} // end function define

function decode_mime($string) {//decode_mime 已在上文中给出,这里略过。}} // end class define

在这里要特别说明一点的是html正文里所用图片的解码。发送html格式的正文时,都会碰到图片如何传送的问题。图片在 html 文档里是一个<img src='https://www.haobala.com/bcjs/21103.html' >的标签,关键是这个源文件从何来的。很多邮件的处理方法是用一个绝对的 url 标识,就是在邮件的html正文里用<img src= http://www.ccidnet.com/image/22.gif >之类的标签,这样,在阅读邮件时,邮件阅读器(通常是用内嵌的浏览器)会自动从网上下载图片,但是如果邮件收下来之后,与 Internet 的连接断了,图片也就不能正常显示。

所以更好的方法是把图片放在邮件中一起发送出去。在 MIME 编码里,描述图片与正文的关系,除了上面所提到的multipart/related MIME头信息之外,还用到了一个 Content-ID: 的属性来使图片与 html 正文之间建立关系。html 文档中的图片在编码时,其MIME头中加入一个 Content-ID:122223443556dsdf@ntsever 之类的属性,122223443556dsdf@ntsever是一个唯一的标识,在 html 文档里,<img>标签被修改成<img src='cid: 122223443556dsdf@ntsever'>,在解码的时候,实际上,还需要把 html 正文中的这些<img src>标签进行修改,使之指向解码后的图片的具体路径。但是考虑到具体的解码程序中对图片会有不同的处理,所以在这个解码的类中,没有对 hmtl 正文中的<img>标签进行修改。所以在实际使用这个类时,对于有图片的 html 正文,还需要一定的处理。正文中的图片,可以用临时文件来保存,也可以用数据库来保存。

现在我们已经介绍了POP3 收取邮件并进行 MIME 解码的原理。下面给出一个使用这两个类的一段小程序:

<?include('pop3.inc.php');include('mime.inc.php');$host='pop.china.com';$user='boss_ch';$pass='mypassword';$rec=new pop3($host,110,2);$decoder=new decode_mail();

if (!$rec->open()) die($rec->err_str);

if (!$rec->login($user,$pass)) die($rec->err_str);

if (!$rec->stat()) die($rec->err_str);echo '共有'.$rec->messages.'封信件,共'.$rec->size.'字节大小<br>';

if ($rec->messages>0) { if (!$rec->listmail()) die($rec->err_str); echo '以下是信件内容:<br>'; for ($i=1;$i<=count($rec->mail_list);$i++) { echo '信件'.$rec->mail_list[$i][num].',大小:'.$rec->mail_list[$i][size].'<BR>'; $rec->getmail($rec->mail_list[$i][num]); $decoder->decode($rec->head,$rec->body); echo '<h3>邮件头的内容:</h3><br>'; echo $decoder->from_name.'('.$decoder->from_mail.') 于'.date('Y-m-d H:i:s',$decoder->mail_time).' 发给'.$decoder->to_name.'('.$decoder->to_mail.')'; echo 'n<br>抄送:';

if ($decoder->cc_to) echo $decoder->cc_to;else echo '无'; echo 'n<br>主题:'.$decoder->subject;

echo 'n<br>回复到:'.$decoder->reply_to;   echo '<h3>邮件正文 :</h3><BR>'; echo '正文类型:'.$decoder->body_type; echo '<br>正文各内容:'; for ($j=0;$j<count($decoder->body);$j++) { echo 'n<br>类型:'.$decoder->body[$j][type]; echo 'n<br>名称:'.$decoder->body[$j][name]; echo 'n<br>大小:'.$decoder->body[$j][size]; echo 'n<br>content_id:'.$decoder->body[$j][content_id]; echo 'n<br>正文字符集'.$decoder->body[$j][char_set]; echo '<pre>'; echo '正文内容:'.$decoder->body[$j][content]; echo '</pre>'; }$rec->dele($i); }}

$rec->close();?>

如有想要取得完整源代码的朋友,请与本人联系: boss_ch@netease.com

<全文完>

标签: PHP