Home > PHP, Zend Framework > Extending Zend_Mail to redirect test e-mails

Extending Zend_Mail to redirect test e-mails

November 24th, 2008 Leave a comment Go to comments

Have you ever wanted to redirect e-mails sent by a php application to a test e-mail address instead of to the actual “to” address? This can be helpful when running tests or while the application is in development mode. Read on where I explain how to accomplish this in Zend Framework.

Why?
It can be useful to redirect the e-mails your application sends when the application uses, for example, e-mail addresses from the database to notify users of updated articles. In this case you’ll have to change the e-mail addresses in the database if you want to know if the e-mails are actually sent out successfully. But then there is no way to check if the recipient e-mail addresses are the correct e-mail addresses for that particular e-mail.

There is an easier way to do these tests. This way it will be entirely transparent when you use the Zend_Mail object to send out e-mails. Zend Framework is easily adaptable to different scenarios because of the use of OOP. We are going to use this to extend the Zend_Mail object to capture our e-mails before they are sent and redirect them to a single e-mail address when development mode or test mode is enabled.

How?
First off, we are going to create a directory in the library path of the Zend Framework application. I myself named this directory ZendC. It stands for Zend Custom just like the ZendX directory stands for Zend Extras.

We will create a file in this directory with the same name of the original Zend_Mail file (/Zend/Mail.php). Like with all class definitions in Zend Framework, the class names are used to determine the path of the file and the filename for that class. Thus our new mail class should be named ZendC_Mail. When you create a new instance of ZendC_Mail, Zend Framework (with Zend_Loader’s autoload option enabled) will include the file in the path specified by the first part of the class name. The last part will be the filename. /ZendC/Mail.php in this case. ZendC could of course be renamed to anything you would like. Just remember to change both the directory structure in the include path and de class name in the php file to reflect that change.

So we begin with this line of code to extend Zend_Mail:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
/**
 * This object redirects e-mails if development or test mode is enabled so customers are not bothered with test e-mails.
 * It should always be used instead of Zend_Mail where e-mails should be redirected to the testing address.
 * The most current known working version with which this object was tested is Zend Framework 1.7.0.
 * 
 * @version 1.0 
 * @uses Zend_Mail
 */
class ZendC_Mail extends Zend_Mail
{
     // ...

The next thing we will have to do is declare all the private variables that we are going to use internally in our object.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    /**
     * Global registry.
     * @var Zend_Registry
     */
    private $_registry;
    /**
     * Contains an array with all recipients.
     * @var array (of strings)
     */
    private $_tempRecipients;
    /**
     * Contains an array with original 'To' recipients when in test mode.
     * @var array (of strings)
     */
    private $_tempTo;
    /**
     * Contains an array with original 'Cc' recipients when in test mode.
     * @var mixed
     */
    private $_TempCc;
    /**
     * Contains an array with original 'Bcc' recipients when in test mode.
     * @var mixed
     */
    private $_TempBcc;
    /**
     * Contains a boolean value, is this enviroment test or not.
     * @var boolean
     */
    private $_devMode;

The next thing we will have to do is overload the Zend_Mail constructor. This way we can check if development mode or test mode is enabled and set the $this->_devMode variable so we can use it later to check if it should customize the method’s statements, or just use the parent class method.
I use an instance of Zend_Registry to store if the application is in development mode or not. I also use a variable to store the current enviroment mode (test for PHPUnit tests, production for the production version). This is used in the Initializer of my Zend Framework application. This is not in the scope of this article so I will leave it there. You can customize this constructor to set $this->_devMode according to your own way of implementing different enviroments.

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    /**
     * Public constructor
     *
     * @param string $charset
     */
    public function __construct ($charset = 'iso-8859-1')
    {
        // Set charset for this e-mail
        $this->_charset = $charset;
        // Get registry from Initializer.
        $this->_registry = Zend_Controller_Front::getInstance()->getPlugin('Initializer')->getRegistry();
        // Get the current enviroment mode.
        $env = Zend_Controller_Front::getInstance()->getPlugin('Initializer')->getEnv();
        // Set addTo to test address if development mode is enabled, or enviroment is not production.
        if ($this->_registry->developmentConfig->developmentMode || $env != 'production') {
            $this->_devMode = true;
            parent::addTo($this->_registry->developmentConfig->emailTestEmail, 'TEST recipient');
        } else {
            $this->_devMode = false;
        }
    }

Next up is overloading the parent class’s setter methods. We begin with the methods to set the recipients of the e-mail.

The first thing it will do is check wether devMode is enabled. It will execute the devMode specific logic when it is, and the parent class’s method if it is disabled.
All of these methods check if a body has been set when devMode is enabled, and throws an exeption if the body is already set. This is important because the body setter methods will add the original recipients to the body of the e-mail. And we don’t want to miss any addresses there. So always set all your recipients before you set the body of the e-mail.

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
    /**
     * Adds To-header and recipient
     *
     * @param  string $name
     * @param  string $email
     * @return Zend_Mail Provides fluent interface
     */
    public function addTo ($email, $name = '')
    {
        // Do nothing if development mode is enabled
        if ($this->_devMode) {
            if (parent::getBodyText() || parent::getBodyHtml()) {
                // Throw error if the body is already set before adding all recipients.
                // All recipients should be known in dev mode before body is set so no e-mail adrresses are omitted
                // in _tempRecipients for addition to the body text/hmtl later.
                throw new Zend_Mail_Exception('Error: Should set e-mail body after adding ALL the recipients!');
            }
            if ($name != '') {
                $this->_TempTo[] = $name . ' <' . $email . '>';
            } else {
                $this->_TempTo[] = $email;
            }
            $this->_tempRecipients[] = $email;
            return $this;
        } else {
            return parent::addTo($email, $name);
        }
    }
    /**
     * Adds Cc-header and recipient
     *
     * @param  string    $name
     * @param  string    $email
     * @return Zend_Mail Provides fluent interface
     */
    public function addCc ($email, $name = '')
    {
        // Do nothing if development mode is enabled
        if ($this->_devMode) {
            if (parent::getBodyText() || parent::getBodyHtml()) {
                // Throw error if the body is already set before adding all recipients.
                // All recipients should be known in dev mode before body is set so no e-mail adrresses are omitted
                // in _tempRecipients for addition to the body text/hmtl later.
                throw new Zend_Mail_Exception('Error: Should set e-mail body after adding ALL the recipients!');
            }
            if ($name != '') {
                $this->_TempCc[] = $name . ' <' . $email . '>';
            } else {
                $this->_TempCc[] = $email;
            }
            $this->_tempRecipients[] = $email;
            return $this;
        } else {
            return parent::addCc($email, $name);
        }
    }
    /**
     * Adds Bcc recipient
     *
     * @param  string    $email
     * @return Zend_Mail Provides fluent interface
     */
    public function addBcc ($email)
    {
        // Do nothing if development mode is enabled
        if ($this->_devMode) {
            if (parent::getBodyText() || parent::getBodyHtml()) {
                // Throw error if the body is already set before adding all recipients.
                // All recipients should be known in dev mode before body is set so no e-mail adrresses are omitted
                // in _tempRecipients for addition to the body text/hmtl later.
                throw new Zend_Mail_Exception('Error: Should set e-mail body after adding ALL the recipients!');
            }
            $this->_TempBcc[] = $email;
            $this->_tempRecipients[] = $email;
            return $this;
        } else {
            return parent::addBcc($email);
        }
    }

The next bit is commented out. It can be enabled again if you want the getRecipients() method to return the test e-mail address (the real recipient when devMode is enabled) instead of the original recipient addresses.

143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
    // Disable custom, always return actual recipients not original in testmode.
    // Other application logic could use these and fail because the method returns the test address instead of the real addresses.
    // But it could be usefull in some cases so I wrote the method, but commented it out.
    //    /**
    //     * Return list of recipient email addresses
    //     *
    //     * @return array (of strings)
    //     */
    //    public function getRecipients()
    //    {
    //        // Do nothing if development mode is enabled
    //        if($this->_devMode) {
    //            return $this->_tempRecipients;
    //        } else {
    //            return parent::getRecipients();
    //        }
    //    }

Next, we will overload the parent class’s body setter methods for both text and html messages.
These methods should always be called after setting all the recipients. They will throw an exception if no recipients have been set yet for this reason.
They will just add the original recipients to the body if devMode is enabled.

160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
    /**
     * Sets the text body for the message.
     *
     * @param  string $txt
     * @param  string $charset
     * @param  string $encoding
     * @return Zend_Mail Provides fluent interface
     */
    public function setBodyText ($txt, $charset = null, $encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE)
    {
        if ($this->_devMode) {
            if (count($this->_tempRecipients) == 0) {
                // Throw error if the body is set before adding all recipients.
                // All recipients should be known in dev mode before body is set.
                throw new Zend_Mail_Exception('Error: Should set e-mail body only after adding ALL the recipients!');
            }
            $txt = "Orginal reciepients:\n" . implode("\n", $this->_tempRecipients) . "\n\nOriginal text:\n" . $txt;
            return parent::setBodyText($txt, $charset, $encoding);
        } else {
            return parent::setBodyText($txt, $charset, $encoding);
        }
    }
    /**
     * Sets the HTML body for the message
     *
     * @param  string    $html
     * @param  string    $charset
     * @param  string    $encoding
     * @return Zend_Mail Provides fluent interface
     */
    public function setBodyHtml ($html, $charset = null, $encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE)
    {
        if ($this->_devMode) {
            if (count($this->_tempRecipients) == 0) {
                // Throw error if the body is set before adding all recipients.
                // All recipients should be known in dev mode before body is set.
                throw new Zend_Mail_Exception('Error: Should set e-mail body only after adding ALL the recipients!');
            }
            $html = 'Orginal reciepients:<br/>' . implode('<br/>', $this->_tempRecipients) . '<br/><br/>Original text:<br/>' . $html;
            return parent::setBodyHtml($html, $charset, $encoding);
        } else {
            return parent::setBodyHtml($html, $charset, $encoding);
        }
    }

The last thing we have to modify is the subject of the e-mail. I just added the word TEST to it to make the test e-mails easy to recognize and filter in my inbox.

204
205
206
207
208
209
210
211
212
213
214
215
216
    /**
     * Sets the subject of the message
     *
     * @param  string    $subject
     * @return Zend_Mail Provides fluent interface
     */
    public function setSubject ($subject)
    {
        if ($this->_devMode) {
            $subject = 'TEST ' . $subject;
        }
        return parent::setSubject($subject);
    }

And we shouldn’t forget to end the class.

217
}

Usage of the ZendC_Mail object
Using this new object is just as easy as using the Zend_Mail version. In fact, it behaves exactly the same because it just forwards all the methods to it’s parent, or does exactly the same in the case of the constructor.
The only difference is that it will send all e-mails to a test address instead of real addresses.

Usage example:

1
2
3
4
5
6
7
$content = 'Content of the e-mail';
$mail = new ZendC_Mail();
$mail->addTo('to@address.com', 'name');
$mail->setFrom('from@address.com', 'name');
$mail->setSubject('Subject');
$mail->setBodyText($content);
$mail->send();

Conclusion
This custom extension of Zend_Mail can be very helpful when your application is in development or testing mode when running PHPUnit tests on your application for example. This is also an example of how easy it is to add your own functionality to the Zend Framework library.

Here is the full /ZendC/Mail.php file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
<?php
/**
 * This object redirects e-mails if development or test mode is enabled so customers are not bothered with test e-mails.
 * It should always be used instead of Zend_Mail where e-mails should be redirected to the testing address.
 * The most current known working version with which this object was tested is Zend Framework 1.7.0.
 * 
 * @version 1.0 
 * @uses Zend_Mail
 */
class ZendC_Mail extends Zend_Mail
{
    /**
     * Global registry.
     * @var Zend_Registry
     */
    private $_registry;
    /**
     * Contains an array with all recipients.
     * @var array (of strings)
     */
    private $_tempRecipients;
    /**
     * Contains an array with original 'To' recipients when in test mode.
     * @var array (of strings)
     */
    private $_tempTo;
    /**
     * Contains an array with original 'Cc' recipients when in test mode.
     * @var mixed
     */
    private $_TempCc;
    /**
     * Contains an array with original 'Bcc' recipients when in test mode.
     * @var mixed
     */
    private $_TempBcc;
    /**
     * Contains a boolean value, is this enviroment test or not.
     * @var boolean
     */
    private $_devMode;
    /**
     * Public constructor
     *
     * @param string $charset
     */
    public function __construct ($charset = 'iso-8859-1')
    {
        // Set charset for this e-mail
        $this->_charset = $charset;
        // Get registry from Initializer.
        $this->_registry = Zend_Controller_Front::getInstance()->getPlugin('Initializer')->getRegistry();
        // Get the current enviroment mode.
        $env = Zend_Controller_Front::getInstance()->getPlugin('Initializer')->getEnv();
        // Set addTo to test address if development mode is enabled, or enviroment is not production.
        if ($this->_registry->developmentConfig->developmentMode || $env != 'production') {
            $this->_devMode = true;
            parent::addTo($this->_registry->developmentConfig->emailTestEmail, 'TEST recipient');
        } else {
            $this->_devMode = false;
        }
    }
    /**
     * Adds To-header and recipient
     *
     * @param  string $name
     * @param  string $email
     * @return Zend_Mail Provides fluent interface
     */
    public function addTo ($email, $name = '')
    {
        // Do nothing if development mode is enabled
        if ($this->_devMode) {
            if (parent::getBodyText() || parent::getBodyHtml()) {
                // Throw error if the body is already set before adding all recipients.
                // All recipients should be known in dev mode before body is set so no e-mail adrresses are omitted
                // in _tempRecipients for addition to the body text/hmtl later.
                throw new Zend_Mail_Exception('Error: Should set e-mail body after adding ALL the recipients!');
            }
            if ($name != '') {
                $this->_TempTo[] = $name . ' <' . $email . '>';
            } else {
                $this->_TempTo[] = $email;
            }
            $this->_tempRecipients[] = $email;
            return $this;
        } else {
            return parent::addTo($email, $name);
        }
    }
    /**
     * Adds Cc-header and recipient
     *
     * @param  string    $name
     * @param  string    $email
     * @return Zend_Mail Provides fluent interface
     */
    public function addCc ($email, $name = '')
    {
        // Do nothing if development mode is enabled
        if ($this->_devMode) {
            if (parent::getBodyText() || parent::getBodyHtml()) {
                // Throw error if the body is already set before adding all recipients.
                // All recipients should be known in dev mode before body is set so no e-mail adrresses are omitted
                // in _tempRecipients for addition to the body text/hmtl later.
                throw new Zend_Mail_Exception('Error: Should set e-mail body after adding ALL the recipients!');
            }
            if ($name != '') {
                $this->_TempCc[] = $name . ' <' . $email . '>';
            } else {
                $this->_TempCc[] = $email;
            }
            $this->_tempRecipients[] = $email;
            return $this;
        } else {
            return parent::addCc($email, $name);
        }
    }
    /**
     * Adds Bcc recipient
     *
     * @param  string    $email
     * @return Zend_Mail Provides fluent interface
     */
    public function addBcc ($email)
    {
        // Do nothing if development mode is enabled
        if ($this->_devMode) {
            if (parent::getBodyText() || parent::getBodyHtml()) {
                // Throw error if the body is already set before adding all recipients.
                // All recipients should be known in dev mode before body is set so no e-mail adrresses are omitted
                // in _tempRecipients for addition to the body text/hmtl later.
                throw new Zend_Mail_Exception('Error: Should set e-mail body after adding ALL the recipients!');
            }
            $this->_TempBcc[] = $email;
            $this->_tempRecipients[] = $email;
            return $this;
        } else {
            return parent::addBcc($email);
        }
    }
    // Disable custom, always return actual recipients not original in testmode.
    // Other application logic could use these and fail because the method returns the test address instead of the real addresses.
    // But it could be usefull in some cases so I wrote the method, but commented it out.
    //    /**
    //     * Return list of recipient email addresses
    //     *
    //     * @return array (of strings)
    //     */
    //    public function getRecipients()
    //    {
    //        // Do nothing if development mode is enabled
    //        if($this->_devMode) {
    //            return $this->_tempRecipients;
    //        } else {
    //            return parent::getRecipients();
    //        }
    //    }
    /**
     * Sets the text body for the message.
     *
     * @param  string $txt
     * @param  string $charset
     * @param  string $encoding
     * @return Zend_Mail Provides fluent interface
     */
    public function setBodyText ($txt, $charset = null, $encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE)
    {
        if ($this->_devMode) {
            if (count($this->_tempRecipients) == 0) {
                // Throw error if the body is set before adding all recipients.
                // All recipients should be known in dev mode before body is set.
                throw new Zend_Mail_Exception('Error: Should set e-mail body only after adding ALL the recipients!');
            }
            $txt = "Orginal reciepients:\n" . implode("\n", $this->_tempRecipients) . "\n\nOriginal text:\n" . $txt;
            return parent::setBodyText($txt, $charset, $encoding);
        } else {
            return parent::setBodyText($txt, $charset, $encoding);
        }
    }
    /**
     * Sets the HTML body for the message
     *
     * @param  string    $html
     * @param  string    $charset
     * @param  string    $encoding
     * @return Zend_Mail Provides fluent interface
     */
    public function setBodyHtml ($html, $charset = null, $encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE)
    {
        if ($this->_devMode) {
            if (count($this->_tempRecipients) == 0) {
                // Throw error if the body is set before adding all recipients.
                // All recipients should be known in dev mode before body is set.
                throw new Zend_Mail_Exception('Error: Should set e-mail body only after adding ALL the recipients!');
            }
            $html = 'Orginal reciepients:<br/>' . implode('<br/>', $this->_tempRecipients) . '<br/><br/>Original text:<br/>' . $html;
            return parent::setBodyHtml($html, $charset, $encoding);
        } else {
            return parent::setBodyHtml($html, $charset, $encoding);
        }
    }
    /**
     * Sets the subject of the message
     *
     * @param  string    $subject
     * @return Zend_Mail Provides fluent interface
     */
    public function setSubject ($subject)
    {
        if ($this->_devMode) {
            $subject = 'TEST ' . $subject;
        }
        return parent::setSubject($subject);
    }
}
  1. Jason
    February 4th, 2009 at 20:20 | #1

    I noticed the \n characters in the setBodyText method were represented as just n. I’m not sure if this is result of some output escaping or something.

    But yeah, awesome class! I was looking for a way to re-route all sendmail calls to a log file but implementing it in application seems like a better solution. Thanks!

  2. February 4th, 2009 at 21:16 | #2

    @Jason
    Thank you for your reply! It seems like wordpress took out the backslashes of the newline chars when I published the article. It is fixed now.

  3. haasalex
    February 8th, 2009 at 15:58 | #3

    This sure works fine and is pretty flexible. I in fact was just searching for the getRecipients()-method because I am going to implement the Zend_Mail_Transport_Interface’s sendMail (Zend_Mail $mail, String $body, mixed $header, String $headers)-method for writing into a file rather than sending by smtp. See http://217.125.122.235:81/zend/documentation/api/Zend_Mail/Transport/Zend_Mail_Transport_Interface.html

  1. No trackbacks yet.