Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Symfony\Component\Mailer\Transport\Smtp;
13
 
14
use Psr\EventDispatcher\EventDispatcherInterface;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\Mailer\Envelope;
17
use Symfony\Component\Mailer\Exception\LogicException;
18
use Symfony\Component\Mailer\Exception\TransportException;
19
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
20
use Symfony\Component\Mailer\SentMessage;
21
use Symfony\Component\Mailer\Transport\AbstractTransport;
22
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
23
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
24
use Symfony\Component\Mime\RawMessage;
25
 
26
/**
27
 * Sends emails over SMTP.
28
 *
29
 * @author Fabien Potencier <fabien@symfony.com>
30
 * @author Chris Corbyn
31
 */
32
class SmtpTransport extends AbstractTransport
33
{
34
    private bool $started = false;
35
    private int $restartThreshold = 100;
36
    private int $restartThresholdSleep = 0;
37
    private int $restartCounter = 0;
38
    private int $pingThreshold = 100;
39
    private float $lastMessageTime = 0;
40
    private AbstractStream $stream;
41
    private string $mtaResult = '';
42
    private string $domain = '[127.0.0.1]';
43
 
44
    public function __construct(AbstractStream $stream = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
45
    {
46
        parent::__construct($dispatcher, $logger);
47
 
48
        $this->stream = $stream ?? new SocketStream();
49
    }
50
 
51
    public function getStream(): AbstractStream
52
    {
53
        return $this->stream;
54
    }
55
 
56
    /**
57
     * Sets the maximum number of messages to send before re-starting the transport.
58
     *
59
     * By default, the threshold is set to 100 (and no sleep at restart).
60
     *
61
     * @param int $threshold The maximum number of messages (0 to disable)
62
     * @param int $sleep     The number of seconds to sleep between stopping and re-starting the transport
63
     *
64
     * @return $this
65
     */
66
    public function setRestartThreshold(int $threshold, int $sleep = 0): static
67
    {
68
        $this->restartThreshold = $threshold;
69
        $this->restartThresholdSleep = $sleep;
70
 
71
        return $this;
72
    }
73
 
74
    /**
75
     * Sets the minimum number of seconds required between two messages, before the server is pinged.
76
     * If the transport wants to send a message and the time since the last message exceeds the specified threshold,
77
     * the transport will ping the server first (NOOP command) to check if the connection is still alive.
78
     * Otherwise the message will be sent without pinging the server first.
79
     *
80
     * Do not set the threshold too low, as the SMTP server may drop the connection if there are too many
81
     * non-mail commands (like pinging the server with NOOP).
82
     *
83
     * By default, the threshold is set to 100 seconds.
84
     *
85
     * @param int $seconds The minimum number of seconds between two messages required to ping the server
86
     *
87
     * @return $this
88
     */
89
    public function setPingThreshold(int $seconds): static
90
    {
91
        $this->pingThreshold = $seconds;
92
 
93
        return $this;
94
    }
95
 
96
    /**
97
     * Sets the name of the local domain that will be used in HELO.
98
     *
99
     * This should be a fully-qualified domain name and should be truly the domain
100
     * you're using.
101
     *
102
     * If your server does not have a domain name, use the IP address. This will
103
     * automatically be wrapped in square brackets as described in RFC 5321,
104
     * section 4.1.3.
105
     *
106
     * @return $this
107
     */
108
    public function setLocalDomain(string $domain): static
109
    {
110
        if ('' !== $domain && '[' !== $domain[0]) {
111
            if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
112
                $domain = '['.$domain.']';
113
            } elseif (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
114
                $domain = '[IPv6:'.$domain.']';
115
            }
116
        }
117
 
118
        $this->domain = $domain;
119
 
120
        return $this;
121
    }
122
 
123
    /**
124
     * Gets the name of the domain that will be used in HELO.
125
     *
126
     * If an IP address was specified, this will be returned wrapped in square
127
     * brackets as described in RFC 5321, section 4.1.3.
128
     */
129
    public function getLocalDomain(): string
130
    {
131
        return $this->domain;
132
    }
133
 
134
    public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage
135
    {
136
        try {
137
            $message = parent::send($message, $envelope);
138
        } catch (TransportExceptionInterface $e) {
139
            if ($this->started) {
140
                try {
141
                    $this->executeCommand("RSET\r\n", [250]);
142
                } catch (TransportExceptionInterface) {
143
                    // ignore this exception as it probably means that the server error was final
144
                }
145
            }
146
 
147
            throw $e;
148
        }
149
 
150
        if ($this->mtaResult && $messageId = $this->parseMessageId($this->mtaResult)) {
151
            $message->setMessageId($messageId);
152
        }
153
 
154
        $this->checkRestartThreshold();
155
 
156
        return $message;
157
    }
158
 
159
    protected function parseMessageId(string $mtaResult): string
160
    {
161
        $regexps = [
162
            '/250 Ok (?P<id>[0-9a-f-]+)\r?$/mis',
163
            '/250 Ok:? queued as (?P<id>[A-Z0-9]+)\r?$/mis',
164
        ];
165
        $matches = [];
166
        foreach ($regexps as $regexp) {
167
            if (preg_match($regexp, $mtaResult, $matches)) {
168
                return $matches['id'];
169
            }
170
        }
171
 
172
        return '';
173
    }
174
 
175
    public function __toString(): string
176
    {
177
        if ($this->stream instanceof SocketStream) {
178
            $name = sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost());
179
            $port = $this->stream->getPort();
180
            if (!(25 === $port || ($tls && 465 === $port))) {
181
                $name .= ':'.$port;
182
            }
183
 
184
            return $name;
185
        }
186
 
187
        return 'smtp://sendmail';
188
    }
189
 
190
    /**
191
     * Runs a command against the stream, expecting the given response codes.
192
     *
193
     * @param int[] $codes
194
     *
195
     * @throws TransportException when an invalid response if received
196
     */
197
    public function executeCommand(string $command, array $codes): string
198
    {
199
        $this->stream->write($command);
200
        $response = $this->getFullResponse();
201
        $this->assertResponseCode($response, $codes);
202
 
203
        return $response;
204
    }
205
 
206
    protected function doSend(SentMessage $message): void
207
    {
208
        if (microtime(true) - $this->lastMessageTime > $this->pingThreshold) {
209
            $this->ping();
210
        }
211
 
212
        if (!$this->started) {
213
            $this->start();
214
        }
215
 
216
        try {
217
            $envelope = $message->getEnvelope();
218
            $this->doMailFromCommand($envelope->getSender()->getEncodedAddress());
219
            foreach ($envelope->getRecipients() as $recipient) {
220
                $this->doRcptToCommand($recipient->getEncodedAddress());
221
            }
222
 
223
            $this->executeCommand("DATA\r\n", [354]);
224
            try {
225
                foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) {
226
                    $this->stream->write($chunk, false);
227
                }
228
                $this->stream->flush();
229
            } catch (TransportExceptionInterface $e) {
230
                throw $e;
231
            } catch (\Exception $e) {
232
                $this->stream->terminate();
233
                $this->started = false;
234
                $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
235
                throw $e;
236
            }
237
            $this->mtaResult = $this->executeCommand("\r\n.\r\n", [250]);
238
            $message->appendDebug($this->stream->getDebug());
239
            $this->lastMessageTime = microtime(true);
240
        } catch (TransportExceptionInterface $e) {
241
            $e->appendDebug($this->stream->getDebug());
242
            $this->lastMessageTime = 0;
243
            throw $e;
244
        }
245
    }
246
 
247
    /**
248
     * @internal since version 6.1, to be made private in 7.0
688 lars 249
     *
148 lars 250
     * @final since version 6.1, to be made private in 7.0
251
     */
252
    protected function doHeloCommand(): void
253
    {
254
        $this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]);
255
    }
256
 
257
    private function doMailFromCommand(string $address): void
258
    {
259
        $this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]);
260
    }
261
 
262
    private function doRcptToCommand(string $address): void
263
    {
264
        $this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]);
265
    }
266
 
267
    public function start(): void
268
    {
269
        if ($this->started) {
270
            return;
271
        }
272
 
273
        $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
274
 
275
        $this->stream->initialize();
276
        $this->assertResponseCode($this->getFullResponse(), [220]);
277
        $this->doHeloCommand();
278
        $this->started = true;
279
        $this->lastMessageTime = 0;
280
 
281
        $this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__));
282
    }
283
 
284
    /**
285
     * Manually disconnect from the SMTP server.
286
     *
287
     * In most cases this is not necessary since the disconnect happens automatically on termination.
288
     * In cases of long-running scripts, this might however make sense to avoid keeping an open
289
     * connection to the SMTP server in between sending emails.
290
     */
291
    public function stop(): void
292
    {
293
        if (!$this->started) {
294
            return;
295
        }
296
 
297
        $this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__));
298
 
299
        try {
300
            $this->executeCommand("QUIT\r\n", [221]);
301
        } catch (TransportExceptionInterface) {
302
        } finally {
303
            $this->stream->terminate();
304
            $this->started = false;
305
            $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
306
        }
307
    }
308
 
309
    private function ping(): void
310
    {
311
        if (!$this->started) {
312
            return;
313
        }
314
 
315
        try {
316
            $this->executeCommand("NOOP\r\n", [250]);
317
        } catch (TransportExceptionInterface) {
318
            $this->stop();
319
        }
320
    }
321
 
322
    /**
323
     * @throws TransportException if a response code is incorrect
324
     */
325
    private function assertResponseCode(string $response, array $codes): void
326
    {
327
        if (!$codes) {
328
            throw new LogicException('You must set the expected response code.');
329
        }
330
 
331
        [$code] = sscanf($response, '%3d');
332
        $valid = \in_array($code, $codes);
333
 
334
        if (!$valid || !$response) {
335
            $codeStr = $code ? sprintf('code "%s"', $code) : 'empty code';
336
            $responseStr = $response ? sprintf(', with message "%s"', trim($response)) : '';
337
 
338
            throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0);
339
        }
340
    }
341
 
342
    private function getFullResponse(): string
343
    {
344
        $response = '';
345
        do {
346
            $line = $this->stream->readLine();
347
            $response .= $line;
348
        } while ($line && isset($line[3]) && ' ' !== $line[3]);
349
 
350
        return $response;
351
    }
352
 
353
    private function checkRestartThreshold(): void
354
    {
355
        // when using sendmail via non-interactive mode, the transport is never "started"
356
        if (!$this->started) {
357
            return;
358
        }
359
 
360
        ++$this->restartCounter;
361
        if ($this->restartCounter < $this->restartThreshold) {
362
            return;
363
        }
364
 
365
        $this->stop();
366
        if (0 < $sleep = $this->restartThresholdSleep) {
367
            $this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep));
368
 
369
            sleep($sleep);
370
        }
371
        $this->start();
372
        $this->restartCounter = 0;
373
    }
374
 
375
    public function __sleep(): array
376
    {
377
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
378
    }
379
 
380
    public function __wakeup()
381
    {
382
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
383
    }
384
 
385
    public function __destruct()
386
    {
387
        $this->stop();
388
    }
389
}