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

问题描述:

我想为我的应用程序实现一个邮件列表系统。我目前使用Zend_Mail_Transport_Smtp('localhost')作为我的交通工具,循环访问我的用户列表,并向每个用户发送新的Zend_Mail。但是,我注意到随着用户数量的增加,脚本完成所需的时间也会增加。从Zend Framework应用程序向数百个收件人发送电子邮件的最佳方法是什么?

我确信必须有一个更专业的方法来做到这一点,涉及电子邮件排队。我认为理想的方法是让用户填写表单,点击发送,然后立即得到一个回复​​,表示电子邮件正在发送,而不是等待数百封电子邮件完成发送。

据我所知,Zend_Mail不做任何排序邮件排队。任何有此经验的人都可以给我一个关于如何做到这一点的概述。我对cron/crontab/cronjobs一无所知,所以如果涉及到这一点,请解释一下过程。

为了使用PHP可靠地发送大量的电子邮件,你必须使用排队机制。正如其他人则建议,使用队列的过程看起来是这样的:

  • 循环在你的用户组,对每一个创建电子邮件,并可能自定义内容
  • 通过每个邮件对象到队列这将延迟发送电子邮件,直到后来
  • 在某种cron脚本中,一次发送几百个队列的内容。 注意:您需要通过查看日志查找实际发送过程中返回的错误来调整发送的电子邮件数量。如果您尝试发送太多,我注意到它达到一个地步,邮件传输将不再接受连接(我使用Qmail)

有几个库,在那里你可以用它来做到这一点,PEAR Mail Queue(Mail_Mime)和SwiftMailer都允许您创建和排队电子邮件。到目前为止,Zend Mail只提供创建电子邮件,而不是排队(稍后更多)。

我有经验主要与PEAR Mail Queue,并有几个陷阱。如果您试图排队大量电子邮件(例如,循环使用20,000个用户并试图在合理的时间内将其加入队列中),则使用Mail Mime的quoted-printable编码实现非常缓慢。你可以通过切换到base64编码来加快速度。对于Zend Mail,您可以编写一个Zend邮件传输对象,它将您的Zend邮件对象放入PEAR邮件队列中。我已经取得了一些成功,但需要一点努力才能做到。为此,请扩展Zend邮件传输摘要,实现_sendMail方法(将您的Zend邮件对象放入邮件队列中),并将传输对象的实例传递给Zend Mail对象的send()方法,或者通过Zend Mail :: setDefaultTransport()。

底线是有很多方法可以做到这一点,但这需要一些研究和学习。然而,这是一个非常可以解决的问题。

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

如果你熟悉框架的工作,我会坚持下去。

重要的事情发送电子邮件到大量的人时,要考虑的是:

  • 你的网络服务器能应对图像请求时,邮件被打开+的人访问您的网站服务器上的负载。

如果答案是否定的或者您不确定,使用apache基准测试应该能够帮助您解决问题。如果您仍然不确定,最好批量发送电子邮件(可以使用crontab定时)来传播负载。

我希望这会有所帮助。

+0

这是一个令人困惑的答案,因为如果不使用cron的脚本,脚本超时时发送的电子邮件将成为一个问题之前交通的关注。 – rick 2009-04-25 18:42:37

+0

我认为菲尔建议您使用cron来限制发送电子邮件。例如,每30分钟一次只发送100个,直到列表用尽。 – grossvogel 2009-04-25 19:05:28

+0

但他似乎建议crontab应该被用作高流量的解决方案?无论如何,我们都应该如此幸运,以便通过营销活动产生过多的流量。有可能,这不是一个问题。 – rick 2009-04-25 19:10:31

来自PHP.net文档。

注意:值得注意的是mail()函数不适合循环中的大量电子邮件。此功能为每封电子邮件打开和关闭SMTP套接字,效率不高。
有关发送大量电子邮件的信息,请参阅»PEAR::Mail和»PEAR::Mail_Queue程序包。

Zend Mail类可能相当不错(Zend的大部分东西都不错)但是如果你想要其他的选择。他们来了。

+0

PEAR :: Mail在我的经验中很慢。 PHPMailer和swiftmailer都非常出色。 – rick 2009-04-25 18:19:13

虽然避免使用其他人注意到的邮件(),但您应该很好地使用PHP使用数千个收件人。我见过一些为大量邮件而设计的系统(超过10万名收件人)跳过了标准邮件功能,并试图更直接地使用MTA。即使如此,我还不清楚这是必需的。

使电子邮件专业人员更关注确保格式是好的(HTML和纯文本尽可能),人们可以轻松取消订阅,反弹处理正确,邮件服务器具有所有正确的DNS记录,并且服务器配置不违反任何主要黑名单系统的规则。您编写应用程序的语言不是几百甚至几千条消息的主要因素。

注意:当我第一次看到你的问题时,我以为它一次说几十万封电子邮件。当我加倍检查时,我注意到它实际上说成百上千。我现在懒得改变我的帖子,所以这里有一些警告:根据我的经验,如果没有商业工具,大概可以运行到40K左右。在大约10K时,你会希望遵循“最低限度”列表,以防止在开始到达更大的列表大小时出现严重的痛苦。我建议尽快实施它。

我以前说过这一点,有两个方面发送电子邮件:

  1. 技术方 - RFC的周围的SMTP 协议基本上都 ,电子邮件格式,DNS 记录,等等。这是温和的 复杂但可以解决。
  2. 神奇的一面 - 电子邮件传递 管理是巫术。你会得到 沮丧,事情将打破没有 明显的原因,你会 考虑离开另一份工作 不涉及电子邮件。

我建议您不要编写自己的批量发件人。我相信PHP可以做得很好,但是你应该把时间花在其他地方。我过去使用过的两种产品是Strongmail和PowerMTA。被警告 - 他们有很高的价格标签,但我几乎可以保证你将花费更多的时间来构建自己的解决方案。

在PHP中编写属于自己的一个领域是节流/焦油点。邮件服务器会在您发送几条消息以减慢速度并阻止您发送垃圾邮件后,开始在睡眠中添加(30)。

通常,这些商业批量发件人运行SMTP协议进行排队。您将继续使用Zend_Mail,但是会将其硬编码以连接到您的服务器。它会尽可能快地发送邮件,然后使用自己的引擎将邮件发送到目的地。

在100K的列表中,您将不得不采用电子邮件最佳做法。至少,你需要:

  • SPF记录,可能DKIM以及超过
  • 多个IP地址,以段交通 - 有3个IP的,一个你信任的质量地址,一个用于中等风险的IP地址另一个用于高风险IP地址。这种设计有助于最大限度地降低将邮件发送给最佳客户的风险。发送IP
  • 正确的反向DNS地址
  • 使用反馈来自AOL,Hotmail的,雅虎循环和其他处理垃圾邮件投诉
  • 退订和弹跳管理 - 确保你修剪这些地址
  • 有打开/点击跟踪也很重要 - 如果您是A列表上的客户不打开您的电子邮件,则需要将其降级到B列表等等。这很重要,因为ISP会将不活动的帐户变成蜜罐。 Hotmail以此而闻名。

最后,如果你真的想发送电子邮件,你会需要一些其他工具,如返回路径。

我在php中实现了一个批量邮件程序,其中每个邮件都是为个人定制的。这并不难,也不需要太长时间。我用swiftmailer和cron。 Zend Mail也可以。我从PEAR邮件队列开始,但排队邮件太慢了。

排队电子邮件的过程中去,像这样:

  1. 创建电子邮件模板,并添加占位符(或使用模板引擎)的,独特的内容将被取代的区域。
  2. 在一个循环中,将占位符替换为任何唯一内容,将生成的电子邮件内容,主题,地址,批次ID以及可选的优先级值插入到数据库表中。

我用cron作业发出批量的电子邮件。 cron时间间隔和每批发送的电子邮件的数量非常重要,因为我在一台有限制的共享主机上。由cron作业调用的脚本只能由cron访问。该脚本从批处理标识和可选的优先级排序的表中读取x个电子邮件。如果电子邮件发送成功,它将从数据库队列中删除。如果电子邮件无法发送,它将保留在队列中,并且该记录的计数器增加。如果一个计数器超过设定的数字,那么该电子邮件已从队列中删除。

我开发了一个新闻通讯管理系统Swiftmailer,它很容易实现。它支持SMTP,加密,附件,批量发送,...

使用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