Making PHP's mail() asynchronous

2019-02-02 01:38发布

I have PHP's mail() using ssmtp which doesn't have a queue/spool, and is synchronous with AWS SES.

I heard I could use SwiftMail to provide a spool, but I couldn't work out a simple recipe to use it like I do currently with mail().

I want the least amount of code to provide asynchronous mail. I don't care if the email fails to send, but it would be nice to have a log.

Any simple tips or tricks? Short of running a full blown mail server? I was thinking a sendmail wrapper might be the answer but I couldn't work out nohup.

7条回答
对你真心纯属浪费
2楼-- · 2019-02-02 01:58

Your best bet is with a stacking or spooling pattern. It's fairly simple and can be described in 2 steps.

  • Store your emails in a table with a sent flag on your current thread.
  • Use cron or ajax to repeatedly call a mail processing php file that will get the top 10 or 20 unsent emails from your database, flag them as sent and actually send them via your favourite mailing method.
查看更多
萌系小妹纸
3楼-- · 2019-02-02 01:59

You have a lot of ways to do this, but handling thread is not necessarily the right choice.

  • register_shutdown_function: the shutdown function is called after the response is sent. It's not really asynchronous, but at least it won't slow down your request. Regarding the implementation, see the example.
  • Swift pool: using symfony, you can easily use the spool.
  • Queue: register the mails to be sent in a queue system (could be done with RabbitMQ, MySQL, redis or anything), then run a cron that consume the queue. Could be done with something as simple as a MySQL table with fields like from, to, message, sent (boolean set to true when you have sent the email).

Example with register_shutdown_function

<?php
class MailSpool
{
  public static $mails = [];

  public static function addMail($subject, $to, $message)
  {
    self::$mails[] = [ 'subject' => $subject, 'to' => $to, 'message' => $message ];
  }

  public static function send() 
  {
    foreach(self::$mails as $mail) {
      mail($mail['to'], $mail['subject'], $mail['message']);
    }
  }
}

//In your script you can call anywhere
MailSpool::addMail('Hello', 'contact@example.com', 'Hello from the spool');


register_shutdown_function('MailSpool::send');

exit(); // You need to call this to send the response immediately
查看更多
Bombasti
4楼-- · 2019-02-02 02:00

I'm using asynchronous php execution by using beanstalkd.
It is a simple message queue, really lightweight and easy to integrate.

Using the following php wrapper for php https://github.com/pda/pheanstalk you can do something as follows to implement a email worker:

use Beanstalk\Client;
$msg="dest_email##email_subject##from_email##email_body";

$beanstalk = new Client(); 
$beanstalk->connect();
$beanstalk->useTube('flux'); // Begin to use tube `'flux'`.
$beanstalk->put(
    23,  // Give the job a priority of 23.
    0,   // Do not wait to put job into the ready queue.
    60,  // Give the job 1 minute to run.
    $msg // job body
);
$beanstalk->disconnect();

Then the job would be done in a code placed into a separate php file.
Something like:

use Beanstalk\Client;
$do=true;

try {
    $beanstalk = new Client();
    $beanstalk->connect();
    $beanstalk->watch('flux');

} catch (Exception $e ) {
    echo $e->getMessage();
    echo $e->getTraceAsString();
    $do = false;
}

while ($do) {
    $job = $beanstalk->reserve(); // Block until job is available.
    $emailParts = explode("##", $job['body'] );

    // Use your SendMail function here

    if ($i_am_ok) {
        $beanstalk->delete($job['id']);
    } else {
        $beanstalk->bury($job['id'], 20);
    }
}
$beanstalk->disconnect();

You can run separately this php file, as an independent php process. Let's say you save it as sender.php, it would be run in Unix as:

php /path/to/sender/sender.php & && disown

This command would run the file and alsow allow you to close the console or logout current user without stopping the process.
Make sure also that your web server uses the same php.ini file as your php command line interpreter. (Might be solved using a link to you favorite php.ini)

I hope it helps.

查看更多
Summer. ? 凉城
5楼-- · 2019-02-02 02:04

Pthreads is your friend :)
This is a sample of how i made in my production application

class AsynchMail extends Thread{
    private $_mail_from;
    private $_mail_to;
    private $_subject;

    public function __construct($subject, $mail_to, ...) {
        $this->_subject = $subject;
        $this->_mail_to = $mail_to;
        // ... 
    }
    // ...
    // you must redefine run() method, and to execute it we must call start() method
    public function run() {
        // here put your mail() function
        mail($this->_mail_to, ...);
    }
}

TEST SCRIPT EXAMPLE

$mail_to_list = array('Shigeru.Miyamoto@nintendo.com', 'Eikichi.Kawasaki@neogeo.com',...);
foreach($mail_to_list as $mail_to) {
    $asynchMail = new AsynchMail($mail_to);
    $asynchMail->start();
}

Let me know if you need further help for installing and using thread in PHP
For logging system, i strongly advice you to use Log4PHP : powerful and easy to use and to configure
For sending mails, i also strongly advice you to use PHPMailer

查看更多
可以哭但决不认输i
6楼-- · 2019-02-02 02:08

An easy way to do it is to call the code which handles your mails asynchronously.

For example if you have a file called email.php with the following code:

// Example array with e-mailaddresses
$emailaddresses = ['example1@test.com', 'example2@example.com', 'example1@example.com'];

// Call your mail function
mailer::sendMail($emailaddresses);

You can then call this asynchronously in a normal request like

exec('nice -n 20 php email.php > /dev/null & echo $!');

And the request will finish without waiting for email.php to finish sending the e-mails. Logging could be added as well in the file that does the e-mails.

Variables can be passed into the exec between the called filename and > /dev/null like

exec('nice -n 20 php email.php '.$var1.' '.$var2.' > /dev/null & echo $!');

Make sure these variables are safe with escapeshellarg(). In the called file these variables can be used with $argv

查看更多
啃猪蹄的小仙女
7楼-- · 2019-02-02 02:13

php-fpm

You must run php-fpm for fastcgi_finish_request to be available.

echo "I get output instantly";
fastcgi_finish_request(); // Close and flush the connection.
sleep(10); // For illustrative purposes. Delete me.
mail("test@example.org", "lol", "Hi");

It's pretty easy queuing up any arbitrary code to processed after finishing the request to the user:

$post_processing = [];
/* your code */
$email = "test@example.org";
$subject = "lol";
$message = "Hi";

$post_processing[] = function() use ($email, $subject, $message) {
  mail($email, $subject, $message);
};

echo "Stuff is going to happen.";

/* end */

fastcgi_finish_request();

foreach($post_processing as $function) {
  $function();
}

Hipster background worker

Instantly time-out a curl and let the new request deal with it. I was doing this on shared hosts before it was cool. (it's never cool)

if(!empty($_POST)) {
  sleep(10);
  mail($_POST['email'], $_POST['subject'], $_POST['message']);
  exit(); // Stop so we don't self DDOS.
}

$ch = curl_init("http://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);

curl_setopt($ch, CURLOPT_TIMEOUT, 1);
curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
  'email' => 'noreply@example.org',
  'subject' => 'foo',
  'message' => 'bar'
]);

curl_exec($ch);
curl_close($ch);

echo "Expect an email in 10 seconds.";
查看更多
登录 后发表回答