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

  •   2009-08-01/08:14
  • 实现 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,"\r\n");
    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]."\r\n";
    $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]."\r\n";
    $i++;
    }

    //$content=implode("\r\n",$content);
    switch ($code_type)
    {
    case "base64":
    $content=base64_decode($content);
    break;

    case "quoted-printable":
    $content=str_replace("\n","\r\n",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="" >的标签,关键是这个源文件从何来的。很多邮件的处理方法是用一个绝对的 url 标识,就是在邮件的html正文里用<img src= >之类的标签,这样,在阅读邮件时,邮件阅读器(通常是用内嵌的浏览器)会自动从网上下载图片,但是如果邮件收下来之后,与 Internet 的连接断了,图片也就不能正常显示。

    所以更好的方法是把图片放在邮件中一起发送出去。在 MIME 编码里,描述图片与正文的关系,除了上面所提到的multipart/relatedMIME头信息之外,还用到了一个 Content-ID: 的属性来使图片与 html 正文之间建立关系。html 文档中的图片在编码时,其MIME头中加入一个 Content-ID:122223443556dsdf@ntsever 之类的属性,是一个唯一的标识,在 html 文档里,<img>标签被修改成<img src="cid: ">,在解码的时候,实际上,还需要把 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();
    ?>

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

    <全文完>


    评论 {{userinfo.comments}}

    {{money}}

    {{question.question}}

    A {{question.A}}
    B {{question.B}}
    C {{question.C}}
    D {{question.D}}
    提交

    驱动号 更多