从Zend Framework应用程序发送电子邮件给数百个收件人的最佳方法是什么?

16

我想要为我的应用程序实现一个邮件列表系统。目前,我使用Zend_Mail_Transport_Smtp('localhost')作为我的传输方式,循环遍历我的订阅者列表,并向每个订阅者发送一封新的Zend_Mail。然而,我发现随着订阅者数量的增加,脚本完成所需的时间也会增加。

我相信肯定有更专业的方法来完成这个任务,涉及到电子邮件队列的使用。我认为理想的方法是:用户填写表单,点击发送,立即收到回复,说明正在发送邮件,而不必等待数百封电子邮件发送完毕。

我知道Zend_Mail不提供任何类型的邮件排队功能。有经验的人能否给我概述一下如何完成这项任务?我对cron/crontab/cronjobs一无所知,如果其中涉及,请解释清楚过程。

8个回答

20

注意:当我第一次读到你的问题时,我以为它说的是一次发送数十万封电子邮件。后来我再次确认,发现实际上是数百到数千封。我懒得现在修改我的帖子,所以这里有一些注意事项:根据我的经验,你可以不用商业工具运行大约40K个电子邮件。在大约10K时,你需要遵循“最小”清单,以防止在达到更大的列表大小时出现严重问题。但我建议立即全部实施。

我曾经说过,在发送电子邮件方面有两个方面:

  1. 技术方面--基本上是所有涉及SMTP协议、电子邮件格式、DNS记录等的RFC文件。这有点复杂,但是可以解决。
  2. 神奇的一面--电子邮件传递管理是巫术。你会感到沮丧,事情会突然崩溃,并且你会考虑离开与电子邮件无关的工作。

我建议不要编写自己的批量发送程序。我相信PHP可以做得很好,但你应该把时间花在其他地方。我过去使用并推荐的两种产品是Strongmail和PowerMTA。请注意,它们的价格很高,但我几乎可以保证,从长远来看,你会花费更多的时间和精力来构建自己的解决方案。

编写自己的PHP程序时,一个容易被卡住的地方是限速/延迟发送。邮件服务器将在你发送了一些消息后添加sleep(30)以减缓你的发送速度并防止你进行垃圾邮件攻击。

通常,这些商业批量发送程序运行SMTP协议进行排队。你将继续使用Zend_Mail,但需要将其硬编码连接到你的服务器上。它会将邮件队列几乎与你发送邮件的速度一样快地排队,然后使用自己的引擎将邮件发送到目标地址。

在100K个列表中,你需要采用电子邮件最佳实践。至少,你需要:

  • SPF记录,可能还需要DKIM
  • 使用多个IP地址来划分流量 - 拥有3个IP地址,一个用于可信赖的高质量地址,一个用于中风险的IP地址,另一个用于高风险的IP地址。此设计有助于最小化发送邮件到最佳客户的风险。
  • 为发送IP地址设置正确的反向DNS
  • 利用AOL、hotmail、yahoo等反馈回路来处理垃圾邮件投诉
  • 退订和退回管理 - 确保您修剪这些地址
  • 也很重要的是打开/点击跟踪 - 如果您的A类客户没有打开您的电子邮件,则需要将其降级为B类客户等。这很重要,因为ISP会将不活动的帐户变成蜜罐。Hotmail以此闻名。

最后,如果您真的认真对待发送电子邮件,您还需要其他工具,如Return Path。


你很懂行。不幸的是,你的回答有些令人不知所措。如何使用Strongmail或PowerMTA与Zend Mail,并涵盖您列出的所有要点? - rick
您可以将应用程序与Strongmail绑定,当我说应用程序时,我指的是数据存储,这意味着您不需要实现任何代码行,只需与Strongmail服务器共享用户详细信息并发送数百万封电子邮件即可 :) - vaske

20
为了可靠地使用PHP发送大量电子邮件,您必须使用队列机制。如其他人建议的那样,使用队列的过程如下所示:
- 循环遍历您的用户集,为每个用户创建一封电子邮件,并可能自定义内容 - 将每个邮件对象传递给队列,该队列将延迟发送电子邮件直到稍后 - 在某种定期脚本中,每次发送几百个队列的内容。注意:您需要通过查看从实际发送过程返回的错误日志来调整您正在发送的电子邮件数量。如果您试图发送太多电子邮件,我注意到会达到一个点,邮件传输将不再接受连接(我正在使用qmail)
有一些库可以用于此,PEAR Mail Queue(带有Mail_Mime)和SwiftMailer都允许您创建和排队电子邮件。到目前为止,Zend Mail仅提供电子邮件的创建,而不是排队(稍后会详细介绍)。
我主要使用PEAR Mail Queue,有一些需要注意的地方。如果您正在尝试排队大量电子邮件(例如,循环遍历20,000个用户并尝试在合理的时间内将它们放入队列中),则使用Mail Mime的引用打印编码实现非常缓慢。您可以通过切换到base64编码来加快此过程。
关于Zend Mail,您可以编写一个Zend Mail Transport对象,将您的Zend Mail对象放入PEAR Mail队列中。我已经成功地做到了这一点,但需要一些尝试才能做到正确。要做到这一点,请扩展Zend Mail Transport Abstract,实现_sendMail方法(这是您将Zend Mail对象放入Mail Queue的位置),并将您的传输对象实例传递给Zend Mail对象的send()方法或通过Zend Mail ::setDefaultTransport()。
底线是有很多方法可以做到这一点,但需要您进行一些研究和学习。不过,这是一个非常可解决的问题。

3
使用Zend_Queue将电子邮件放入队列中,以进行异步后台处理。您需要一个cron作业来在后台处理队列。
protected function _enqueueEmail(WikiEmailArticle $email)
{
    static $intialized = false; 

    if (!$initialized) {

        $this->_initializeMailQueue("wikiappwork_queue");
        $initialized = true;
    }

    $this->_mailQueue->send(serialize($email));  
}
protected function _initializeMailQueue()
{
    /* See: 1.) http://framework.zend.com/manual/en/zend.queue.adapters.html and
     *      2.) Zend/Queue/Adapter/Db/mysql.sql. 
     */

 $ini = Zend_Controller_Front::getInstance()->getParam('bootstrap')
                                            ->getOptions(); 

     $queueAdapterOptions =    array( 'driverOptions' => array(
    'host' => $ini['resources']['multidb']['zqueue']['host'],
    'username' => $ini['resources']['multidb']['zqueue']['username'],
    'password' => $ini['resources']['multidb']['zqueue']['password'],
    'dbname' => $ini['resources']['multidb']['zqueue']['dbname'],
    'type' => $ini['resources']['multidb']['zqueue']['adapter'] ),
    'name' => $ini['resources']['multidb']['zqueue']['queueName'] );

    $this->_mailQueue = new Zend_Queue('Db', $queueAdapterOptions);

 }

对于Cron任务,可以使用如下脚本:

<?php
use \Wiki\Email\WikiEmailArticle;

// Change this define to correspond to the location of the wikiapp.work/libary
define('APPLICATION_PATH', '/home/kurt/public_html/wikiapp.work/application');

set_include_path(implode(PATH_SEPARATOR, array(
     APPLICATION_PATH . '/../library',
     get_include_path(),
 )));

// autoloader (uses closure) for loading both WikiXXX classes and Zend_ classes.
spl_autoload_register(function ($className) { 

  // Zend classes need underscore converted to PATH_SEPARATOR
  if (strpos($className, 'Zend_' ) === 0) {

        $className = str_replace('_', '/', $className );   
  }

  $file = str_replace('\\', '/', $className . '.php');

  // search include path for the file.
  $include_dirs = explode(PATH_SEPARATOR, get_include_path());

  foreach($include_dirs as $dir) {

    $full_file = $dir . '/'. $file;

    if (file_exists($full_file)) { 

        require_once $full_file; 
        return true; 
    }
  }

  return false; 
 }); 

// Load and parese ini file, grabing sections we need.
$ini = new Zend_Config_Ini(APPLICATION_PATH . 
                          '/configs/application.ini', 'production');

$queue_config = $ini->resources->multidb->zqueue;

$smtp_config = $ini->email->smtp;

$queueAdapterOptions =  array( 'driverOptions' => array(
                                        'host'      => $queue_config->host,
                    'username'  => $queue_config->username,
                    'password'  => $queue_config->password,
                    'dbname'    => $queue_config->dbname,
                    'type'      => $queue_config->adapter),
                'name' => $queue_config->queuename);

$queue = new Zend_Queue('Db', $queueAdapterOptions);


$smtp = new Zend_Mail_Transport_Smtp($smtp_config->server, array(
                'auth'      => $smtp_config->auth,
        'username'  => $smtp_config->username,
        'password'  => $smtp_config->password,
        'port'      => $smtp_config->port,
        'ssl'       => $smtp_config->ssl
        ));

Zend_Mail::setDefaultTransport($smtp);

$messages = $queue->receive(10); 

foreach($messages as $message) {

        // new WikiEmailArticle.     
    $email = unserialize($message->body);

        try {

            $email->send();

        }  catch(Zend_Mail_Exception $e) {

               // Log the error?
               $msg = $e->getMessage();
               $str = $e->__toString();
               $trace =  preg_replace('/(\d\d?\.)/', '\1\r', $str);
        } // end try

$queue->deleteMessage($message);

} // end foreach

3

来自PHP.net文档。

注意:值得注意的是,mail()函数不适用于循环中的大量电子邮件。此函数为每个电子邮件打开和关闭SMTP套接字,效率不高。
对于发送大量电子邮件,请参见»PEAR::Mail和»PEAR::Mail_Queue包。

Zend Mail类可能非常好(Zend的大部分东西都很好),但如果您想要其他选项,这里有一些。


我的经验是 PEAR::Mail 很慢。PHPMailer 和 swiftmailer 非常出色。 - rick

2
您可以在发送邮件的数量不超过数千个时使用PHP,但是要避免使用mail()函数,正如其他人所指出的那样。我见过一些针对大量邮件(10万个以上收件人)设计的系统,它们会绕过标准邮件功能,试图更直接地与MTA进行交互。即使如此,我仍然不清楚是否需要这样做。
让电子邮件专业化更多地涉及到确保格式良好(尽可能使用HTML和纯文本),人们可以轻松地取消订阅,正确处理退信,邮件服务器具有所有正确的DNS记录,并且服务器配置不违反任何主要黑名单系统的规则。您编写应用程序时使用的语言在几百甚至几千条消息中并不是一个主要因素。

2
我在php中实现了一个批量邮件发送程序,其中每封电子邮件都是根据个人情况定制的。这并不难,也没有花太长时间。我使用了swiftmailer和cron。Zend Mail也可能可以。我最初使用了PEAR邮件队列,但排队电子邮件的速度太慢了。
排队电子邮件的过程如下:
1. 创建电子邮件模板并添加占位符(或使用模板引擎)以替换唯一内容的区域。 2. 在循环中,用任何唯一内容替换占位符,将结果电子邮件内容、主题、地址、批次ID和可选的优先级值插入到数据库表中。
我使用了cron作业来发送批量电子邮件。cron时间间隔和每批发送的电子邮件数量非常重要,因为我在共享主机上有限制。由cron调用的脚本只能由cron访问。该脚本从按批次ID和可选优先级排序的表中读取x封电子邮件。如果电子邮件成功发送,则从数据库队列中删除它。如果无法发送电子邮件,则它将保留在队列中,并为该记录递增计数器。如果计数器超过设定值,则从队列中删除该电子邮件。

加速使用PEAR_Mail排队的几个有用技巧是将*_seq设置为Mysql中的MEMORY表。我还将其每100个条目包装在一个事务中。 - Alister Bulman

0

Zend Mail类看起来不错,使用起来也很简单,它还允许您发送电子邮件的纯文本和HTML版本,在电子邮件营销中非常重要。

如果您熟悉这个框架,我建议您继续使用它。

发送大量电子邮件时需要考虑以下重要事项:

  • 当电子邮件被打开并且有人访问您的网站时,您的Web服务器能否处理图像请求的负载。

如果答案是否定的或者您不确定,使用Apache基准测试应该可以帮助您确定它是否可以。如果您仍然不确定,最好批量发送电子邮件(可以使用crontab定时),以分散负载。

希望这可以帮到您。


这是一个令人困惑的答案,因为如果不使用cron,在关注流量之前,发送电子邮件时脚本超时将成为一个问题。 - rick
我认为Phil建议您使用cron来限制电子邮件的发送。例如,每30分钟一次,每次仅发送100封,直到列表耗尽。 - grossvogel
但是他似乎在暗示应该使用crontab作为高流量的解决方案?无论如何,我们都应该很幸运地通过营销活动产生太多的流量。很可能这不是一个问题。 - rick

0

我使用Swiftmailer开发了一个简单易用的电子邮件管理系统。它支持SMTP、加密、附件、批量发送等功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接