Day 12: Emails
Previously on symfony
Yesterday, the askeet application was extended to broadcast content on another media - RSS feed. Symfony is not just about web pages, and today's tutorial will illustrate it again. We will send an email by taking advantage of the MVC implementation.
Password recovery
The login forms (the AJAX one in every page, and the classic one accessed by the upper menu) require a nickname and a password, but it happens very often that users forget them. We must provide a mechanism to let them connect again in this case.
As we don't store the passwords in clear, we will be obliged to reset it to a random password, and send it to the user by email. For now, a user cannot modify his/her password, so the random one will not be very easy to remember, but we will address this issue later.
Password request form
In the user
module, we will create a new action that displays a form requesting an email address. In askeet/apps/frontend/modules/user/actions/action.class.php
, add:
public function executePasswordRequest() { }
In modules/user/templates/
, create the following passwordRequestSuccess.php
:
<h2>Receive your login details by email</h2> <p>Did you forget your password? Enter your email to receive your login details:</p> <?php echo form_tag('@user_require_password') ?> <?php echo form_error('email') ?> <label for="email">email:</label> <?php echo input_tag('email', $sf_params->get('email'), 'style=width:150px') ?><br /> <?php echo submit_tag('Send') ?> </form>
This form has to be accessible from the login forms, so add in each of them (in layout.php
and in loginSuccess.php
):
<?php echo link_to('Forgot your password?', '@user_require_password') ?>
Add the password request rule in the application routing.yml
:
user_require_password: url: /password_request param: { module: user, action: passwordRequest }
Form validation
First, we will set the validation rules for the form submission. Create a passwordRequest.yml
file in the modules/user/validate/
directory:
methods: post: [email] names: email: required: Yes required_msg: You must provide an email validators: emailValidator emailValidator: class: sfEmailValidator param: email_error: 'You didn''t enter a valid email address (for example: name@domain.com). Please try again.'
Next, have the passwordRequest
form being displayed again with the error messages if an error is detected by adding to the askeet/apps/frontend/modules/user/actions/actions.class.php
:
public function handleErrorPasswordRequest() { return sfView::SUCCESS; }
Handling the request
As described during day six, we will use the same action to handle the form submission, so modify it to:
public function executePasswordRequest() { if ($this->getRequest()->getMethod() != sfRequest::POST) { // display the form return sfView::SUCCESS; } // handle the form submission $c = new Criteria(); $c->add(UserPeer::EMAIL, $this->getRequestParameter('email')); $user = UserPeer::doSelectOne($c); // email exists? if ($user) { // set new random password $password = substr(md5(rand(100000, 999999)), 0, 6); $user->setPassword($password); $this->getRequest()->setAttribute('password', $password); $this->getRequest()->setAttribute('nickname', $user->getNickname()); $raw_email = $this->sendEmail('mail', 'sendPassword'); $this->logMessage($raw_email, 'debug'); // save new password $user->save(); return 'MailSent'; } else { $this->getRequest()->setError('email', 'There is no askeet user with this email address. Please try again'); return sfView::SUCCESS; } }
If the user exists, the action determines a random password to give to the user. Then it passes the request to another action (mail/sendPassword
) and gets the result in a $raw_email
variable. The ->sendEmail()
method of the sfAction
class is a special kind of ->forward()
that executes another action but comes back afterward (it doesn't stop the execution of the current action). In addition, it returns a raw email that can be written into a log file (you will find more information about the way to log information in the debug chapter of the symfony book).
If the email is successfully sent, the action specifies that a special template has to be used in place of the default passwordRequestSuccess.php
: return 'mailsent';
will launch the passwordRequestMailSent.php
template.
note
Had we followed the example of day 6, the verification of the existence of the email address should have been done in a custom validator. But you know that "There Is More Than One Way To Do It", and the use of the ->setError()
method avoids a double request to the database, and the creation of a much longer validation file.
So create the new template passwordRequestMailSent.php
for the confirmation page:
<h2>Confirmation - login information sent</h2> <p>Your login information was sent to</p> <p><?php echo $sf_params->get('email') ?></p> <p>You should receive it shortly, so you can proceed to the <?php echo link_to('login page', '@login') ?>.</p>
Send an email
Ok, so if a user enters a valid email address, a mail/sendPassword
action is called. We now need to create it.
Email sending action
Create a new mail
module:
$ symfony init-module frontend mail
Add a new sendPassword
action to this module:
public function executeSendPassword() { $mail = new sfMail(); $mail->addAddress($this->getRequestParameter('email')); $mail->setFrom('Askeet <askeet@symfony-project.com>'); $mail->setSubject('Askeet password recovery'); $mail->setPriority(1); $mail->addEmbeddedImage(sfConfig::get('sf_web_dir').'/legacy/images/askeet_logo.gif', 'CID1', 'Askeet Logo', 'base64', 'image/gif'); $this->mail = $mail; $this->nickname = $this->getRequest()->getAttribute('nickname'); $this->password = $this->getRequest()->getAttribute('password'); }
The action uses the sfMail
object, which is an interface to a mail sender. All the email headers are defined in the action, but as the body will be more complicated than a simple text, we choose to use a template for it - otherwise, we could use a simple ->setBody()
method.
Embedded images are added by a call to the ->addEmbeddedImage()
method, and the image path on the server, a unique ID for insertion into the template, an alternate text and a format description must be passed as arguments.
note
The sfMail
object is also a good way to add attachments to a mail:
// document attachment $mail->addAttachment(sfConfig::get('sf_data_dir').'/MyDocument.doc'); // string attachment $mail->addStringAttachment('this is some cool text to embed', 'file.txt');
You will find more details about the sfMail
object in the mail chapter of the symfony book.
Mail template
Once the action is executed, the mail view handles the defined variables to the sendPasswordSuccess.php
, which is the default HTML template for the email body:
<p>Dear askeet user,</p> <p>A request for <?php echo $mail->getSubject() ?> was sent to this address.</p> <p>For safety reasons, the askeet website does not store passwords in clear. When you forget your password, askeet creates a new one that can be used in place.</p> <p>You can now connect to your askeet profile with:</p> <p> nickname: <strong><?php echo $nickname ?></strong><br/> password: <strong><?php echo $password ?></strong> </p> <p>To get connected, go to the <?php echo link_to('login page', '@login', array('absolute' => true)) ?> and enter these codes.</p> <p>We hope to see you soon on <img src="cid:CID1" /></p> <p>The askeet email robot</p>
Just like in any other template, the standard helpers (like the link_to()
helper used here) work seamlessly in an email template. You can also insert any presentational HTML that you need to make the email look good.
Embedding an image is as simple as passing a sid:
parameter corresponding to the unique id of the image loaded in the action.
Alternate mail template
If the view finds a sendPasswordSuccess.altbody.php
, it will use it to add an alternate (text) body to the email. This allows you to define a text-only template for email clients not accepting HTML:
Dear askeet user, A request for <?php echo $mail->getSubject() ?> was sent to this address. For safety reasons, the askeet website does not store passwords in clear. When you forget your password, askeet creates a new one that can be used in place. You can now connect to your askeet profile with: nickname: <?php echo $nickname ?> password: <?php echo $password ?> To get connected, go to the login page (http://www.askeet.com/login) and enter these codes. We hope to see you soon on askeet! The askeet email robot
Configuration
The sfMail
being the view defined for this action, it can accept additional configuration. Create a mailer.yml
configuration file with:
dev: deliver: off all: mailer: sendmail
This stipulates the mailer program to be used to send mails, and deactivates the sending of mails in the development environment - the emails in the test data are fake anyway.
You don't want users to have direct access to this mailing action. So create a module.yml
in the module config/
directory with:
all: is_internal: on
Test
Test the new password recovery system by creating a custom user in the test data with your personal email, launch the import_data.php
batch.
Clear the cache and navigate to the password recovery page in the production environment. After entering your email address and submitting the form, you should receive the email shortly.
See you Tomorrow
The email system of symfony is both simple and powerful. Simple emails are as easy to send as possible; complex emails are no harder to write than complex HTML pages, and you take full advantage of the MVC architecture. So for your next emailing campaign, maybe you should use symfony instead of a commercial emailing solution...
Anyway, tomorrow will be the tag day. Askeet questions will be tagged, the tags will be searchable, and we will give you the nicest tag cloud that you have ever dreamed of.
As usual, today's code is available in the askeet SVN repository, tagged /tags/release_day_12
. We are still uncertain about what to talk during day 21, so post your suggestions to the askeet mailing-list or to the askeet forum.
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.