1<?php 2/** 3 * CommonPlugin for phplist. 4 * 5 * This file is a part of CommonPlugin. 6 * 7 * @category phplist 8 * 9 * @author Duncan Cameron 10 * @copyright 2011-2018 Duncan Cameron 11 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License, Version 3 12 */ 13 14namespace phpList\plugin\Common; 15 16use JMathai\PhpMultiCurl\MultiCurl; 17 18/** 19 * This class handles the sending of an email using either curl or multi-curl. 20 */ 21class MailSender 22{ 23 /** @var IMailClient client instance */ 24 private $client; 25 /** @var array the outstanding multi-curl calls */ 26 private $calls = []; 27 /** @var MultiCurl instance */ 28 private $mc = null; 29 /** @var bool whether to use multi-curl */ 30 private $useMulti; 31 /** @var int the maximum number of concurrent curl calls */ 32 private $multiLimit; 33 /** @var bool whether to create a log of multi-curl usage */ 34 private $multiLog; 35 /** @var bool whether to generate verbose curl output */ 36 private $curlVerbose; 37 /** @var bool whether to validate the ssl certificate */ 38 private $verifyCert; 39 /** @var int total of multi-curl calls that were successful */ 40 private $totalSuccess = 0; 41 /** @var int total of multi-curl calls that failed */ 42 private $totalFailure = 0; 43 /** @var phpList\plugin\Common\Logger */ 44 private $logger; 45 46 /** 47 * Constructor. 48 */ 49 public function __construct(IMailClient $client, $useMulti, $multiLimit, $multiLog, $curlVerbose, $verifyCert) 50 { 51 $this->client = $client; 52 $this->useMulti = $useMulti; 53 $this->multiLimit = $multiLimit; 54 $this->multiLog = $multiLog; 55 $this->curlVerbose = $curlVerbose; 56 $this->verifyCert = $verifyCert; 57 $this->logger = Logger::instance(); 58 } 59 60 /** 61 * Complete any outstanding multi-curl calls. 62 * Any emails sent after this point will use single send. 63 */ 64 public function shutdown() 65 { 66 if ($this->mc !== null) { 67 $this->completeCalls(); 68 $this->mc = null; 69 $this->useMulti = false; 70 } 71 } 72 73 /** 74 * This method redirects to send single or multiple emails. 75 * 76 * @see 77 * 78 * @param PHPlistMailer $phplistmailer mailer instance 79 * @param string $messageheader the message http headers 80 * @param string $messagebody the message body 81 * 82 * @return bool success/failure 83 */ 84 public function send(\PHPlistMailer $phplistmailer, $messageheader, $messagebody) 85 { 86 try { 87 return $this->useMulti 88 ? $this->multiSend($phplistmailer, $messageheader, $messagebody) 89 : $this->singleSend($phplistmailer, $messageheader, $messagebody); 90 } catch (Exception $e) { 91 logEvent($e->getMessage()); 92 93 return false; 94 } 95 } 96 97 private function initialiseCurl() 98 { 99 global $tmpdir; 100 101 if (($curl = curl_init()) === false) { 102 throw new Exception('Unable to create curl handle'); 103 } 104 curl_setopt($curl, CURLOPT_URL, $this->client->endpoint()); 105 curl_setopt($curl, CURLOPT_TIMEOUT, 30); 106 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 107 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifyCert); 108 curl_setopt($curl, CURLOPT_HEADER, false); 109 curl_setopt($curl, CURLOPT_DNS_USE_GLOBAL_CACHE, true); 110 curl_setopt($curl, CURLOPT_USERAGENT, NAME . ' (phpList version ' . VERSION . ', http://www.phplist.com/)'); 111 curl_setopt($curl, CURLOPT_POST, true); 112 curl_setopt($curl, CURLINFO_HEADER_OUT, true); 113 114 if ($this->curlVerbose) { 115 curl_setopt($curl, CURLOPT_VERBOSE, true); 116 $log = fopen(sprintf('%s/curl_%s.log', $tmpdir, date('Y-m-d')), 'a+'); 117 curl_setopt($curl, CURLOPT_STDERR, $log); 118 } 119 120 return $curl; 121 } 122 123 /** 124 * Waits for a call to complete. 125 * 126 * @param array $call 127 */ 128 private function waitForCallToComplete(array $call) 129 { 130 $manager = $call['manager']; 131 $httpCode = $manager->code; 132 133 if ($httpCode == 200 && $this->client->verifyResponse($manager->response)) { 134 ++$this->totalSuccess; 135 } else { 136 ++$this->totalFailure; 137 logEvent(sprintf('Multi-curl http code %s result %s email %s', $httpCode, $manager->response, $call['email'])); 138 } 139 } 140 141 /** 142 * Waits for each outstanding call to complete. 143 * Writes the sequence of calls to a log file. 144 * Writes to the event log except when only one email has been sent. 145 */ 146 private function completeCalls() 147 { 148 global $tmpdir; 149 150 while (count($this->calls) > 0) { 151 $this->waitForCallToComplete(array_shift($this->calls)); 152 } 153 154 if ($this->multiLog) { 155 file_put_contents("$tmpdir/multicurl.log", $this->mc->getSequence()->renderAscii()); 156 } 157 158 if (!($this->totalSuccess == 1 && $this->totalFailure == 0)) { 159 logEvent(sprintf('Multi-curl successes: %d, failures: %d', $this->totalSuccess, $this->totalFailure)); 160 } 161 } 162 163 /** 164 * Send an email using curl multi to send multiple emails concurrently. 165 * 166 * @param PHPlistMailer $phplistmailer mailer instance 167 * @param string $messageheader the message http headers 168 * @param string $messagebody the message body 169 * 170 * @return bool success/failure 171 */ 172 private function multiSend($phplistmailer, $messageheader, $messagebody) 173 { 174 if ($this->mc === null) { 175 $this->mc = MultiCurl::getInstance(); 176 register_shutdown_function([$this, 'shutdown']); 177 } 178 179 /* 180 * if the limit has been reached then wait for the oldest call 181 * to complete 182 */ 183 if (count($this->calls) == $this->multiLimit) { 184 $this->waitForCallToComplete(array_shift($this->calls)); 185 } 186 $curl = $this->initialiseCurl(); 187 $body = $this->client->requestBody($phplistmailer, $messageheader, $messagebody); 188 curl_setopt($curl, CURLOPT_POSTFIELDS, $body); 189 curl_setopt($curl, CURLOPT_HTTPHEADER, $this->client->httpHeaders($messageheader, $body)); 190 191 $this->calls[] = [ 192 'manager' => $this->mc->addCurl($curl), 193 'email' => $phplistmailer->destinationemail, 194 ]; 195 196 return true; 197 } 198 199 /** 200 * This method uses curl directly with an optimisation of re-using 201 * the curl handle. 202 * 203 * @param PHPlistMailer $phplistmailer mailer instance 204 * @param string $messageheader the message http headers 205 * @param string $messagebody the message body 206 * 207 * @return bool success/failure 208 */ 209 private function singleSend($phplistmailer, $messageheader, $messagebody) 210 { 211 static $curl = null; 212 213 if ($curl === null) { 214 $curl = $this->initialiseCurl(); 215 } 216 $body = $this->client->requestBody($phplistmailer, $messageheader, $messagebody); 217 curl_setopt($curl, CURLOPT_POSTFIELDS, $body); 218 curl_setopt($curl, CURLOPT_HTTPHEADER, $this->client->httpHeaders($messageheader, $body)); 219 220 $response = curl_exec($curl); 221 $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); 222 $sentHeaders = curl_getinfo($curl, CURLINFO_HEADER_OUT); 223 224 if ($response === false || preg_match('/^2\d\d$/', $httpCode) !== 1 || !$this->client->verifyResponse($response)) { 225 $error = curl_error($curl); 226 logEvent(sprintf('MailSender http code: %s, result: %s, curl error: %s', $httpCode, strip_tags($response), $error)); 227 curl_close($curl); 228 $curl = null; 229 230 return false; 231 } 232 233 return true; 234 } 235} 236