PHP Bulk Email Sender (Simple Email Queue Daemon)

Once upon a time, a student created a “mass email sender PHP script” using AJAX long polling. Well, it works. But Master Coffee couldn’t help but laugh at the “browser must be permanently open on the server” design… There are better ways to do it, and here’s a quick and simple example. Let’s go!

 

CODE DOWNLOAD

I have released this under the MIT license, feel free to use it in your own project – Personal or commercial. Some form of credits will be nice though. 🙂

 

 

VIDEO TUTORIAL

 

 

PART 1) THE DATABASE

1-queue.sql
CREATE TABLE `queue` (
  `campaign` bigint(20) NOT NULL,
  `email` varchar(255) NOT NULL,
  `timestamp` datetime DEFAULT NULL,
  `status` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;

ALTER TABLE `queue`
  ADD PRIMARY KEY (`campaign`,`email`),
  ADD KEY `timestamp` (`timestamp`),
  ADD KEY `status` (`status`);

INSERT INTO `queue` (`campaign`, `email`, `timestamp`, `status`) VALUES
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL),
(1, '[email protected]', NULL, NULL);

First, create an “email send queue and delivery status” table. Here’s a simple one.

  • campaign Primary key, campaign ID.
  • email Primary key, the subscriber’s email.
  • timestamp Date/time when the email is processed.
  • status Status of email delivery.
    • NULL Not processed yet.
    • 1 Successfully sent.
    • 2 Send error.

 

 

PART 2) PHP EMAIL SENDER DAEMON

2-send.php
<?php
// (PART A) SETTINGS
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHAR", "utf8mb4");
define("DB_USER", "root");
define("DB_PASS", "");
define("DB_ERROR", PDO::ERRMODE_EXCEPTION);
define("DB_FETCH", PDO::FETCH_ASSOC);
define("CYCLE_EACH", 5);  // send 5 emails every batch
define("CYCLE_BATCH", 1); // pause 1 second before sending next email batch
define("CYCLE_CHECK", 5); // pause 5 seconds before next cycle

// (PART B) MYSQL DATABASE CLASS
class DB {
  // (B1) CONNECT TO DATABASE
  public $error = null;
  private $pdo = null;
  private $stmt = null;
  function __construct () {
    $this->pdo = new PDO(
      "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHAR,
      DB_USER, DB_PASS, [
      PDO::ATTR_ERRMODE => DB_ERROR,
      PDO::ATTR_DEFAULT_FETCH_MODE => DB_FETCH
    ]);
  }

  // (B2) CLOSE DATABASE CONNECTION
  function __destruct () {
    if ($this->stmt !== null) { $this->stmt = null; }
    if ($this->pdo !== null) { $this->pdo = null; }
  }

  // (B3) RUN SQL QUERY
  function query ($sql, $data=null) : void {
    $this->stmt = $this->pdo->prepare($sql);
    $this->stmt->execute($data);
  }

  // (B4) FETCH
  function fetch ($sql, $data=null) {
    $this->query($sql, $data);
    return $this->stmt->fetchAll();
  }
}

// (PART C) ENDLESS LOOP
while (true) {
  // (C1) GET QUEUE & SEND EMAIL
  $db = new DB();
  $batch = [];
  do {
    // (C1-1) GET EMAILS
    $batch = $db->fetch(
      "SELECT `campaign`, `email`
       FROM `queue`
       WHERE `status` IS NULL
       LIMIT " . CYCLE_EACH
    );

    // (C1-2) SEND EMAIL
    if (count($batch)>0) {
      foreach ($batch as $b) {
        $ok = @mail($b["email"], "Title", "Message");
        $db->query(
          "UPDATE `queue` SET `timestamp`=?, `status`=? WHERE `campaign`=? AND `email`=?",
          [date("Y-m-d H:i:s"), $ok ? 1 : 0, $b["campaign"], $b["email"]]
        );
      }
      sleep(CYCLE_BATCH);
    }
  } while (count($batch)>0);

  // (C2) NEXT CYCLE
  unset($db);
  sleep(CYCLE_CHECK);
}
  1. Database and “email batch processing” settings – Remember to change these to your own.
  2. A simple class library to work with the database.
  3. Yes, the “mass mailer” is essentially an endless loop.
    • (C1) Connect to the database, get the emails to send, and loop until the queue is empty.
    • (C1) For this example – We send 5 emails every batch and wait for 1 second before sending the next batch.
    • (C2) When the queue is empty, the endless loop turns into “passive mode”. Check the queue every 5 seconds.

 

 

PART 3) LAUNCH EMAIL DAEMON IN THE COMMAND LINE

Finally, launch php 2-send.php in the command line. The smarter way is to run this on system startup, we will not walk through the details, but a quick summary:

  • (Windows) Run shell:startup, create an email.bat file with one line – php PATH/TO/2-send.php.
  • (Linux) Similarly, create an email.sh file – php PATH/TO/2-send.php. But depending on your distro, setting it to run at startup will be different – Check out this guide.
  • (Mac) Do your own research. 😛

 

 

THE END – POSSIBLE IMPROVEMENTS

That’s all for this short tutorial and sharing. Before you deploy this on a live production server, there are plenty of things to complete on your own.

  • Go ahead and increase CYCLE_EACH. Modern servers are capable of handling way more than 5 emails per second… Just don’t flood and crash your SMTP server.
  • Tie in with your newsletter system.
  • Load the email template from a flat file or the database – Check out my PHP email template tutorial if you want.
  • For simplicity, I have used the default PHP mail(). Go ahead and use PHPMailer if you want.
  • Add “priority” to the queue, and maybe a “resend” function.
  • The slightly more advanced folks might want to check out ReactPHP – Turn this daemon into an API endpoint or worker.

 

 

CHEAT SHEET

Bulk Send Email in PHP (click to enlarge)