You are viewing revision #4 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version or see the changes made in this revision.
- Overview
- Database Structure
- [sql]
- -- Structure for table tbl_email_queue
- MailQueue Model
- Console Command
- Usage
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. ~~~
[sql] ¶
-- 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.
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.
Issues
If another cron runs in the meanwhile previous has not completed its work then there may be possibility of sending duplicate mails
Nice and simple
This is a nice and simple queue system. Thanks!
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.