Simple Mail Queue

11 followers

This wiki article has not been tagged with a corresponding Yii version yet.
Help us improve the wiki by updating the version information.

Overview

This tutorial shows how to create a simple mail queue. It is usually run from a cron job but can be run from the command line too.

The basic idea is to create a complete mail message and store it a Db table along with all info necessary for sending valid emails (to_email, from_email, from_name, subject etc.)

Database Structure

A simple table for holding mail queue items.

-- 
-- Structure for table `tbl_email_queue`
-- 
DROP TABLE IF EXISTS `tbl_email_queue`;
CREATE TABLE IF NOT EXISTS `tbl_email_queue` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `from_name` varchar(64) DEFAULT NULL,
  `from_email` varchar(128) NOT NULL,
  `to_email` varchar(128) NOT NULL,
  `subject` varchar(255) NOT NULL,
  `message` text NOT NULL,
  `max_attempts` tinyint(3) unsigned NOT NULL DEFAULT '3',
  `attempts` tinyint(3) unsigned NOT NULL DEFAULT '0',
  `success` tinyint(1) NOT NULL DEFAULT '0',
  `date_published` datetime DEFAULT NULL,
  `last_attempt` datetime DEFAULT NULL,
  `date_sent` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `to_email` (`to_email`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8_general_ci;

MailQueue Model

CRUD operations for tbl_mail_queue

<?php
/**
 * This is the model class for table "{{email_queue}}".
 *
 * The followings are the available columns in table '{{email_queue}}':
 * @property integer $id
 * @property string $from_name
 * @property string $from_email
 * @property string $to_email
 * @property string $subject
 * @property string $message
 * @property integer $max_attempts
 * @property integer $attempts
 * @property integer $success
 * @property string $date_published
 * @property string $last_attempt
 * @property string $date_sent
 */
class EmailQueue extends CActiveRecord
{
 
    /**
        * Returns the static model of the specified AR class.
        * @param string $className active record class name.
        * @return EmailQueue the static model class
        */
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
 
    /**
        * @return string the associated database table name
        */
    public function tableName()
    {
        return '{{email_queue}}';
    }
 
    /**
        * @return array validation rules for model attributes.
        */
    public function rules()
    {
        return array(
            array('from_email, to_email, subject, message', 'required'),
            array('max_attempts, attempts, success', 'numerical', 'integerOnly' => true),
            array('from_name', 'length', 'max' => 64),
            array('from_email, to_email', 'length', 'max' => 128),
            array('subject', 'length', 'max' => 255),
            array('date_published, last_attempt, date_sent', 'safe'),
 
            array('id, from_name, from_email, to_email, subject, message, max_attempts, attempts, success, date_published, last_attempt, date_sent', 'safe', 'on' => 'search'),
        );
    }
 
    /**
        * @return array customized attribute labels (name=>label)
        */
    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'from_name' => 'From Name',
            'from_email' => 'From email',
            'to_email' => 'To email',
            'subject' => 'Subject',
            'message' => 'Message',
            'max_attempts' => 'Max Attempts',
            'attempts' => 'Attempts',
            'success' => 'Success',
            'date_published' => 'Date Published',
            'last_attempt' => 'Last Attempt',
            'date_sent' => 'Date Sent',
        );
    }
}
?>

Console Command

Retrieves a list of active mail queue objects and fires off the emails

<?php
/**
 * MailQueueCommand class file.
 *
 * @author Matt Skelton
 * @date 26-Jun-2011
 */
 
/**
 * Sends out emails based on the retrieved EmailQueue objects. 
 */
class MailQueueCommand extends CConsoleCommand
{
 
    public function run($args)
    {
        $criteria = new CDbCriteria(array(
                'condition' => 'success=:success AND attempts < max_attempts',
                'params' => array(
                    ':success' => 0,
                ),
            ));
 
        $queueList = EmailQueue::model()->findAll($criteria);
 
        /* @var $queueItem EmailQueue */
        foreach ($queueList as $queueItem)
        {
            $message = new YiiMailMessage();
            $message->setTo($queueItem->to_email);
            $message->setFrom(array($queueItem->from_email => $queueItem->from_name));
            $message->setSubject($queueItem->subject);
            $message->setBody($queueItem->message, 'text/html');
 
            if ($this->sendEmail($message))
            {
                $queueItem->attempts = $queueItem->attempts + 1;
                $queueItem->success = 1;
                $queueItem->last_attempt = new CDbExpression('NOW()');
                $queueItem->date_sent = new CDbExpression('NOW()');
 
                $queueItem->save();
            }
            else
            {
                $queueItem->attempts = $queueItem->attempts + 1;
                $queueItem->last_attempt = new CDbExpression('NOW()');
 
                $queueItem->save();
            }
        }
    }
 
    /**
        * Sends an email to the user.
        * This methods expects a complete message that includes to, from, subject, and body
        *
        * @param YiiMailMessage $message the message to be sent to the user
        * @return boolean returns true if the message was sent successfully or false if unsuccessful
        */
    private function sendEmail(YiiMailMessage $message)
    {
        $sendStatus = false;
 
        if (Yii::app()->mail->send($message) > 0)
            $sendStatus = true;
 
        return $sendStatus;
    }
 
}
?>

Usage

Now that we've got our structure setup, we can simply start creating MailQueue objects. This can be implemented in a behavior, event handlers, or simply in a controller's action.

Below, I'm creating a MailQueue object in a model's afterSave event handler.

// Typical usage in a controller or model
public function afterSave()
{
    $queue = new EmailQueue();
    $queue->to_email = 'bill_hicks@afterlife.com';
    $queue->subject = "Mall Kids Are People Too, Damnit!";
    $queue->from_email = Yii::app()->params['adminEmail'];
    $queue->from_name = Yii::app()->name;
    $queue->date_published = new CDbExpression('NOW()');
    $queue->message = Yii::app()->controller->renderPartial('//mail/sarcastic/notify', array(
        ...
    ), true); // Make sure to return true since we want to capture the output
 
    $queue->save();
 
    parent::afterSave();
}

That's it. Now you can point your CRON/Task Scheduler at the command and watch the electromagnetic mail fly!

Feedback welcome.

Total 1 comment

#14193 report it
atrandafir at 2013/07/25 10:43am
Add queue() method to Yii::app()->mail component

Hey I have just build my own "mail_queue" system because I forgot to come here to see if there was already an extension.

Basically it does the same as yours but it has two different things:

One: I have added a method in Yii::app()->mail component named queue(), and this way if your application was sending instant emails that you want instead to save them and send them later with the cron job, you simply use Yii::app()->mail->queue($message) instead of Yii::app()->mail->send($message). And the message will be saved for later delivery.

Second: The other requirement that I had for my application is to avoid re-sending the same email if a user clics a button several times. For example, users on your website can follow other users, so, someone clicks follow and you send a email to the followed user saying "Hey, you've got a new follwer".

The problem is that if the user clics Unfollow, and then Follow again, your application will end up sending two emails.

So what I did is a "simple system to make emails unique" and it works this way: The Yii::app()->mail->queue() method has two optional parameters: $check_unique and $unique_params

So what you have to simply do is this:

Yii::app()->mail->queue($message, true, array(
    'type'=>'new_follower',
    'first_user_id'=>$first_user_id,
    'second_user_id'=>$second_user_id,
));

The parameters provided in the array will be serialized and converted to a md5 hash that will be saved in the queue table to later identify if you are trying to add a duplicate queue for the same message.

Those are the methods added to the YiiMail component:

public function queue(YiiMailMessage $message, $check_unique=false, $unique_params=null) {
 
  foreach ($message->to as $email => $name) {
    $mail_queue=new MailQueue();
    if (is_array($message->from)) {
      foreach ($message->from as $from_email => $from_name) {
        $mail_queue->from=$from_email;
        $mail_queue->from_name=$from_name;
      }
    }
    $mail_queue->to=$email;
    $mail_queue->to_name=$name;
    $mail_queue->subject=$message->subject;
    $mail_queue->template=$message->view;
    $mail_queue->date_add=date("Y-m-d H:i:s");
    $mail_queue->body=$message->body;
    $mail_queue->unique_hash=$this->queue_unique_hash($mail_queue, $unique_params);
 
    $is_unique=true;
    if ($check_unique) {
      $check_unique=MailQueue::model()->count("t.unique_hash=:hash", array(":hash"=>$mail_queue->unique_hash));
      $is_unique=$check_unique > 0 ? false : true;
    }
 
    if (($check_unique && $is_unique) || !$check_unique) {
      if (!$mail_queue->save()) {
        $this->send($message);
      }
    }
 
 
  }
 
  return true;
}
 
private function queue_unique_hash($mail_queue, $unique_params=null) {
  $hash='';
  if ($unique_params != null) {
    if (is_array($unique_params)) {
      $unique_params=CJSON::encode($unique_params);
    }
    $unique_params="{$mail_queue->template}-$unique_params";
  } else {
    $unique_params="{$mail_queue->from}-{$mail_queue->to}-{$mail_queue->subject}-{$mail_queue->template}";
  }
  $hash=md5($unique_params);
  return $hash;
}

And this is the table for the model where the queues are saved:

CREATE TABLE IF NOT EXISTS `mail_queue` (
`id_mail_queue` int(11) NOT NULL AUTO_INCREMENT,
`unique_hash` varchar(32) NOT NULL,
`from` varchar(255) NOT NULL,
`from_name` varchar(255) DEFAULT NULL,
`to` varchar(255) NOT NULL,
`to_name` varchar(255) DEFAULT NULL,
`subject` varchar(255) NOT NULL,
`body` text NOT NULL,
`template` varchar(255) DEFAULT NULL,
`sent` tinyint(1) NOT NULL DEFAULT '0',
`date_add` datetime NOT NULL,
`date_sent` datetime DEFAULT NULL,
PRIMARY KEY (`id_mail_queue`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

If you think some of this could be useful to integrate into your own extension feel free to do it, else, I hope someone finds it helpful for his project.

Leave a comment

Please to leave your comment.

Write new article