A solution for e-mail sending in CakePHP
Posted on 5/8/06 by Felix Geisendörfer
Deprecated post
The authors of this post have marked it as deprecated. This means the information displayed is most likely outdated, inaccurate, boring or a combination of all three.
Policy: We never delete deprecated posts, but they are not listed in our categories or show up in the search anymore.
Comments: You can continue to leave comments on this post, but please consult Google or our search first if you want to get an answer ; ).
Warning: This code has been produced in the time between 2am and 6am *without* the influence of coffee. Usage on your own risk ; ).
One of the things when I always felt like I wasn't quite following the DRY(don't repeat yourself)-concept when working with CakePHP was sending out emails. I always ended up with pretty long controller functions for every mail, since I tried to use CakePHP's View class to render my mail templates, and I always had to create the $header and a couple other things.
Now even worse was, that when I finished one of those mail() actions, I felt like it wasn't very reusable, even within the application. Something like reminder messages might need to be send from several Controllers/Actions, which wasn't really confortable to do.
So the last couple of days I've been thinking about the best way to approach mailing in cakephp. I always felt that an E-Mail was something to model, so I felt like implementing it as a Model. On the other Site, I wanted the ability to use the CakePHP render() functionality for my mail templates, which would be easier to do from within a Component (Controller).
Today I finally decided to see if Rails provided a native way for e-mailing and I was pleasently surprised when I found the documention of their ActionMailer class. Their solution to the Problem is to use a kind of hybrid between a Model and a Controller, which provides the base class for one or more app-wide mailer-models. This basically means that you create a new class in your app/models path, and extend the ActionMailer. After that you start putting your own mail (domain) logic into this Model which then can be used from within any Controller to send the kinds of mails you define. One special thing about this mail models is, that they have their own folder named after them inside views, just like a controller, where their mail templates rest.
I liked the concept, so I took it as an inspiration for an ActionMailer for CakePHP. The usage turns out to be as simple as this:
app/models/app_mailer.php
class AppMailer extends ActionMailer
{
var $name = "AppMailer";
function contact_form($contact_info)
{
// Standard Mail stuff
$this->addRecipient('contact@business.com');
$this->setSubject('Web Form Contact');
$this->setSender($contact_info['email'], $contact_info['name']);
// Custom variables for our mail template
$this->set('name', $contact_info['name']);
$this->set('email', $contact_info['email']);
$this->set('phone', $contact_info['phone']);
$this->set('message', $contact_info['message']);
$this->set('ip', env('REMOTE_ADDR'));
$this->set('browser', env('HTTP_USER_AGENT'));
}
}
app/controllers/mail_controller.php
{
var $name = "Mail";
var $uses = array('AppMailer');
function contact()
{
// Notice the 'send_' prefix in front of our AppMailer function!
if ($this->AppMailer->send_contact_form($this->data['Contact']))
{
// Mail was sent successfully
}
else
{
// Some error occured
}
}
}
app/views/app_mailer/contact_form.thtml
<b>Mail:</b> <?php echo $email; ?><br/>
<b>Phone:</b> <?php echo $phone; ?>
<b>Browser:</b> <?php echo $browser; ?><br/>
<hr>
That's it. Highly resuable and easy to integrate.
Now I do have to admit that the ActionMailer could use a couple additional functions like attachments and multi-part email handling as well as integration with alternative mail engines (not only php's mail() function). But other then that, it might already worth for you to take a look at it.
Download
Download the code (v. 0.7.3) at CakeForge and put it into app/vendors/action_mailer.php.
Note: I just updated the code with some bug fixes, the new version is 0.7.3.
Usage
Have a look at the AppMailer example above. You need to have a folder inside views with the underscored name of your mailer (app_mailer in the example). In this folder you have to put the templates for your Mailer functions (like you do with controller actions). Each function represents one type of e-mail your application sends out.
In your Controller include the Mailer model you've created by using the var $uses; array. After that you can send your e-mails by calling $this->{Mailer}->send_{mail_function}();. Instead of
'send_' as a prefix to all your functions, you can also use 'deliver_'.
Important: In order for the ActionMailer to work you will have to create a new layout in your app/views/layouts folder called mail.thtml. You can change this name by overwriting ActionMailer::layout.
Build-in functions
As of right now, the following functions can be used inside the Mailer model:
ActionMailer::addRecipient($email, $name = null): Add's an email adress (and eventually name) to the list of recipients. Info: I had some problems using $name on my Win32 dev box, you might want to leave it blank in the beginning.
ActionMailer::setRecipient($email, $name = null): Just like addRecipient, the only difference is that it will remove all previously set recipients before adding the new one.
ActionMailer::setHeader($name, $value): Set's an http header for the email that is sent. Use it like this: ActionMailer::setHeader('Content-type', 'text/html');
ActionMailer::set($one, $two = null): Set's a variable to make it available to the mail template. Similiar to Controller::set();
ActionMailer::setSender($email, $name = null): Set's the FROM field of the email. Problems like mentioned with addRecipient do not seem to appear.
ActionMailer::setSubject($subject): Set's the subject of the email.
ActionMailer::reset($subject): Resets header, recipients, sender and data of an email. Automatically called before send_{functions} are being executed.
One more thing
Some people might think this is a horrible solution as it violates some of the fundamental MVC rules. I agree, it does do that, but I also think that it is legitamate in terms of DRY and RAD'ness and doesn't seem to conflict with the other things going on in ones application. So therefor I believe this is one of the things where compromises have to be made for the greater good ; ). Anyway, if you can think of a better solution, let me know, and I'll see what I can do.
--Felix Geisendörfer aka the_undefined
You can skip to the end and add a comment.
Hi Daniel,
yes, I used a wrong path for the MailController, I corrected it above. Regarding the view: I didn't show the view of the MailControlller::contact() action but from the AppMailer::contact_form() function. The controller view would be inside app/views/mail/contact.thtml, I just skipped it. But the AppMailer has it's own view folder, since it's a Controller/Model hybrid.
Concerning the 'send_' prefix: I think you could also call:
$this->AppMailer->reset();
$this->AppMailer->contact_form($this->data['Contact']);
$this->AppMailer->send();
or:
$this->send('contact_form', $this->data['Contact']);
But do you want to? I think the 'send_'/'deliver_' prefix is a convenient shortcut helping the RAD'ness in your app ; ). Any comments on it besides that *g*?
Have you tried using phpmailer which has all the additional functions like inline attachments etc. You could aslo use fck editor for composing the messages.
(http://wiki.cakephp.org/tutorials:sending_email_with_phpmailer)
Note: This comment was on the wrong post, I moved it to the correct one.
h: You know, a while ago I was really into taking all those 3rd party scripts, writing a little cake interface for them, and getting the job done very fast. It was DRY, RAD and still ... causing a lot of problems.
One of the first problems were libraries that would use conflicts with functions in CakePHP. MagpieRSS (an RSS reader/parser) for example used it's own debug() function, so I had to go in and uncomment it. Now when updating libraries like that, I always had to repeat those (sometimes tidious) steps in order to make things work. I hated it.
Another thing was bloat: A lot of those libraries where really great and did everything I needed, but they where just way to big, making my app more and more bloated, even so I was just using a fraction of their functionality.
The probably most annoying thing were licenses. Most OS scripts I found where either GPL or LGPL, which means more or less restrictive (especially GPL which is no free software license to me). Very few of them, like CakePHP, were licensed under my favourite and least restrictive license, MIT (or BSD). Since one of my sideprojects is a commercial application that is maybe going to be sold several times, I need as much rights over the code I'm using as possible.
Oh and before I forget: Some of those 3rd party libraries are poorly written and extremly hard to debug. I don't claim that my code is perfect, but I know what it does, it's following the (old) CakePHP coding standards, and I have a better feeling when using it. With those 3rd party apps you just never know when you're creating a Security vulnerbility in your code unless you go through them very carefuly.
So last but not least: Since I started writing my own solutions for things like e-mail/rss parsing/etc. I've improved my knowledge about this technologies a good bit, and looking at all the trouble I had with the 3rd party libs, I've not been slowed down by it a lot. I'll still ocassionally use foreign code in my app's when necessary, but whenever I can, I'll write my own one ; ).
This may border on a philosophical debate, but it seems to me email really belongs in a component, not a model/controller. Vary rarely are my email-sending chores as simple as a contact page, and at least in my current project, none of them come from their own urls—they're all part of other actions on other controllers elsewhere in the app. Email is an action, not a model.
Now, I understand that requestAction could do it in this case, or (ick!) manually instantiating ActionMailer, but this really seems like a job for a component. (It'll certainly break the MVC less).
Chris Renner: Thanks for your comment. I've thought about using a component as well, let me explain you why I didn't do it:
A component is esentially an enhancment for a (specific) controller since it's normaly directly linked to it by using the startup() function. Now you could create all of your email functionality inside a component, no question about it. But for each type of e-mail your application sends out, you would have to setup the email details inside your controller actions. Now if you have the same kind of kind of email and want to send it from different controller actions, you would esentially repeat yourself by replicating the setup (from, to, subject, etc.).
But when you really look at it, sending emails is it's own Domain by itself, that should be encapsulated seperatly from your controller logic. Having all your email specific code in one common Model (or maybe more), makes it much easier for you to maintain your mailing code. It keeps you from repeating yourself, provides you with a very nice convention, and makes adding a new type of email very fast.
I also don't get why you say that requestAction would be needed for the ActionMailer. You simply include your Mailer Model (i.e. AppMailer) in the var $uses; array of your controllers, and call it's functions by using the 'send_' prefix and any kind of parameters needed to send the email.
Again: sending an email is an action, and therfore triggered from within the controller. The email itself is something that can (and should) be modeled, and therfor goes into it's own Model class.
Maybe you can give me a scenario where a component approach would be easier to handle then the Controller/Model hybrid one (as it's being done in Rails as well).
Have you see the mail template solution that Rossoft did? http://rossoft.wordpress.com/2006/07/01/mail-templates/
Mladen Mihajlovic: Yes I did. I don't like additional markup's in my views, especially not when they are essentially containing Model/Controller logic. His solution also depends on the PHPMailer, which I try to avoid for the reasons mentioned above.
Also, line 299 of action_mailer.php:
$this->render ('contact_form');
Should be:
$this->render ($fct);
Brandon: Now, that does make me look sort of stupid, doesn't it? : P. I fixed it and uploaded the new version to CakeForge.
[...] On the weekend, Felix Geisendörfer (aka the_undefined) presented an email solution for CakePHP based on the ActionMailer class from Ruby on Rails. It inspired me to think about this topic. [...]
Can we add a live chat to my site?
Dominic C: What are you talking about? Are you some kind of weird spam bot ; ) ?
I installed action_mailer.php to the app/vendors directory, created my model in models/mailer.php, and included the action_mailer vendor file, but this is the error I'm getting:
Fatal error: Method ActionMailer::__call() must take exactly 2 arguments in /Applications/MAMP/htdocs/SportsLeader/app/vendors/action_mailer.php on line 374
So, I removed the third argument in ActionMailer::__call and I don't get that error. What gives? Am I breaking the code by doing this?
Felix - I come from the Rails arena and I say keep it AS IS! (except adding a few things like multipart functionality.)
I *like* your current implementation, its simple and clean - and best of all, just like Rails, hehe.
I initially thought it was weird that Rails mailer classes were models, when they're kinda/sorta controllers, but hey, it just WORKS. And no need to complicate the syntax. Less is more. send_ = beauty.
This also belongs in CakePHP core, IMO.
Felix, is there a reason why you did not follow the Cake naming convention in naming the MailController? If I understand correctly, the controllers are named plural. So I'd think that it might be named as mails_controller.php (MailsController) even though I'm not quite sure if there is a plural for mail.
steve: Actually no. I just needed a Controller name for the example to post here and thougtht "MailController" would do. You are probably correct that "MailsController" would be more cakish. And since PhpNut_ is working on an E-Mails component for 1.2.x anyway, there should be an integrated method for sending mails in the framework soon as well.
[...] That said, you can use it anyway with a nifty surrogate technology called CakePHP. I’ve had a chance to start toying with it, and so far the down side I see is the absence of built-in database migrations (although a somewhat primitive migrations snippet is available here). There is also no pre-packaged equivalent to Rails’ ActionMailer, and I don’t yet know whether the independently developed solution is any good. [...]
It took me time to make it work ...
Do not forget to put into your mail.thtml layout and the .thtml from your app_mailer function will be rendered ...
hey it removed my code ...
just put echo $content_for_layout;
DAMN, I was becoming crazy. What Fraisoulle says just above is VERY IMPORTANT! I missed it. What kind of stupid am I? Don't answer.
Felix, don't get dizzy, but your current code has some stupid mistakes, enough for me to get it blamed as "errorneous 3rd party libraries". Really, each next programmer thinks he/she is smarter then "them". =)
notable: line 105-110 at v.0.7.3 (btw, it still has 0.7.2 in header)
if (!is_array($one))
{
$data = array($one => $two);
}
$this->data = array_merge($this->data, $data);
return $data;
should be replace with something like this:
if (!is_array($one))
{
$data = array($one => $two);
$this->data = array_merge($this->data, $data);
} else {
$this->data = array_merge($this->data, $one);
}
return $this->data;
Anyway, I like your implementation, beacuse of
1. it is small and thus easily reviewable. Yep, 'h', It has no attachments, thought I don't need it and thus I don't need to fix any possible errors out there.
2. it has rudimentary 'anti-822-headers-injection' routines, which is the thing I lacked in PHPMailer however codebloated it was. Strictly speaking this is not standard compliant implementation, meaning it does not support header folding syntax, but hey, that's pretty enough for most cases.
3. it look reusable. Really, creating special mail controllers and accessing them via requestAction is kind of shooting your leg off. And creating components really doesn't save you from code duplication: to be properly used you anyway should layout your component input data. So, reuse is really possible when you incapsulate this data in some model, say 'user', 'order' and then, 'user registration email', 'order confirmation email' really look like subclasses/flyweights of those models.
The only pitfall I see is that this model coupling may not be applicable with grouplists or forums, and, yes, attachments also fell out of this scheme - because it is really aggregation/presentation issue.
Good job!
one more bug:
if (!isset($headers['Content-type']))
$this->setHeader('Content-type', 'text/html; charset=utf-8');
==>
if (!isset($this->headers['Content-type']))
$this->setHeader('Content-type', 'text/html; charset=utf-8');
Just wanted to let you know I uploaded version 0.7.4 to cakeforge, fixing the headers bug.
Hmm Nice..
I think Mailing is one of those things that are Programmer'taste's specific.
Good for you if you learnt things while developping it, and yes, there are some bloated 3rd party libs outta there, but hey, not all of them, hopefully! or else, people that are outside the Cake world would thing it's bloated! Oh Ma!
Anyway, I believe Swift Mailer is a nice Mailing class, really clean IP.
and added a swiftMailer Component to bakery
The component:
http://bakery.cakephp.org/articles/view/192
The docs:
http://bakery.cakephp.org/articles/view/193
It basically allows to views, or wrap a body msg with a layout, embed images on the fly etc. useful for a newsLetter, for example.
That is just my taste at the moment.
Regards
Greetings Felix!
I have recently moved to using CakePHP for many of my projects, and was happy to see that in the 1.2 release there is now an email component. What do you think of the new email component - in comparison to your method?
Hope all is well....
Nate K: Rock n' Roll nate! I was hoping you'd do that ; ). Anyway, the Cake 1.2 email component is much superior to my attempt here and I highly recommend you to use it! Good to see you around ; ).
i have use this code. email was send but no send data in contact_form.thtml. why ?
I'm getting the same error as Joel
Using the latest version from cakeforge v. 0.7.4)
I also removed the third argument in ActionMailer::__call like he suggested and it seems to work fine, but not sure what other effects this has.
Fatal error: Method ActionMailer::__call() must take exactly 2 arguments in /cakeapps/company/vendors/action_mailer.php on line 374
josh: This might be due to some recent changes in CakePHP. However if you check out Cake 1.2 there is an excellent new EmailComponent which I would recommend you.
I uploaded a new version (0.7.5) to cakeforge, fixing a bug when trying to set an array of data.
Chris: cool - thx.
By the way, with that last bugfix, I think it's now entirely possible to change ActionMailer to extend AppModel instead of Object, and use validation and other model features on it. If you're not actually saving to DB, you'll still need to set $useTable to false.
Hi, frist great work, and second I could nor make it work, I follow your instructions, and I got this error.(using version 0.7.5 )
Fatal error: Method ActionMailer::__call() must take exactly 2 arguments
the same above, so I delete the third parameter in that function and the error went away, but now there is another, I think, it shows
"Missing Layout
You are seeing this error because the layout file can't be found or doesn't exist.
Notice: If you want to customize this error message, create app\views/errors/missing_layout.thtml.
Fatal: Confirm you have created the file :"
What file ????
And I did not undertand what was trying to say the commente above
"Fraisouille said,
It took me time to make it work …
Do not forget to put into your mail.thtml layout and the .thtml from your app_mailer function will be rendered …"
could you explain this better?
Leonardo: If you are using cake 1.2 please try out the new EmailComponent - it's much better then this solution. From what you wrote I cannot really figure out what is going wrong. It has also been over a year that I wrote this post.
Leonardo: You need a "mail.thtml" file in your layouts folder. I went very simple, so all my contains is : "".
Very clever of me. Mine contains "echo $content_for_layout;" within php tags.
[...] Отправка почты [...]
Your solutions are superb and help me a lot to work thru cake....Thanks
This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.
The solution looks nice, but I am a bit confused with the folders you use. Shouldn't the controller be in the controllers folder? And the "view" in app/views/mail/contact_form.thtml?
And I don't see the reason why you introduced the "send_" prefix. I don't like it ;-) Why don't you use the same function names as in the "model"?