This topic is locked

Send 100,000+ email without worrying managing delivery!

6/23/2011 3:16:27 PM
PHPRunner Tips and Tricks
F
FunkDaddy author

I recently had to implement a solution for sending out mass emails to users in our system. I was initially using the default email server (Mail Enable free version) provided by our hosting service (on a virtual dedicated cloud machine) but was running into script timeout issues, slow foreach loops, and a complete lack of "email management" tools such as handling unsubscribes, bad email addresses, blacklisting, etc.
I needed a reliable way of sending large volumes of dynamic email to each user (ex: emails with each person's login info and other personal data). The problem was not only in how fast I could send emails to my outbox on my server (without bottle necking it), but also in ensuring that my emails "from address" (my domain) would not be eventually blocked by ISPS when some of these users either provided bad email address (invalid domains, etc), or marked my emails to them as spam, or a host of other things that can complicate reliable email delivery.
First, I turned to Amazon SES (Simple Email Service) because it offered an attractive pricing model: pay-per-use! Plus it was Amazon! After all, who else is better at handling cloud computing and massive data redundancy? So I created an account with their SES service, read their API, and successfully implemented their code by simply adding one or two files to my project's web folder and using a simple include in one of my PHPR "custom buttons" (you know the ClientBefore, Server, Client After feature). And voila... I was able to send emails using Amazon's server. However.... unfortunately, Amazon did not provide an easy and efficient way for me to manage what happened to my emails after they were sent. I'm not saying they don't have API calls that can help you handle that, however, it would require more work that I was able to put into my project given my client's time constraints and requirement priorities. I quickly realized that handling unsubscribes, bounces, bad email addresses, etc would take a little while to develop, test, and deploy. So, unfortunately, I began looking for another solution that I could implement halfway through my 2 week iteration cycle (yes, I'm an agile/scrum believer and work very hard towards having "something" done, tested, and deployed in a 2 week period)... lucky for me, I stumbled upon a service called Sendgrid.com. After reading about it online and seeing that Foursquare and several other start-ups were using them, I decided to sign-up for their free trial (allow me to send 200 emails per day just like Amazon SES did) and within 24 hours I was sold!
I quickly implemented their API with PHPR and had my solution working perfectly in 3 days (after tweaking based on my client's feedback). Actually, SendGrid.com gives me much more than I originally anticipated or even currently need. They provide what they call "apps" (I guess that is a hot word these days) which adds functionality to email such as automatically inserting footers, unsubscribe links, gravatars, and much more into your outgoing email.
Perhaps the greatest benefit of using SendGrid (besides the fact that they handle your email delivery) is that they also provide you with a web dashboard that lets you see what is happening to your mass mailings with plenty of analytics to support your efforts. They also ensure that bad email addresses or bounces are never sent again to prevent ISPs from establishing a pattern that your "from" address (your domain) is apt to sending bad emails or spam. This happens even when you generate a new mass mailer that may once again contain the culprit addresses that would go out for delivery. You don't even need to update your own database if you don't want to remove them, SendGrid will automatically skip over them to save you the hassle (though, I recommend you write some script to handle the emails they identify as bad or marked as unsubscribe since they will count the attempt to resend them against your monthly quota).
Anyhow, enough talk on my part, let's get to the good stuff. Here is how I did it in PHPR:
Step 1:

Create your free account with SendGrid.com
Step 2:

Read their API and download SwiftMailer (open-source and free).
Step 3:

Create a folder in your projects web folder called "swift" (in the root of your project folder. aka where you see classes, images, include, libs, etc)
Step 4:

Copy all of the contents inside the "lib" folder of the downloaded swift mailer folder and paste into your newly created "swift folder" (there should be 2 folders and 5 php files).
Step 5:

Create a folder in your projects web folder called "sendgrid" (in the root of your project folder. aka where you see classes, images, include, libs, etc)
Step 6:

Inside the "sendgrid" folder create the SmtpApiHeader.php" file as shown in the SendGrid developer API documentation
Step 7:

In the list page where you have the email records you want to send (this assumes that each of those list page records already contain your email data such as from, to, subject, body, etc) insert a custom button using visual editor.
Step 8

In that newly created button (I named mine "Email_Selected_Records") add the following code:



Client Before tab:

this.setEnabled(); //ensure button will work even after 1st time its clicked in case your page doesn't explicitly call for a refresh)

if (confirm ('Are you sure you want to email selected records now?\n\rThis may take 1 to 2 minutes to complete.'))

{

ctrl.setMessage("Emailing records... please be patient while we process your request. This can take between 60 to 120 seconds");

this.setDisabled();

} else {

return;

};


Step 9:

Tweak this code as you see fit. This is a mash-up for what I found in the PHPR online manual (for looping through selected records to send email) and the API provided by sendgrid).



On Server tab:

include_once('swift/swift_required.php');

include_once('sendgrid/SmtpApiHeader.php');

ignore_user_abort(true); //will run script even if user closes browser or disconnects

set_time_limit(300);

$hdr = new SmtpApiHeader();

//$hdr->addFilterSetting('footer', 'enable', 1);

//$hdr->addFilterSetting('footer', "text/plain", "Thank you for your business");

global $dal;

$dal_TableName = $dal->Table("letters_sub_tbl");

$body="";

$result["txt"]="";

$username = 'your_sendgrid_username';

$password = 'your_send_grid_password';

// Create new swift connection and authenticate

$transport = Swift_SmtpTransport::newInstance('smtp.sendgrid.net', 25);

$transport ->setUsername($username);

$transport ->setPassword($password);

$swift = Swift_Mailer::newInstance($transport);
foreach(@$keys as $keyblock)

{

$arr=split("&",refine($keyblock["LettersSubID"]));

if(count($arr)<1)

continue;

$arr2=array();

$arr2["LettersSubID"]=urldecode(@$arr[0]);

$where = KeyWhere($arr2);

$rstmp = $dal_TableName->Query($where,"");

$data=db_fetch_array($rstmp);
if($data["Parent1_Email"] > '' and filter_var($data["Parent1_Email"], FILTER_VALIDATE_EMAIL)){

//Custom part not part of the online manual example

$to=$data["Parent1_Email"];

$from=$data["Email_From"];

$subject=$data["Letter_Subject"];

$text = $data["Letter_Body"];

$html = "<html>".$data["Letter_Body"]."</html>";

$hdr->setUniqueArgs(array('from'=>$from, 'subject'=>$subject)); //MBR needed in order for mbr_sendgrid_bounce_action.php receiver page to capture "from" and "subject" so we can route bounce to correct person

$hdr->setCategory($from); //we want to categorize based on from email so we know which schools are sending which batches in sendgrid.

// Create a message (subject)

$message = new Swift_Message($subject);

// add SMTPAPI header to the message

$headers = $message->getHeaders();

$headers->addTextHeader('X-SMTPAPI', $hdr->asJSON());

// attach the body of the email

$message->setFrom($from);

$message->setBody($html, 'text/html');

$message->setTo($to);

$message->addPart($text, 'text/plain');

// send message

if ($recipients = $swift->send($message, $failures))

{

$t = microtime(true); //http://php.net/manual/en/function.date.php'>http://php.net/manual/en/function.date.php

$micro = sprintf("%06d",($t - floor($t)) * 1000000);

$d = new DateTime( date('Y-m-d H:i:s.'.$micro,$t) );

$sql = "UPDATE letters_sub_tbl SET Microseconds = '".$d->format("Y-m-d H:i:s.u")."',Emailed_On = now()WHERE LettersSubID = '" . $data["LettersSubID"]."'";

CustomQuery($sql);

}else{

//echo "Something went wrong - ";

//print_r($failures);

//$result["EmailsSent"] = 0;

$result["txt"] = print_r($failures);

}

}

if($data["Parent2_Email"] > '' AND filter_var($data["Parent2_Email"], FILTER_VALIDATE_EMAIL)){

//Custom part not part of the online manual example

$to=$data["Parent2_Email"];

$from=$data["Email_From"];

$subject=$data["Letter_Subject"];

$text = $data["Letter_Body"];

$html = "<html>".$data["Letter_Body"]."</html>";

$hdr->setUniqueArgs(array('from'=>$from, 'subject'=>$subject)); //needed in order for mbr_sendgrid_bounce_action.php receiver page to capture "from" and "subject" so we can route bounce to correct person

$hdr->setCategory($from); //we want to categorize based on from email so we know which schools are sending which batches in sendgrid.

// Create a message (subject)

$message = new Swift_Message($subject);

// add SMTPAPI header to the message

$headers = $message->getHeaders();

$headers->addTextHeader('X-SMTPAPI', $hdr->asJSON());

// attach the body of the email

$message->setFrom($from);

$message->setBody($html, 'text/html');

$message->setTo($to);

$message->addPart($text, 'text/plain');

// send message

if ($recipients = $swift->send($message, $failures))

{

$t = microtime(true);//http://php.net/manual/en/function.date.php'>http://php.net/manual/en/function.date.php

$micro = sprintf("%06d",($t - floor($t)) * 1000000);

$d = new DateTime( date('Y-m-d H:i:s.'.$micro,$t) );

$sql = "UPDATE letters_sub_tbl SET Microseconds = '".$d->format("Y-m-d H:i:s.u")."',Emailed_On = now() WHERE LettersSubID = '" . $data["LettersSubID"]."'";

CustomQuery($sql);

}else{

//echo "Something went wrong - ";

//print_r($failures);

//$result["EmailsSent"] = 0;

$result["txt"] = print_r($failures);

}

}

}


Step 10:

Provide feedback message to users!



var message = result["txt"];

ctrl.setMessage(message);

alert('Emails were sent successfully. Page will now reload.');

window.location.reload();


Done, now you have a button that sends emails using SendGrid's service... you can then login to your account and monitor all email activity and even include their apps for enhanced functionality without adding any more code (although, you may have to tweak some parameters in the On Server tab in step 9 to handle things like category, footer, etc).
Note: sending email via sendgrid actually uses their servers, however the email from shown to recipients will be the from address you provide. In some instances, it may show to recipient as "sent on behalf of' instead of your actual domain. SendGrid has a "whitelabel" wizard that allows you to update your mx records so that the email will always show as coming from your domain. I opted to forgo that since it seems that the "sent on behalf messages" occurs very infrequently.
I hope this helps others... and no, I am not affiliated with SendGrid in any way, shape, or form. I am simply a thankful customer of theirs, who appreciates the amount of time they saved me in solving my mass emailing issues.
Cheers,

admin 6/23/2011

Marcelo,
great article!
I second SendGrid, we use it to send all our newsletters (though we use a built-in email newsletter feature they provide).

F
FunkDaddy author 6/24/2011

Sergey,
Thanks for the praise. Yes, SendGrid has a bunch of other awesome add-on features such as the newsletter you just mentioned. I am definitely excited about using their product given how managing emails can sometimes become a full time project.
As a suggestion, perhaps you could share this tip on the xlinesoft blog with other users. Feel free to modify it in any way you see fit. If you want me to take some SendGrid screenshots, etc just let me know.
Cheers,

S
smez 6/28/2015

Hi Marcelo,
Thanks for this and the many other tips you add to the forum.
I am planning to use the Sendgrid api for a phprunner project. Since it is a while since you made this (2011) post I want to check if this is still optimal for PHPrunner 8.0. or if you have made any tweaks since?
Thanks
Graeme