理解 Zend 框架,第 6 部分: 发送电子邮件

理解 Zend 框架” 系列的前几期文章使用了 PHP Zend Framework 来创建基本的 Chomp 在线提要阅读器。现在是时候把电子邮件添加进来了。本文解释了如何使用 Zend_Mail 组件向用户发送文本及 HTML 形式的电子邮件,以提醒用户其订阅的提要增添了新内容。

Gina Deol (gdeol@binaryits.com), 副总裁,电子商务开发, Binary IT Solutions

Gina Deol 正在 Sacramento State University 攻读计算机科学与会计信息系统专业的理学学士,现在已经是最后一年。她曾是一名 Web 开发工程师兼项目协调员,目前她在位于 Sacramento 的 Binary IT Solutions 公司担任电子商务开发副总裁。



2006 年 10 月 26 日

先决条件

本系列中使用的样例应用程序是使用 PHP Zend Framework 的 V0.1.3 构建的。Zend Framework 的先决条件包括 PHP V5 和 Apache Web 服务器。详细的安装说明,请参见 第 2 部分

想要继续学习,您需要对 PHP 编程有一个基本的了解。您还需要具有 “理解 Zend 框架” 系列以前各部分中的代码。


简介

第 1 部分 中,我们探讨了 Zend 框架的全部概念,包括一系列相关类和对 MVC 模式的总体探讨。在 第 2 部分 中,我们详述了这部分内容以展示如何在 Zend 框架应用程序中实现 MVC 模式。我们还创建了用户注册和登录过程,将用户信息添加到数据库中并重新获取这些信息。

第 3 和第 4 部分探讨了实际的 RSS 和 Atom 提要。在 第 3 部分 中,我们使用户能够订阅独立的提要并显示列于这些提要中的条目。还讨论了 Zend 框架的一些表单处理功能,如验证数据和清除提要条目。而 第 4 部分,则解释了如何创建代理以从不含提要的站点中提取数据。

本系列余下的部分涉及到为 Chomp 应用程序增值。第 5 部分 解释了如何使用 Zend_PDF 模块使用户能够为保存的文章、图像和搜索结果创建一个定制的 PDF。第 6 部分(本文)中使用 Zend_Mail 模块来提醒用户有新文章。在 第 7 部分 中,将探讨搜索保存的内容并返回排列好的结果。在第 8 部分中,我们将创建自己的混合体以添加 Amazon、Flickr 和 Yahoo! 中的信息。而在第 9 部分中,我们将使用 JavaScript 对象表示法为网站添加 Ajax 交互方式。

在本文中,我们将发送两种类型的电子邮件消息。第一种是基于简单文本的电子邮件,该邮件仅仅列出用户所订阅的、在上次阅读后已更新的提要。第二种电子邮件发送一些新文章,这些文章发表于上次用户阅读某个特定的提要之后。

我们首先要更新数据库,让每一个提要都有一个精确的最近更新的值。当用户查看订阅的提要时,我们还要用最近获得的时间值更新这些提要。最后,我们要创建一个程序,让它来比较这些日期,创建电子邮件消息并发送这些消息。


电子邮件简介

电子邮件的结构是由消息头和消息体组成的。消息头包括关于消息的信息,诸如接收人、发送人、日期、消息 ID、发送人服务器 IP 地址、路由信息及多用途因特网邮件扩展(Multipurpose Internet Mail Extensions,MIME)类型。MIME 类型 将消息的类型确定为简单的文本消息、HTML 消息或带有附件的消息。消息体 仅仅是消息中的文本,可以扩充为很多行并可能带有附件。

电子邮件通过邮件传输代理、邮件发送代理及邮件用户代理发送。邮件传输代理 也叫邮件服务器 ,是用来把邮件从一台计算机发送到另一台计算机的软件。邮件传输代理从另一个邮件传输代理、邮件用户代理或邮件提交代理处接收消息。邮件发送代理 是一个计算机程序,用于接收输入的消息并将它们转发至位于远程服务器上的目的地,该服务器被称为简单邮件传输协议(Simple Mail Transfer Protocol,SMTP)服务器。如果目的帐户在本地机器上,邮件发送代理就会将消息直接分发到计划接收人的邮件箱中。邮件用户代理 是一个用于阅读和发送消息的软件,如 Mozilla Thunderbird 或 Microsoft® Outlook®。


Zend_Mail 组件

尽管 Zend 框架相对来说还不成熟,但它开发的 Zend_Mail 却具有一些非常重要的特性,如在一个 SMTP 连接上添加多个接收人或发送多个电子邮件消息的能力。Zend_Mail 也能发送 HTML 电子邮件或创建多部分的电子邮件消息,其中可提供文本或者 HTML。您也能发送具有 MIME 附件、定制邮件标题以及控制字符集的高级消息。Zend_Mail 支持 8 位、base64、二进制、可引用-可打印编码以及备选的传输方式(如 POP3 和 IMAP),同时,也能阅读接收到的电子邮件。

Zend_Mail 组件包括:

  • Zend_Mail_Message
  • Zend_Mail_Abstract
  • Zend_Mail_Mbox
  • Zend_Mail_POP3
  • Zend_Mail_Transport_POP3
  • Zend_Mail_Maildir
  • Zend_Mail_Imap
  • Zend_Mail_Transport_Imap

数据库更改

在这您需要将一个 lastpulled 列添加到 subscribedfeeds 表,并把一个 lastupdated 列添加到提要表。为此,启动 MySQL 控制台并键入下列代码:

alter table feeds add lastupdated datetime;
alter table subscribedfeeds add lastpulled datetime;

FeedController->viewChannelAction() 中的修改

FeedController->viewChannelAction() 中,您需要用当前日期更新 lastpulled 字段,从而保存当前用户最后一次查看该字段的日期。

清单 1. 更新 lastpulled 的值
<?php
    public function viewChannelAction()
    {
        $filterSession = Zend::registry('fSession');
        $username = $filterSession->getRaw('username');

        $filterGet = Zend::registry('fGet');
        $feedTitle = $filterGet->getRaw('title');

        $db = Zend::registry('db');
        $select = $db->select();
        $select->from('feeds', '*');
        $select->where('feedname=?', $feedTitle);
        $results = $db->fetchAll($select);

        $curDate = date("Y-m-d H:i:s")
        $table = 'subscribedfeeds';
        $row = array(
             'lastpulled' => $curDate
             );

        $where = "username='$username' and feedname='$feedTitle'";
        $rowsAffected = $db->update($table, $row, $where);

        $feedLink = $results[0]['link'];
        $rssFeed = Zend_Feed::import($feedLink);
        
        $view = Zend::registry('view');
        $view->title = $feedTitle;
        $view->rssFeed = $rssFeed;
        echo $view->render('viewChannel.php');
    }
?>

为更新 subscribedfeeds 表,我们需要知道登录用户的 username,所以我们要从会话中获取该信息。然后,我们为当前日期创建一个字符串,并使用它创建一个用于更新 subscribedfeeds 表的信息行。注意,用于更新的行并不必须包含表中所有的列;只包含那些将用于更新该表的列。然后我们创建这个查询(换言之,where 子句)并执行更新。


更新提要数据

下一步是要确保我们能够给出提要最近更新的日期。为此,要创建一个新的名为 sendUpdates.php 的 PHP 文件。(最简单的方式是将该文件放到 <ZEND_HOME>/library 目录下。)您的第一直觉也许是将该功能放到 IndexController 中,但最好是将其作为一个独立的文件,有两个原因:首先,我们想要使用 cron、Windows 脚本主机或类似的调度技术定期执行该文件,如一天一次。第二,将其同 Web 应用程序脱离意味着您无需担心未授权的 Web 用户执行它。

首先在数据库中搜索所有 RSS 或 Atom 类型的提要(同 HTML 页面相对而言)。

清单 2. 寻找提要
<?php
include 'Zend.php';

function __autoload($class)
{
    Zend::loadClass($class);
}

$params = array ('host' => 'localhost',
                 'username' => 'chompuser',
                 'password' => 'chomppassword',
                 'dbname'   => 'chomp');
$db = Zend_Db::factory('pdoMysql', $params);

$select = $db->select();
$select->from('feeds', '*');
$select->where("rss = 'true'");
$results = $db->fetchAll($select);
foreach($results as $row)
{
    echo $row['link']."\n";
}
?>

我们连接到数据库并创造性地执行了查询。我们输出了每一个提要的 URL。为执行这个文件,在命令行中,将路径改变至您保存该文件的目录,并键入 php sendUpdates.php

清单 3. 获取日期
<?php
include 'Zend.php';

function __autoload($class)
{
    Zend::loadClass($class);
}

$params = array ('host' => 'localhost',
                 'username' => 'chompuser',
                 'password' => 'chomppassword',
                 'dbname'   => 'chomp');
$db = Zend_Db::factory('pdoMysql', $params);

$select = $db->select();
$select->from('feeds', '*');
$select->where("rss = 'true'");
$results = $db->fetchAll($select);
foreach($results as $row)
{
    echo $row['link']."\n";
    $lastUpdated = null;

    try {
        $thisFeed = Zend_Feed::import($row['link']);
        $lastUpdated = null;

        if ($thisFeed->pubDate())
        {
            //echo "Feed last updated at ".$thisFeed->pubDate()."\n";
            $lastUpdated = $thisFeed->pubDate();
        } 
        else 
        {
            if (isset($thisFeed[0]))
            {
                $firstItem = $thisFeed[0];
                if ($firstItem->pubDate()){
                    //echo "Most recent item on
 ".$firstItem->pubDate()."\n";
                    $lastUpdated = $firstItem->pubDate();
                } 
                else 
                {
                    if ($firstItem->published())
                    {
                        //echo "Most recent atom item on
 ".$firstItem->published()."\n";
                        $lastUpdated = $firstItem->published();
                    } 
                    else 
                    {
                        //echo "No date information available.\n";
                        $lastUpdated = null;
                    }
                }
            }
        }
    } 
    catch (Zend_Feed_Exception $e) 
    {
        //echo "Exception caught importing feed: {$e->getMessage()}\n";
    }

    //echo "Last updated is ".$lastUpdated."<br />";
    if (isset($lastUpdated)){
        $lastUpdated = strtotime($lastUpdated);
        $lastUpdated = date('Y-m-d H:i:s', $lastUpdated);
    }
    $table = 'feeds';
    $updateRow = array('lastupdated' => $lastUpdated);
    $where = "link = '".$row['link']."'";
    $rowsAffected = $db->update($table, $updateRow, $where);
}
?>

首先,我们要装载实际的 XML 提要。假设我们处理的是 RSS,首先要检查提要的全部 pubDate。因为这个值是可选的,我们也许需要回过来检查最近发表的条目的 pubDate。最后,如果这是一个 Atom 提要,我们而是要请求发表日期。一旦有了这个日期,就可以将其从相应的格式中提取出来并在数据库中更新该行。


寻找已更新的提要

既然我们了解到每一个字段都已经更新,就可以向用户发送电子邮件,让用户得知他们收藏的提要已进行了更新。为此,需要比较提要表的 lastupdated 字段和 subscribedfeeds 表的 lastpulled 字段,并向用户发送含有最新提要的电子邮件消息。将下列代码添加到 sendUpdates.php 中。

清单 4. 寻找已更新的字段
...
    $table = 'feeds';
    $updateRow = array('lastupdated' => $lastUpdated);
    $where = "link = '".$row['link']."'";
    $rowsAffected = $db->update($table, $updateRow, $where);
}
$select = $db->select();
$select->from('users, subscribedfeeds, feeds', '*');
$select->where('users.Username=subscribedfeeds.username');
$select->where('feeds.feedname=subscribedfeeds.feedname');
$select->where('feeds.lastupdated > subscribedfeeds.lastpulled');
$results = $db->fetchAll($select);
foreach($results as $row)
{
    $email = $row['emailaddress'];
    $fName = $row['firstname'];
    $lName = $row['lastname'];
    $feedName = $row['feedname'];

    //send e-mail here
}
?>

在对提要表的 lastupdated 字段和 subscribedfeeds 表的 lastpulled 字段进行比较后,我们从数据库中获取数据。

现在是实际发送电子邮件消息的时候了。


发送消息

Zend_Mime 支持的 Zend_Mail 创建并发送文本或 HTML 格式的电子邮件消息。Zend_Mime 是处理多部分 MIME 消息的一个支持集。它不仅应用于 Zend_Mail,还应用于任何其他需要 MIME 支持的应用程序。Zend_Mime 拥有一套特定用于 MIME 的方法,包括:

  • boundary(),它返回 MIME 边界字符串,用于将消息的多个部分分隔
  • boundaryLine(),它返回完整的 MIME 边界行
  • encodeBase64(),它将字符串编码成 base64 编码
  • encodeQuotablePrintable(),它用可引用-可打印机制对字符串进行编码
  • isPrintable(),在给定字符串不包含不可打印字符时返回 TRUE,而在给定字符串确实包含不可打印字符时返回 FALSE
  • mimeEnd(),它返回完整的 MIME 端边界行

Zend_Mail 允许用一种简单的方式发送电子邮件,同时阻止邮件注入。Zend_Mail 消息可以通过 SMTP 发送,所以 Zend_Mail_Transport_SMTP 需要在 send() 方法被调用前用 Zend_Mail 进行创建和注册。将下列代码添加到 sendUpdates.php 中。

清单 5. 发送电子邮件消息
...
    $table = 'feeds';
    $updateRow = array('lastupdated' => $lastUpdated);
    $where = "link = '".$row['link']."'";
    $rowsAffected = $db->update($table, $updateRow, $where);
}

require_once 'Zend/Mail/Transport/Smtp.php';
$tr = new
Zend_Mail_Transport_Smtp('mail.example.com');
Zend_Mail::setDefaultTransport($tr);
$select = $db->select();
$select->from('users, subscribedfeeds, feeds', '*');
$select->where('users.Username=subscribedfeeds.username');
$select->where('feeds.feedname=subscribedfeeds.feedname');
$select->where('feeds.lastupdated > subscribedfeeds.lastpulled');
$results = $db->fetchAll($select);
foreach($results as $row)
{
    $email = $row['emailaddress'];
    $fName = $row['firstname'];
    $lName = $row['lastname'];
    $feedName = $row['feedname'];

    $mail = new Zend_Mail ();
    $mail->setFrom ('alerts@chomp.backstopmedia.com', 'Chomp! Alerts');
    $mail->addTo ($email, "$fName $lName");
    $mail->setSubject ('Your feeds have been updated');
    $mail->setBodyText ("One of your subscribed feeds, $feedName, ".
                        "has been updated.");
    $mail->send ();
}
?>

第一步是为消息设置传输方式。这让您能够指定一个与列在 PHP.ini 文件中的 SMTP 服务器不同的服务器。下一步,我们创建真实的 Zend_Mail 对象并填充初始值。在这里,我们正在设置接收者及发送者值,同时也设置主题和主体。在本例中,我们只指定了一个文本主体。我们马上就介绍发送 HTML 消息和多部分消息。

最后,我们实际地发送了消息。请注意该例程组织的方式,我们实际上为每一个更新的提要发送了一份电子邮件。很显然,这对于拥有多个提要的用户来说是很不方便的。

让我们来看一看如何给每位用户只发送一份邮件以及如何使用 HTML 功能发送独立的条目链接。


发送 HTML 电子邮件消息

更新例程的最后一步是改变这个过程,这样,在存在多个提要的情况下,其中任何的提要进行了更新,我们都只向用户发送一份电子邮件,而不是在每一个提要更新后都发给用户一份新邮件。我们也会为实际的条目本身添加链接。

首先将单个的查询变为两个查询,用 outside 查询在每个用户间进行循环。

清单 6. 按用户分解查询
...
    $where = "link = '".$row['link']."'";
    $rowsAffected = $db->update($table, $updateRow, $where);
}

require_once 'Zend/Mail/Transport/Smtp.php';
$tr = new
Zend_Mail_Transport_Smtp('mail.example.com');
Zend_Mail::setDefaultTransport($tr);

$userSelect = $db->select();
$userSelect->from('users', '*');
$userResults = $db->fetchAll($userSelect->__toString());
foreach ($userResults as $thisUserRow){

    $thisUser = $thisUserRow['username'];
    $email = $thisUserRow['emailaddress'];
    $fName = $thisUserRow['firstname'];
    $lName = $thisUserRow['lastname'];

    $select = $db->select();
    $select->from('subscribedfeeds, feeds', '*');
    $select->where("subscribedfeeds.username='$thisUser'");
    $select->where('feeds.feedname=subscribedfeeds.feedname');
    $select->where('feeds.lastupdated > subscribedfeeds.lastpulled');
    $results = $db->fetchAll($select->__toString());

    $bodytext = "";

    foreach($results as $row)
    {

        $feedName = $row['feedname'];
        $bodytext = $bodytext . "One of your subscribed feeds, " .
                    $feedName . ", "has been updated.\n";

    }

    $mail = new Zend_Mail ();
    $mail->setFrom ('alerts@chomp.backstopmedia.com', 'Chomp! Alerts');
    $mail->addTo ($email, "$fName $lName");
    $mail->setSubject ('Your feeds have been updated');
    $mail->setBodyText ($bodytext);
    $mail->send ();

}
?>

我们检索到了当前用户的所有信息及提要主键。随后我们将邮件的实际发送移动到循环体外面,而在循环体中构建一个要添加到消息体的文本字符串。脚本找到当前用户的所有提要后,就会创建电子邮件并将其发送出去。

现在我们只需要添加实际的提要条目。

清单 7. 添加提要条目
...
    $select->where('feeds.lastupdated > subscribedfeeds.lastpulled');
    $results = $db->fetchAll($select->__toString());

    $bodytext = "";
    $bodyhtml = "";

    foreach($results as $row)
    {

        $feedName = $row['feedname'];
 
        $bodytext = $bodytext . '\n' . $feedName . " has new items:\n\n";
        $bodyhtml = $bodyhtml . '<p>' . $feedName . " has new
 items:</p>";

        $thisFeed = Zend_Feed::import($row['link']);
        foreach($thisFeed as $thisItem){
           $thisItemDate = null;
           if ($thisItem->pubDate())
           {
               $thisItemDate = $thisItem->pubDate();
           } 
           else 
           {
               $thisItemDate = $thisItem->published();
           } 
           if ($thisItemDate > $row[lastpulled]){
               $bodytext = $bodytext . $row['feedname'].'\n';
               $bodyhtml = $bodyhtml . '<a
 href="'.$row['link'].'">'.$row['feedname'].'</a><br />';
           }
        }
    }

    $mail = new Zend_Mail ();
    $mail->setFrom ('alerts@chomp.backstopmedia.com', 'Chomp! Alerts');
    $mail->addTo ($email, "$fName $lName");
    $mail->setSubject ('Your feeds have been updated');
    $mail->setBodyText ($bodytext);
    $mail->setBodyHtml ($bodyhtml);

    $mail->send ();

}
?>

在本例中,我们创建了发送文本及 HTML 版本的消息,所以我们创建变量来保存这两个字符串。然后,对于每一个提要,我们导入提要,在每个条目间进行循环,从而找到在用户最后一次阅读提要后是否有新的文章发表,就像 lastpulled 列中定义的那样。如果有任何条目符合这一条件,我们就将它们添加到消息体中。在文本主体中,我们只添加名称,而对 HTML 版本,我们会添加到条目的链接。

准备好发送实际的消息时,我们像从前那样填充文本,我们也使用 setBodyHtml() 方法填充 HTML 版本。


结束语

本文探讨了如何使用 Zend_Mail 模块实现多种功能。我们用它来提醒用户接收所订阅提要中的新文章,并用 HTML 版的电子邮件将这些文章发送出去。在生产应用程序中,您很容易找到此功能的多种用途,如发送密码提醒和定期订户邮件。

在 “理解 Zend 框架” 系列的 第 7 部分 中,我们将介绍如何使用 Zend Framework 的搜索功能。


下载

描述名字大小
Part 6 source codeos-php-zend6.zip3KB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=170355
ArticleTitle=理解 Zend 框架,第 6 部分: 发送电子邮件
publish-date=10262006