SimpleMailer Forum Board

This is the forum board for the SimpleMailer Yii Extension.

SimpleMailer is a module that helps the administrator on creating and delivering emails from your application. It is created because the need of a simple emailing system tightly integrated to the Yii apps I develop. After many unsuccessful tries to integrate PHPList to my apps I decided to write a solution that can fit in my shoes.

The module, and its documentation can be found at the SimpleMailer extension page.

Please test my freshly released version 0.1 of this Module and don’t hesitate to give me any sort of feedback (Bug Reports, Feature Requests, and so on).

Enjoy.

Nice! Will have to try this.

Have you thought about allowing inbound emails too?

Not really. I’ve created this extension with a delivery-only pattern in mind.

I hope you’ll find this module useful. :)

Hi chirvo,

Thank you for the extension. This is my first implementation of mail-queues, so thanks to you I’m having a good start point :)

I just started to dig into the code and have a couple of minor notices, mostly documentation related

  1. Seems, in command to create migration option ‘templateFile’ should rather be used right after the command, e.g.



php yiic migrate create  --templateFile=application.modules.SimpleMailer.migrations.template add_simple_mailer_tables



  1. Small typos in configuration:



'SimpleMailer' => array(

            'attachImages' => true[b];[/b]

            'sendEmailLimit'=> 500[b];[/b]

        ),

Should be comma(s) instead.

Also at the moment I’m getting Error 404 trying to reach http://DOMAIN/SimpleMailer. I’ll give it a closer look tomorrow and will let you know if it was just a problem in my own implementation (which is highly possible) :).

Regards,

Yuga

Hello yugene,

I’ve fixed the glitches you found in the documentation. Thanks for pointing them out.

Ok, don’t forget to keep me updated about the (possible) routing/url error. Maybe you just found another outstanding bug. Who knows? :)

Also, since you’re also looking to implement email queues, if you have any suggestions or ideas about how to improve the extension I’ll be more than glad to hear them.

Have a good day Yuga,

Hi Chirvo,

Well, yes, appeared that’s something you’d better to update :), cause anyone on Linux machine won’t be able to reach the module because of case-sensitive file-system. If I rename module to ‘mailer’, for example, use this name in config file as module ID and rename module main file and class respectively (MailerModule), then I can reach the module without any hassles.

I didn’t dig deeper yet which way exactly paths are resolved to look for an appropriate module, so can’t say for sure the definite cure, but ‘simpleMailer’ won’t work too.

I had the same problem not so long time ago with one more module that had uppercase letters in its name, but that time I didn’t have time to investigate the problem and just needed some pieces of the code from it. Happy this time I found the cause :)

Will work today on mailing system implementation, so will let you know if will find / invent anything interesting.

Sincerely,

Yuga

That’s weird. I use Linux here and in my servers, didn’t get any issues with SimpleMailer and broken URLs. Anyway I’m gonna put an eye on this.

Thanks for your response.

Have a good day Yuga. :)

Strange then, on some free time will look into it.

Anyway, I have some results to share with you.

First of all, I really like the functionality provided with your module, it gave me even more then I even was thinking about. I think my client may like the idea of Mailing Lists as well :).

Second, I found 2 bugs so far, both are db related:

  1. sm_queue misses headers field, which is used in the code.

  2. some indexes are wrongly set to be unique, these ones should be fixed to :




		$this->createIndex('idx_sm_queue_to', 'sm_queue', 'to', false);

		$this->createIndex('idx_sm_queue_subject', 'sm_queue', 'subject', false);

		$this->createIndex('idx_sm_queue_status', 'sm_queue', 'status', false);



Third, I updated the code a bit as

a. I want/need to support variables in all template attributes.

b. I want/need to support global, application specific variables.

I added


public $globalReplacements = array();

to SimpleMailerModule

and assigned vars to it in init(), e.g.





	public function init() {

		$this->setImport(array(

			'mailer.models.*',

			'mailer.components.*',

		));

                $this->globalReplacements = array(

                    '__appname__'=>Yii::app()->name

                );

	}



Updated compileMultipartTemplate():





	/**

 	* Heavily based on http://stackoverflow.com/questions/1606588/how-to-attach-and-show-image-in-mail-using-php and

 	* http://www.phpeveryday.com/articles/PHP-Email-Using-Embedded-Images-in-HTML-Email-P113.html

 	* @param MailerTemplate $template the template instance

 	* @param array $template_partials HTML code to be inserted in the email. array('__key_for_partial__' => '<h1>html</h1>').

 	*        Defaults to array();

 	* @return array Contents: array($template, $headers)

 	* @throws CException

 	*/

	public static function compileMultipartTemplate($template, Array $template_partials = array(), Array $template_vars = array()) {

		$attach_images = Yii::app()->getModule('admin')->getModule('mailer')->attachImages;

		if (!is_a($template, 'MailerTemplate')) {

			throw new CException('Mailer::compileMultipartTemplate(): '. Yii::t(

				'mailer',

				'Wrong object passed, expected MailerTemplate instance.'

			));

		}

		if (is_array($template_partials) && !empty($template_partials))

			$template->body = strtr($template->body, $template_partials);


		//Then substitute the template variables with actual data

                $template_vars = array_merge($template_vars, Yii::app()->getModule('admin')->getModule('mailer')->globalReplacements);

		if (!empty($template_vars)){

			$template->body = strtr($template->body, $template_vars);

			$template->alternative_body = strtr($template->alternative_body, $template_vars);

                        $template->subject = strtr($template->subject, $template_vars);

                        $template->from = strtr($template->from, $template_vars);

                }

                

		$boundary = md5(uniqid(time()));


		// Substitute every url for img tags with a cid in $template->body

		if ($attach_images) {

			$paths = array();

			preg_match_all('~<img.*?src=.([\/.a-z0-9:_-]+).*?>~si', $template->body, $matches);

			foreach ($matches[1] as $img) {

				if (strpos($img, "http://") == false) {

					$content_id = md5($img);

					$url = parse_url($img);

					$paths[] = array(

						'path' => $_SERVER['DOCUMENT_ROOT'] . $url['path'],

						'cid' => $content_id,

					);

					$template->body = str_replace($img, 'cid:' . $content_id, $template->body);

				}

			}

		}


		// Multipart header

		$headers = "MIME-Version: 1.0\r\n";

		$headers .= "Content-Type: multipart/alternative; boundary=\"PHP-alt-$boundary\"\r\n";

		$headers .= "From: " . $template->from . "\r\n";

		$headers .= "X-Sender-IP: $_SERVER[SERVER_ADDR]\r\n";

		$headers .= 'Date: ' . date('n/d/Y g:i A') . "\r\n";


		// Text-only body

		$multipart = "--PHP-alt-$boundary\n";

		$multipart .= "Content-Type: text/plain; charset=utf-8\n";

		$multipart .= "$template->alternative_body\n\n";


		$multipart .= "--PHP-alt-$boundary\n";

		$multipart .= "Content-Type: multipart/related; boundary=\"PHP-related-$boundary\"\n\n";


		// HTML body

		$multipart .= "--PHP-related-$boundary\n";

		$multipart .= "Content-Type: text/html; charset=utf-8\n";

		$multipart .= "Content-Transfer-Encoding: Quot-Printed\n\n";

		$multipart .= "$template->body\n\n";


		// Images as attachment

		if ($attach_images) {

			foreach ($paths as $path) {

				if (file_exists($path['path']))

					$fp = fopen($path['path'], "r");

				if (!$fp) {

					throw new CException('Mailer::compileMultipartTemplate(): '. Yii::t(

						'mailer',

						'Cannot open file ')

					. $path['path']);

				}


				$image_type = substr(strrchr($path['path'], '.'), 1);

				$file = fread($fp, filesize($path['path']));

				fclose($fp);


				$message_part = '';


				switch ($image_type) {

					case 'png':

					case 'PNG':

						$message_part .= "Content-Type: image/png";

						break;

					case 'jpg':

					case 'jpeg':

					case 'JPG':

					case 'JPEG':

						$message_part .= "Content-Type: image/jpeg";

						break;

					case 'gif':

					case 'GIF':

						$message_part .= "Content-Type: image/gif";

						break;

				}


				$message_part .= "; file_name = \"" . $path['path'] . "\"\n";

				$message_part .= 'Content-ID: <' . $path['cid'] . ">\n";

				$message_part .= "Content-Transfer-Encoding: base64\n";

				$message_part .= "Content-Disposition: inline; filename = \"" . basename($path['path']) . "\"\n\n";

				$message_part .= chunk_split(base64_encode($file)) . "\n";

				$multipart .= "--PHP-related-$boundary\n" . $message_part . "\n";

			}

			$multipart .= "--PHP-related-$boundary\n\n";

		}

		// Closing compiled email template

		$template->body = $multipart. "--PHP-alt-$boundary--\n";


		return array($template, $headers);

	}



Updated process():





	private static function process($to, $template_name, Array $template_vars = array(), Array $template_partials = array(), $action = 'send') {

		if (is_string($to)) {

			$to = array($to);

		}

		/** @var $template MailerTemplate */

		$template = MailerTemplate::model()->findByAttributes(array(

			'name' => $template_name,

		));

		if (!$template) {

			throw new CException(Yii::t('mailer', 'Template does not exists.'));

		}

		//Template compilation

		list($template, $headers) = self::compileMultipartTemplate($template, $template_partials, $template_vars);

                $statuses = array();


                foreach ($to as $receiver) {

			switch ($action) {

				case 'send':

					$statuses[$receiver] = mail($receiver, $template->subject, $template->body, $headers);

					break;

				case 'enqueue':

					$queue = new MailerQueue;

					$queue->to = $receiver;

					$queue->subject = $template->subject;

					$queue->body = $template->body;

					$queue->headers = $headers;

					$queue->status = MailerQueue::STATUS_NOT_SENT;

					$statuses[$receiver] = $queue->save();

					break;

				default:

					break;

			}

		}

		foreach ($statuses as $email => $status) {

			if (!$status) {

				Yii::log(Yii::t('app', 'Failed to deliver email: '.$email), 'error');

				return false;

			}

		}

		return true;

	}



Please let me know if you have any comments on these changes.

Okay, next - are just small comments, maybe you’ll get any useful information from it:

  1. I moved Send Preview Email above the iframe, as iframe has large height and it’s not obvious there’s more beneath.

  2. I manually updated captions/links everywhere to something more neutral, e.g. ‘Manage templates’

  3. I use your module as a nested module, so to get properties values I call


Yii::app()->getModule('admin')->getModule('mailer')->sendEmailLimit

, which is quite long n such case :)

  1. Also, well, sorry, but I renamed all classes to a shorter variant, e.g. ‘MailerXxxx’, Mailer::process(), hope you won’t mind it :)

  2. Also, my idea is maybe later (if will be needed) to implement SwiftMailer to be used with your module.

That’s all for now.

Thank you for your module and hope my comments will be helpful somehow.

Back to mailing…

Hi Chirvo,

there’re some more minor updates, maybe you would like to implement some of them into the library:

  1. added getStatusName() to SimpleMailerEnqueue, to show not 0/1 but status text name.

  2. in MailerCommand





			if (!$emails) {

				Yii::log(Yii::t('mailer', 'No emails in queue. Exiting.'), 'error', 'application.commands.MailerCommand');

				exit(0);

			}



error level is changed to CLogger::LEVEL_INFO

  1. template name is read-only on update, to avoid clashes with one name in the code and the other in templates.

  2. Really minor:


 $email->update(array('status'));

instead of save(), MailerCommand.

Cheers

Thanks Yuga for all those tuneups. I’m gonna review and merge them into the code. :)

Also I created a Github repository for the module. You can find it here:

I like the idea of changing the names of classes to something shorter. Being a KISS solution it’s funny that one has to type ReallyLongNamesForClasses every time. :) Gonna use EMailer* to stick to the recommendations for creating Yii extensions. Also I’m gonna do the small changes in the views. You’re right with your observations.

You made a great contribution Yuga. Thank you again! :D

BTW I think I might know why you couldn’t access to SimpleMailer via http://your_domain/SimpleMailer, Yuga. Since you’re using SimpleMailer as a nested module in your admin module, the route should change to http://your_domain/admin/SimpleMailer.

Well, you managed to fix that for your application, but I want to have a record of this for future users with the same issue.

Hi Chirvo,

Well, I first installed the module NOT as a nested one and only moved it after I could reach it successfully, so it’s not the case.

So, back to the thinking chair! If you come up with something about this don’t forget to tell me. :)

I think I’ll have some time to debug the issue today, so will let you know any results.

Thanks for taking some time of your weekend to try to figure out what happens there. I also gonna do a fresh installation of the module (I already made some changes you suggested) and gonna see what happens.

Hi Chirvo,

I’ve found the solution, and it’s quite simple.

Usually I don’t change




         'urlManager' => array(

                    	'caseSensitive' => false,

   	)



this parameter, and I didn’t give enough attention the previous developer changed it :)

Docs are quite descriptive about the change:

So, maybe it’s a good idea to put a notice about possible problems when installing a module if this parameter is changed… It may save some time to other developers, I think. I will add this notice to modules description in guide as well.

You rock. I’m gonna put the solution in the FAQ.

Thanks in advance!

Cheers,

Hi Chirvo,

I mostly finished integration of the library, only final tests on production are left (if client won’t request additional functionality).

Module works stable and allowed to implement very convenient features just in few hours, so Thanks a lot one more time :)

Two more small comments:

  1. I finally moved Send Preview right after the caption, and it looks more natural for me there.

  2. I added Array $testData to TemplateController, and now previews and test emails are rendered with this test data (+ merged with global vars), it think it gives more natural view of templates, especially taking in notice a site admin ( who will handle templates ) isn’t a programmer.

Cheers,

Yuga

hey … this is a perfect example and i want to implement just this … the problem is i am not able to make it work … in the usage when you say go to http::/yourdomain/SimpleMailer/createnewtemplate …i can reach till simplemailer and then there is no create new template or how do i add the template and access simplemailer… can you please send an example code or demo … or can you please help me make it work … thanks

To add a new template you need to access to http://yourdomain/SimpleMailer, then click on "Manage Templates" > "Create Template" and fill up that form.

SimpleMailer relies on the //layout/column2 layout file to render its pages correctly. If you’re not getting the right menu with the navigation options it can be related to your application layout.

Hope this help you solving your issue.

Cheers,