16

我正在尝试为我的应用程序实现一个邮件列表系统。我目前正在使用它作为我的交通工具,循环浏览我的订阅者列表,并向每个Zend_Mail_Transport_Smtp('localhost')订阅者发送一个新的。Zend_Mail但是,我注意到脚本完成所需的时间会随着订阅者数量的增加而增加。

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

我知道这Zend_Mail不会进行任何排序邮件排队。有这方面经验的人可以给我概述一下如何做到这一点吗?我对cron/crontab/cronjobs一无所知,所以如果涉及到,请解释一下过程。

4

8 回答 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。

于 2009-04-25T14:21:03.557 回答
20

为了使用 PHP 可靠地发送大量电子邮件,您必须使用排队机制。正如其他人所建议的,使用队列的过程如下所示:

  • 遍历您的一组用户,为每个用户创建一封电子邮件,并可能自定义内容
  • 将每个邮件对象传递给队列,这将延迟发送电子邮件直到稍后
  • 在某种 cron 脚本中,一次发送几百个队列的内容。注意:您需要通过查看日志来调整您发送的电子邮件数量,以了解实际发送过程中返回的错误。如果您尝试发送太多,我注意到它达到了邮件传输将不再接受连接的地步(我正在使用 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 邮件队列。我已经成功地做到了这一点,但是需要花点时间才能做到这一点。为此,扩展 Zend Mail Transport Abstract,实现 _sendMail 方法(您将在此将 Zend Mail 对象放入邮件队列)并将传输对象的实例传递给 Zend Mail 对象的 send() 方法或通过 Zend Mail::setDefaultTransport()。

最重要的是,您可以通过多种方式做到这一点,但这需要代表您进行一些研究和学习。然而,这是一个非常可以解决的问题。

于 2009-05-07T02:36:36.253 回答
3

来自 PHP.net 文档。

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

Zend Mail 类可能非常好(Zend 的大部分东西都很好)但是如果你想要其他选择。他们来了。

于 2009-04-25T12:29:59.220 回答
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
于 2011-05-26T00:07:49.063 回答
2

尽管像其他人所指出的那样避免使用 mail() ,但您应该可以将 PHP 用于数千个收件人。我已经看到一些为大量邮件(超过 100,000 个收件人)设计的系统开始绕过标准邮件功能并尝试更直接地使用 MTA。即使那样,我也不清楚这是必需的。

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

于 2009-04-25T12:49:58.923 回答
2

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

排队邮件的过程是这样的:

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

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

于 2009-04-25T18:37:10.867 回答
0

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

如果您熟悉框架工作,我会坚持使用它。

向大量人员发送电子邮件时要考虑的重要事项是:

  • 您的网络服务器能否在打开电子邮件时处理图像请求 + 访问您网站的人在服务器上的负载。

如果答案是否定的或您不确定,那么使用 apache benchmark 应该可以帮助您解决问题。如果您仍然不确定,最好批量发送电子邮件(可以使用 crontab 定时)以分散负载。

我希望这有帮助。

于 2009-04-25T09:47:41.420 回答
0

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

于 2009-04-26T00:57:45.163 回答