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(11) 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` int(11) NOT NULL DEFAULT '3', `attempts` int(11) 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 AUTO_INCREMENT=5 DEFAULT CHARSET=latin1;
MailQueue Model
CRUD operations for tbl_mail_queue
/** * 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() { // NOTE: you should only define rules for those attributes that // will receive user inputs. 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'), // The following rule is used by search(). // Please remove those attributes that should not be searched. 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 relational rules. */ public function relations() { // NOTE: you may need to adjust the relation name and the related // class name for the relations automatically generated below. return array( ); } /** * @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', ); } /** * Retrieves a list of models based on the current search/filter conditions. * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions. */ public function search() { // Warning: Please modify the following code to remove attributes that // should not be searched. $criteria = new CDbCriteria; $criteria->compare('id', $this->id); $criteria->compare('from_name', $this->from_name, true); $criteria->compare('from_email', $this->from_email, true); $criteria->compare('to_email', $this->to_email, true); $criteria->compare('subject', $this->subject, true); $criteria->compare('message', $this->message, true); $criteria->compare('max_attempts', $this->max_attempts); $criteria->compare('attempts', $this->attempts); $criteria->compare('success', $this->success); $criteria->compare('date_published', $this->date_published, true); $criteria->compare('last_attempt', $this->last_attempt, true); $criteria->compare('date_sent', $this->date_sent, true); return new CActiveDataProvider($this, array( 'criteria' => $criteria, )); } }
Console Command
Retrieves a list of active mail queue objects and fires off the emails
/** * 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.