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