Pull to refresh

Простой способ организации очереди e-mail с помощью Zend_Mail

Reading time 4 min
Views 3.7K
Всем привет.

Хочу поделиться очень простым и легким способом организации e-mail очереди с помощью Zend_Mail. Примеры в статье намеренно сделаны максимально простыми и не привязаны к фреймворку, т.к. цель статьи показать способ, а не конкретную реализацию. К тому же данное решение не обязательно должно быть использовано только в рамках Zend Framework, оно легко впишется в любой проект.

Сразу к делу. Нам потребуется:
— таблица в БД;
— класс транспорта;
— файл реализующий отправку сообщений (запускается по крону).

Сперва определим таблицу БД:
CREATE TABLE email_queue ( 
    id                INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 
    recipients        TEXT NOT NULL, 
    subject           CHAR(255) NOT NULL, 
    message           TEXT NOT NULL, 
    header            TEXT NOT NULL, 
    parameters        TEXT, 
    max_attempts      TINYINT UNSIGNED NOT NULL DEFAULT 3, 
    attempts          TINYINT UNSIGNED NOT NULL DEFAULT 0, 
    is_false          TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
    in_process        INT UNSIGNED DEFAULT NULL DEFAULT 0, 
    time_last_attempt INT UNSIGNED DEFAULT NULL,
    create_time       INT UNSIGNED NOT NULL
);


Далее, создадим класс нашего транспорта:
class EmailQueueTransport extends Zend_Mail_Transport_Sendmail
{
    /**
     * Send mail using EmailQueue
     *
     * @access public
     * @return void
     * @throws Zend_Mail_Transport_Exception If parameters is set but not a string
     * @throws Zend_Mail_Transport_Exception If failed to add a message in queue
     */
    public function _sendMail()
    {
        if ($this->parameters !== null && !is_string($this->parameters)) {
            /**
             * Exception is thrown here because $parameters is a public property
             */
            throw new Zend_Mail_Transport_Exception('Parameters were set but are not a string');
        }
               
        $db = Zend_Db_Table_Abstract::getDefaultAdapter();
        $statement = $db->prepare('
            INSERT email_queue 
            SET recipients  = :recipients,
                subject     = :subject, 
                message     = :message,
                header      = :header,
                parameters  = :parameters,
                create_time = :create_time
        ');          
        $result = $statement->execute(array(
            'recipients'  => $this->recipients,
            'subject'     => $this->_mail->getSubject(), 
            'message'     => $this->body,
            'header'      => $this->header,
            'parameters'  => $this->parameters,
            'create_time' => time()
        ));
        
        if (!$result) {
            throw new Zend_Mail_Transport_Exception(
                'Failed to add a message in queue.');
        }          
    }
}

Всё что делает этот класс, это — получает уже подготовленные данные из Zend_Mail и сохраняет их в БД. Eсли создать запись в таблице не удалось, выбрасывает исключение Zend_Mail_Transport_Exception.

Ну и файл реализующий отправку сообщений, предположим, что файл лежит в отдельной папке, в корне приложения:
<?php
/**
 * Add the messages in the log
 * 
 * @param type $message 
 * @return void
 */
function set_log($message) 
{
    $message = date('H:i d.m.Y ', time()) . $message . "\r\n";
    $logFile = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . basename(__FILE__, '.php') . '.log';
    error_log($message, 3, $logFile);         
}

try {
    $config = include realpath(dirname(__FILE__) . '/../') . '/application/config/config.php'; // Path to config file
    $configDb = $config['db']['params'];

    $db = new PDO(
        'mysql:host=' . $configDb['host'] . ';dbname=' . $configDb['dbname'],
        $configDb['username'],
        $configDb['password'],
        array(
            PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'",
            'profiler' => false,
        )
    );
} catch (PDOException $e) {
    set_log('Connection error: ' . $e->getMessage());
}  

$limit            = 100; // limit rows
$processLimitTime = 600; // 10 minutes
$statement = $db->query('
    SELECT * 
    FROM email_queue 
    WHERE attempts   < max_attempts
    AND   in_process < ' . (time() - $processLimitTime) . '
    ORDER BY id ASC
    LIMIT ' . $limit
);
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
    $db->beginTransaction();
    $result = $db->exec('
        UPDATE email_queue 
        SET in_process = ' . time() . '
        WHERE id = ' . $row['id']
    );
    if (!$result) {
        set_log('Error when updating record from the table "email_queue". Id queue is ' . $row['id'] . '.');
        continue;
    }
    
    $isSent = mail(
        $row['recipients'],
        $row['subject'], 
        $row['message'], 
        $row['header'], 
        $row['parameters']
    );
    $db->commit();
    
    if ($isSent) {
        $result = $db->exec('DELETE from email_queue WHERE id = ' . $row['id']);
        if (!$result) {
            set_log('Error when deleting record from the table "email_queue". Id queue is ' . $row['id'] . '.');
        }

    } else {
        $result = $db->exec('
            UPDATE email_queue 
            SET is_false   = 1, 
                in_process = 0,
                attempts   = ' . ($row['attempts'] + 1) . ' 
            WHERE id = ' . $row['id']
        );
        if (!$result) {
            set_log('Error when updating record from the table "email_queue". Id queue is ' . $row['id'] . '.');
        }
        set_log('Error when sending messages to e-mail. Id queue is ' . $row['id'] . ', e-mails is ' . $row['recipients'] . '.');
    }
}

Использование очереди:
$mail = new Zend_Mail();
$mail->setFrom($from);
$mail->addTo($to);
$mail->setBodyHtml($body);
$mail->setSubject($subject);

$transport = new \EmailQueueTransport();
$mail->send($transport);

Соответственно, если нужно отправить сообщение без очереди, через mail() — не передайте транспорт либо передайте null.

P.S. Возможно кто-то посчитает более правильным использовать Zend_Queue, и возможно будет прав. Я ни в коем случае не утверждаю, что способ описанный в статье является единственным верным решением. Решений всегда множество, я описал всего лишь один из них.

P.P.S. Отдельное спасибо за дельные замечания пользователям markPnk и gen.
Tags:
Hubs:
+4
Comments 29
Comments Comments 29

Articles