1 | <?php |
||||
2 | /** |
||||
3 | * @package toolkit |
||||
4 | */ |
||||
5 | |||||
6 | /** |
||||
7 | * Exceptions to be thrown by the SMTP Client class |
||||
8 | */ |
||||
9 | class SMTPException extends Exception |
||||
10 | { |
||||
11 | } |
||||
12 | |||||
13 | /** |
||||
14 | * A SMTP client class, for sending text/plain emails. |
||||
15 | * This class only supports the very basic SMTP functions. |
||||
16 | * Inspired by the SMTP class in the Zend library |
||||
17 | * |
||||
18 | * @author Huib Keemink <[email protected]> |
||||
19 | * @version 0.1 - 20 okt 2010 |
||||
20 | */ |
||||
21 | class SMTP |
||||
22 | { |
||||
23 | const TIMEOUT = 30; |
||||
24 | |||||
25 | protected $_host; |
||||
26 | protected $_port; |
||||
27 | protected $_user = null; |
||||
28 | protected $_pass = null; |
||||
29 | protected $_transport = 'tcp'; |
||||
30 | protected $_secure = false; |
||||
31 | |||||
32 | protected $_header_fields = array(); |
||||
33 | |||||
34 | protected $_from = null; |
||||
35 | |||||
36 | protected $_helo_host = null; |
||||
37 | protected $_connection = false; |
||||
38 | |||||
39 | protected $_helo = false; |
||||
40 | protected $_mail = false; |
||||
41 | protected $_data = false; |
||||
42 | protected $_rcpt = false; |
||||
43 | protected $_auth = false; |
||||
44 | |||||
45 | /** |
||||
46 | * Constructor. |
||||
47 | * |
||||
48 | * @param string $host |
||||
49 | * Host to connect to. Defaults to localhost (127.0.0.1) |
||||
50 | * @param integer $port |
||||
51 | * When ssl is used, defaults to 465 |
||||
52 | * When no ssl is used, and ini_get returns no value, defaults to 25. |
||||
53 | * @param array $options |
||||
54 | * Currently supports 3 values: |
||||
55 | * $options['secure'] can be ssl, tls or null. |
||||
56 | * $options['username'] the username used to login to the server. Leave empty for no authentication. |
||||
57 | * $options['password'] the password used to login to the server. Leave empty for no authentication. |
||||
58 | * $options['helo_hostname'] the hostname address used in the EHLO/HELO commands. Ideally an FQDN. |
||||
59 | * $options['local_ip'] the ip address used in the EHLO/HELO commands if no helo_hostname is given. |
||||
60 | * @throws SMTPException |
||||
61 | */ |
||||
62 | public function __construct($host = '127.0.0.1', $port = null, $options = array()) |
||||
0 ignored issues
–
show
Coding Style
introduced
by
![]() |
|||||
63 | { |
||||
64 | if ($options['secure'] !== null) { |
||||
65 | switch (strtolower($options['secure'])) { |
||||
66 | case 'tls': |
||||
67 | $this->_secure = 'tls'; |
||||
68 | break; |
||||
69 | case 'ssl': |
||||
70 | $this->_transport = 'ssl'; |
||||
71 | $this->_secure = 'ssl'; |
||||
72 | if ($port === null) { |
||||
73 | $port = 465; |
||||
74 | } |
||||
75 | break; |
||||
76 | case 'no': |
||||
77 | break; |
||||
78 | default: |
||||
79 | throw new SMTPException(__('Unsupported SSL type')); |
||||
80 | } |
||||
81 | } |
||||
82 | |||||
83 | if (!empty($options['helo_hostname'])) { |
||||
84 | $this->_helo_host = $options['helo_hostname']; |
||||
85 | } elseif (!empty($options['local_ip'])) { |
||||
86 | $this->_helo_host = '[' . $options['local_ip'] . ']'; |
||||
87 | } else { |
||||
88 | $this->_helo_host = '[' . gethostbyname(php_uname('n')) . ']'; |
||||
89 | } |
||||
90 | |||||
91 | if ($port === null) { |
||||
92 | $port = 25; |
||||
93 | } |
||||
94 | |||||
95 | if (($options['username'] !== null) && ($options['password'] !== null)) { |
||||
96 | $this->_user = $options['username']; |
||||
97 | $this->_pass = $options['password']; |
||||
98 | } |
||||
99 | |||||
100 | $this->_host = $host; |
||||
101 | $this->_port = $port; |
||||
102 | } |
||||
103 | |||||
104 | /** |
||||
105 | * Checks to see if `$this->_connection` is a valid resource. Throws an |
||||
106 | * exception if there is no connection, otherwise returns true. |
||||
107 | * |
||||
108 | * @throws SMTPException |
||||
109 | * @return boolean |
||||
110 | */ |
||||
111 | public function checkConnection() |
||||
112 | { |
||||
113 | if (!is_resource($this->_connection)) { |
||||
0 ignored issues
–
show
|
|||||
114 | throw new SMTPException(__('No connection has been established to %s', array($this->_host))); |
||||
115 | } |
||||
116 | |||||
117 | return true; |
||||
118 | } |
||||
119 | |||||
120 | /** |
||||
121 | * The actual email sending. |
||||
122 | * The connection to the server (connecting, EHLO, AUTH, etc) is done here, |
||||
123 | * right before the actual email is sent. This is to make sure the connection does not time out. |
||||
124 | * |
||||
125 | * @param string $from |
||||
126 | * The from string. Should have the following format: [email protected] |
||||
127 | * @param string $to |
||||
128 | * The email address to send the email to. |
||||
129 | * @param string $subject |
||||
130 | * The subject to send the email to. |
||||
131 | * @param string $message |
||||
132 | * @throws SMTPException |
||||
133 | * @throws Exception |
||||
134 | * @return boolean |
||||
135 | */ |
||||
136 | public function sendMail($from, $to, $message) |
||||
137 | { |
||||
138 | $this->_connect($this->_host, $this->_port); |
||||
139 | $this->mail($from); |
||||
140 | |||||
141 | if (!is_array($to)) { |
||||
0 ignored issues
–
show
|
|||||
142 | $to = array($to); |
||||
143 | } |
||||
144 | |||||
145 | foreach ($to as $recipient) { |
||||
146 | $this->rcpt($recipient); |
||||
147 | } |
||||
0 ignored issues
–
show
|
|||||
148 | $this->data($message); |
||||
149 | $this->rset(); |
||||
150 | } |
||||
151 | |||||
152 | /** |
||||
153 | * Sets a header to be sent in the email. |
||||
154 | * |
||||
155 | * @throws SMTPException |
||||
156 | * @param string $header |
||||
157 | * @param string $value |
||||
158 | * @return void |
||||
159 | */ |
||||
160 | public function setHeader($header, $value) |
||||
161 | { |
||||
162 | if (is_array($value)) { |
||||
0 ignored issues
–
show
|
|||||
163 | throw new SMTPException(__('Header fields can only contain strings')); |
||||
164 | } |
||||
165 | |||||
166 | $this->_header_fields[$header] = $value; |
||||
167 | } |
||||
168 | |||||
169 | |||||
170 | /** |
||||
171 | * Initiates the ehlo/helo requests. |
||||
172 | * |
||||
173 | * @throws SMTPException |
||||
174 | * @throws Exception |
||||
175 | * @return void |
||||
176 | */ |
||||
177 | public function helo() |
||||
178 | { |
||||
179 | if ($this->_mail !== false) { |
||||
180 | throw new SMTPException(__('Can not call HELO on existing session')); |
||||
181 | } |
||||
182 | |||||
183 | //wait for the server to be ready |
||||
184 | $this->_expect(220, 300); |
||||
185 | |||||
186 | //send ehlo or ehlo request. |
||||
187 | try { |
||||
188 | $this->_ehlo(); |
||||
189 | } catch (SMTPException $e) { |
||||
190 | $this->_helo(); |
||||
191 | } catch (Exception $e) { |
||||
192 | throw $e; |
||||
193 | } |
||||
194 | |||||
195 | $this->_helo = true; |
||||
196 | } |
||||
197 | |||||
198 | /** |
||||
199 | * Calls the MAIL command on the server. |
||||
200 | * |
||||
201 | * @throws SMTPException |
||||
202 | * @param string $from |
||||
203 | * The email address to send the email from. |
||||
204 | * @return void |
||||
205 | */ |
||||
206 | public function mail($from) |
||||
207 | { |
||||
208 | if ($this->_helo == false) { |
||||
0 ignored issues
–
show
|
|||||
209 | throw new SMTPException(__('Must call EHLO (or HELO) before calling MAIL')); |
||||
210 | } elseif ($this->_mail !== false) { |
||||
211 | throw new SMTPException(__('Only one call to MAIL may be made at a time.')); |
||||
212 | } |
||||
213 | |||||
214 | $this->_send('MAIL FROM:<' . $from . '>'); |
||||
215 | $this->_expect(250, 300); |
||||
216 | |||||
217 | $this->_from = $from; |
||||
218 | $this->_mail = true; |
||||
219 | $this->_rcpt = false; |
||||
220 | $this->_data = false; |
||||
221 | } |
||||
222 | |||||
223 | /** |
||||
224 | * Calls the RCPT command on the server. May be called multiple times for more than one recipient. |
||||
225 | * |
||||
226 | * @throws SMTPException |
||||
227 | * @param string $to |
||||
228 | * The address to send the email to. |
||||
229 | * @return void |
||||
230 | */ |
||||
231 | public function rcpt($to) |
||||
232 | { |
||||
233 | if ($this->_mail == false) { |
||||
0 ignored issues
–
show
|
|||||
234 | throw new SMTPException(__('Must call MAIL before calling RCPT')); |
||||
235 | } |
||||
236 | |||||
237 | $this->_send('RCPT TO:<' . $to . '>'); |
||||
238 | $this->_expect(array(250, 251), 300); |
||||
239 | |||||
240 | $this->_rcpt = true; |
||||
241 | } |
||||
242 | |||||
243 | /** |
||||
244 | * Calls the data command on the server. |
||||
245 | * Also includes header fields in the command. |
||||
246 | * |
||||
247 | * @throws SMTPException |
||||
248 | * @param string $data |
||||
249 | * @return void |
||||
250 | */ |
||||
251 | public function data($data) |
||||
252 | { |
||||
253 | if ($this->_rcpt == false) { |
||||
0 ignored issues
–
show
|
|||||
254 | throw new SMTPException(__('Must call RCPT before calling DATA')); |
||||
255 | } |
||||
256 | |||||
257 | $this->_send('DATA'); |
||||
258 | $this->_expect(354, 120); |
||||
259 | |||||
260 | foreach ($this->_header_fields as $name => $body) { |
||||
261 | // Every header can contain an array. Will insert multiple header fields of that type with the contents of array. |
||||
262 | // Useful for multiple recipients, for instance. |
||||
263 | if (!is_array($body)) { |
||||
264 | $body = array($body); |
||||
265 | } |
||||
266 | |||||
267 | foreach ($body as $val) { |
||||
268 | $this->_send($name . ': ' . $val); |
||||
269 | } |
||||
270 | } |
||||
0 ignored issues
–
show
|
|||||
271 | // Send an empty newline. Solves bugs with Apple Mail |
||||
272 | $this->_send(''); |
||||
273 | |||||
274 | // Because the message can contain \n as a newline, replace all \r\n with \n and explode on \n. |
||||
275 | // The send() function will use the proper line ending (\r\n). |
||||
276 | $data = str_replace("\r\n", "\n", $data); |
||||
277 | $data_arr = explode("\n", $data); |
||||
278 | |||||
279 | foreach ($data_arr as $line) { |
||||
280 | // Escape line if first character is a period (dot). http://tools.ietf.org/html/rfc2821#section-4.5.2 |
||||
281 | if (strpos($line, '.') === 0) { |
||||
282 | $line = '.' . $line; |
||||
283 | } |
||||
0 ignored issues
–
show
|
|||||
284 | $this->_send($line); |
||||
285 | } |
||||
286 | |||||
287 | $this->_send('.'); |
||||
288 | $this->_expect(250, 600); |
||||
289 | $this->_data = true; |
||||
290 | } |
||||
291 | |||||
292 | /** |
||||
293 | * Resets the current session. This 'undoes' all rcpt, mail, etc calls. |
||||
294 | * |
||||
295 | * @throws SMTPException |
||||
296 | * @return void |
||||
297 | */ |
||||
298 | public function rset() |
||||
299 | { |
||||
300 | $this->_send('RSET'); |
||||
301 | // MS ESMTP doesn't follow RFC, see [ZF-1377] |
||||
302 | $this->_expect(array(250, 220)); |
||||
303 | |||||
304 | $this->_mail = false; |
||||
305 | $this->_rcpt = false; |
||||
306 | $this->_data = false; |
||||
307 | } |
||||
308 | |||||
309 | /** |
||||
310 | * Disconnects to the server. |
||||
311 | * |
||||
312 | * @throws SMTPException |
||||
313 | * @return void |
||||
314 | */ |
||||
315 | public function quit() |
||||
316 | { |
||||
317 | $this->_send('QUIT'); |
||||
318 | $this->_expect(221, 300); |
||||
319 | $this->_connection = null; |
||||
320 | } |
||||
321 | |||||
322 | /** |
||||
323 | * Authenticates to the server. |
||||
324 | * Currently supports the AUTH LOGIN command. |
||||
325 | * May be extended if more methods are needed. |
||||
326 | * |
||||
327 | * @throws SMTPException |
||||
328 | * @return void |
||||
329 | */ |
||||
330 | protected function _auth() |
||||
331 | { |
||||
332 | if ($this->_helo == false) { |
||||
0 ignored issues
–
show
|
|||||
333 | throw new SMTPException(__('Must call EHLO (or HELO) before calling AUTH')); |
||||
334 | } elseif ($this->_auth !== false) { |
||||
335 | throw new SMTPException(__('Can not call AUTH again.')); |
||||
336 | } |
||||
337 | |||||
338 | $this->_send('AUTH LOGIN'); |
||||
339 | $this->_expect(334); |
||||
340 | $this->_send(base64_encode($this->_user)); |
||||
341 | $this->_expect(334); |
||||
342 | $this->_send(base64_encode($this->_pass)); |
||||
343 | $this->_expect(235); |
||||
344 | $this->_auth = true; |
||||
345 | } |
||||
346 | |||||
347 | /** |
||||
348 | * Calls the EHLO function. |
||||
349 | * This is the HELO function for more modern servers. |
||||
350 | * |
||||
351 | * @throws SMTPException |
||||
352 | * @return void |
||||
353 | */ |
||||
354 | protected function _ehlo() |
||||
355 | { |
||||
356 | $this->_send('EHLO ' . $this->_helo_host); |
||||
357 | $this->_expect(array(250, 220), 300); |
||||
358 | } |
||||
359 | |||||
360 | /** |
||||
361 | * Initiates the connection by calling the HELO function. |
||||
362 | * This function should only be used if the server does not support the HELO function. |
||||
363 | * |
||||
364 | * @throws SMTPException |
||||
365 | * @return void |
||||
366 | */ |
||||
367 | protected function _helo() |
||||
368 | { |
||||
369 | $this->_send('HELO ' . $this->_helo_host); |
||||
370 | $this->_expect(array(250, 220), 300); |
||||
371 | } |
||||
372 | |||||
373 | /** |
||||
374 | * Encrypts the current session with TLS. |
||||
375 | * |
||||
376 | * @throws SMTPException |
||||
377 | * @return void |
||||
378 | */ |
||||
379 | protected function _tls() |
||||
380 | { |
||||
381 | if ($this->_secure == 'tls') { |
||||
382 | $this->_send('STARTTLS'); |
||||
383 | $this->_expect(220, 180); |
||||
384 | if (!stream_socket_enable_crypto($this->_connection, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { |
||||
0 ignored issues
–
show
$this->_connection of type boolean is incompatible with the type resource expected by parameter $stream of stream_socket_enable_crypto() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
385 | throw new SMTPException(__('Unable to connect via TLS')); |
||||
386 | } |
||||
0 ignored issues
–
show
|
|||||
387 | $this->_ehlo(); |
||||
388 | } |
||||
389 | } |
||||
390 | |||||
391 | /** |
||||
392 | * Send a request to the host, appends the request with a line break. |
||||
393 | * |
||||
394 | * @param string $request |
||||
395 | * @throws SMTPException |
||||
396 | * @return boolean|integer number of characters written. |
||||
397 | */ |
||||
398 | protected function _send($request) |
||||
399 | { |
||||
400 | $this->checkConnection(); |
||||
401 | |||||
402 | $result = fwrite($this->_connection, $request . "\r\n"); |
||||
0 ignored issues
–
show
$this->_connection of type boolean is incompatible with the type resource expected by parameter $handle of fwrite() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
403 | |||||
404 | if ($result === false) { |
||||
405 | throw new SMTPException(__('Could not send request: %s', array($request))); |
||||
406 | } |
||||
0 ignored issues
–
show
|
|||||
407 | return $result; |
||||
408 | } |
||||
409 | |||||
410 | /** |
||||
411 | * Get a line from the stream. |
||||
412 | * |
||||
413 | * @param integer $timeout |
||||
414 | * Per-request timeout value if applicable. Defaults to null which |
||||
415 | * will not set a timeout. |
||||
416 | * @throws SMTPException |
||||
417 | * @return string |
||||
418 | */ |
||||
419 | protected function _receive($timeout = null) |
||||
0 ignored issues
–
show
|
|||||
420 | { |
||||
421 | $this->checkConnection(); |
||||
422 | |||||
423 | if ($timeout !== null) { |
||||
424 | stream_set_timeout($this->_connection, $timeout); |
||||
0 ignored issues
–
show
$this->_connection of type boolean is incompatible with the type resource expected by parameter $stream of stream_set_timeout() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
425 | } |
||||
426 | |||||
427 | $response = fgets($this->_connection, 1024); |
||||
0 ignored issues
–
show
$this->_connection of type boolean is incompatible with the type resource expected by parameter $handle of fgets() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
428 | $info = stream_get_meta_data($this->_connection); |
||||
0 ignored issues
–
show
$this->_connection of type boolean is incompatible with the type resource expected by parameter $stream of stream_get_meta_data() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
429 | |||||
430 | if (!empty($info['timed_out'])) { |
||||
431 | throw new SMTPException(__('%s has timed out', array($this->_host))); |
||||
432 | } elseif ($response === false) { |
||||
433 | throw new SMTPException(__('Could not read from %s', array($this->_host))); |
||||
434 | } |
||||
435 | |||||
436 | return $response; |
||||
437 | } |
||||
438 | |||||
439 | /** |
||||
440 | * Parse server response for successful codes |
||||
441 | * |
||||
442 | * Read the response from the stream and check for expected return code. |
||||
443 | * |
||||
444 | * @throws SMTPException |
||||
445 | * @param string|array $code |
||||
446 | * One or more codes that indicate a successful response |
||||
447 | * @param integer $timeout |
||||
448 | * Per-request timeout value if applicable. Defaults to null which |
||||
449 | * will not set a timeout. |
||||
450 | * @return string |
||||
451 | * Last line of response string |
||||
452 | */ |
||||
453 | protected function _expect($code, $timeout = null) |
||||
0 ignored issues
–
show
|
|||||
454 | { |
||||
455 | $this->_response = array(); |
||||
0 ignored issues
–
show
|
|||||
456 | $cmd = ''; |
||||
0 ignored issues
–
show
|
|||||
457 | $more = ''; |
||||
0 ignored issues
–
show
|
|||||
458 | $msg = ''; |
||||
0 ignored issues
–
show
|
|||||
459 | $errMsg = ''; |
||||
460 | |||||
461 | if (!is_array($code)) { |
||||
462 | $code = array($code); |
||||
463 | } |
||||
464 | |||||
465 | // Borrowed from the Zend Email Library |
||||
466 | do { |
||||
467 | $result = $this->_receive($timeout); |
||||
468 | list($cmd, $more, $msg) = preg_split('/([\s-]+)/', $result, 2, PREG_SPLIT_DELIM_CAPTURE); |
||||
469 | |||||
470 | if ($errMsg !== '') { |
||||
471 | $errMsg .= ' ' . $msg; |
||||
472 | } elseif ($cmd === null || !in_array($cmd, $code)) { |
||||
473 | $errMsg = $msg; |
||||
474 | } |
||||
475 | } while (strpos($more, '-') === 0); // The '-' message prefix indicates an information string instead of a response string. |
||||
476 | |||||
477 | if ($errMsg !== '') { |
||||
478 | $this->rset(); |
||||
479 | throw new SMTPException($errMsg); |
||||
480 | } |
||||
481 | |||||
482 | return $msg; |
||||
483 | } |
||||
484 | |||||
485 | /** |
||||
486 | * Connect to the host, and perform basic functions like helo and auth. |
||||
487 | * |
||||
488 | * |
||||
489 | * @param string $host |
||||
490 | * @param integer $port |
||||
491 | * @throws SMTPException |
||||
492 | * @throws Exception |
||||
493 | * @return void |
||||
494 | */ |
||||
495 | protected function _connect($host, $port) |
||||
496 | { |
||||
497 | $errorNum = 0; |
||||
498 | $errorStr = ''; |
||||
499 | |||||
500 | $remoteAddr = $this->_transport . '://' . $host . ':' . $port; |
||||
501 | |||||
502 | if (!is_resource($this->_connection)) { |
||||
0 ignored issues
–
show
|
|||||
503 | $this->_connection = @stream_socket_client($remoteAddr, $errorNum, $errorStr, self::TIMEOUT); |
||||
504 | |||||
505 | if ($this->_connection === false) { |
||||
506 | if ($errorNum == 0) { |
||||
507 | throw new SMTPException(__('Unable to open socket. Unknown error')); |
||||
508 | } else { |
||||
509 | throw new SMTPException(__('Unable to open socket. %s', array($errorStr))); |
||||
510 | } |
||||
511 | } |
||||
512 | |||||
513 | if (@stream_set_timeout($this->_connection, self::TIMEOUT) === false) { |
||||
514 | throw new SMTPException(__('Unable to set timeout.')); |
||||
515 | } |
||||
516 | |||||
517 | $this->helo(); |
||||
518 | |||||
519 | if ($this->_secure == 'tls') { |
||||
520 | $this->_tls(); |
||||
521 | } |
||||
522 | |||||
523 | if (($this->_user !== null) && ($this->_pass !== null)) { |
||||
524 | $this->_auth(); |
||||
525 | } |
||||
526 | } |
||||
527 | } |
||||
528 | } |
||||
529 |