Merge branch '2.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / mail / SMTPMailSender.class.php
1 <?php
2 namespace wcf\system\mail;
3 use wcf\system\exception\SystemException;
4 use wcf\system\io\RemoteFile;
5 use wcf\util\StringUtil;
6
7 /**
8 * Sends a Mail with a connection to a smtp server.
9 *
10 * @author Tim Duesterhus, Alexander Ebert
11 * @copyright 2001-2014 WoltLab GmbH
12 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
13 * @package com.woltlab.wcf
14 * @subpackage data.mail
15 * @category Community Framework
16 */
17 class SMTPMailSender extends MailSender {
18 /**
19 * smtp connection
20 * @var \wcf\system\io\RemoteFile
21 */
22 protected $connection = null;
23
24 /**
25 * last received status code
26 * @var string
27 */
28 protected $statusCode = '';
29
30 /**
31 * last received status message
32 * @var string
33 */
34 protected $statusMsg = '';
35
36 /**
37 * mail recipients
38 * @var array
39 */
40 protected $recipients = array();
41
42 /**
43 * Creates a new SMTPMailSender object.
44 */
45 public function __construct() {
46 Mail::$lineEnding = "\r\n";
47 }
48
49 /**
50 * Destroys the SMTPMailSender object.
51 */
52 public function __destruct() {
53 $this->disconnect();
54 }
55
56 /**
57 * Connects to the smtp-server
58 */
59 protected function connect() {
60 // connect
61 $this->connection = new RemoteFile(MAIL_SMTP_HOST, MAIL_SMTP_PORT);
62 $this->getSMTPStatus();
63 if ($this->statusCode != 220) {
64 throw new SystemException($this->formatError("can not connect to '".MAIL_SMTP_HOST.":".MAIL_SMTP_PORT."'"));
65 }
66
67 $host = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
68 if (empty($host)) {
69 $host = gethostname();
70 if ($host === false) {
71 $host = 'localhost';
72 }
73 }
74
75 // send ehlo
76 $this->write('EHLO '.$host);
77 $extensions = explode(Mail::$lineEnding, $this->read());
78 $this->getSMTPStatus(array_shift($extensions));
79 if ($this->statusCode == 250) {
80 $extensions = array_map(function($element) {
81 return strtolower(substr($element, 4));
82 }, $extensions);
83
84 if ($this->connection->hasTLSSupport() && in_array('starttls', $extensions)) {
85 $this->write('STARTTLS');
86 $this->getSMTPStatus();
87
88 if ($this->statusCode != 220) {
89 throw new SystemException($this->formatError("cannot enable STARTTLS, though '".MAIL_SMTP_HOST.":".MAIL_SMTP_PORT."' advertised it"));
90 }
91
92 if (!$this->connection->setTLS(true)) {
93 throw new SystemException('enabling TLS failed');
94 }
95
96 // repeat EHLO
97 $this->write('EHLO '.$host);
98 $extensions = explode(Mail::$lineEnding, $this->read());
99 $this->getSMTPStatus(array_shift($extensions));
100
101 if ($this->statusCode != 250) {
102 throw new SystemException($this->formatError("could not EHLO after enabling STARTTLS at '".MAIL_SMTP_HOST.":".MAIL_SMTP_PORT."'"));
103 }
104 }
105
106 // do authentication
107 if (MAIL_SMTP_USER != '' || MAIL_SMTP_PASSWORD != '') {
108 $this->auth();
109 }
110 }
111 else {
112 // send helo
113 $this->write('HELO '.$host);
114 $this->getSMTPStatus();
115 if ($this->statusCode != 250) {
116 throw new SystemException($this->formatError("can not connect to '".MAIL_SMTP_HOST.":".MAIL_SMTP_PORT."'"));
117 }
118 }
119 }
120
121 /**
122 * Formats a smtp error message.
123 *
124 * @param string $message
125 * @return string
126 */
127 protected function formatError($message) {
128 return $message.': '.$this->statusMsg.' ('.$this->statusCode.')';
129 }
130
131 /**
132 * Does the authentification of the client on the server
133 */
134 protected function auth() {
135 // init authentication
136 $this->write('AUTH LOGIN');
137 $this->getSMTPStatus();
138
139 // checks if auth is supported
140 if ($this->statusCode != 334) {
141 throw new SystemException($this->formatError("smtp mail server '".MAIL_SMTP_HOST.":".MAIL_SMTP_PORT."' does not support user authentication"));
142 }
143
144 // sending user information to smtp-server
145 $this->write(base64_encode(MAIL_SMTP_USER));
146 $this->getSMTPStatus();
147 if ($this->statusCode != 334) {
148 throw new SystemException($this->formatError("unknown smtp user '".MAIL_SMTP_USER."'"));
149 }
150
151 $this->write(base64_encode(MAIL_SMTP_PASSWORD));
152 $this->getSMTPStatus();
153 if ($this->statusCode != 235) {
154 throw new SystemException($this->formatError("invalid password for smtp user '".MAIL_SMTP_USER."'"));
155 }
156 }
157
158 /**
159 * @see \wcf\system\mail\MailSender::sendMail()
160 */
161 public function sendMail(Mail $mail) {
162 $this->recipients = array();
163 if (count($mail->getTo()) > 0) $this->recipients = $mail->getTo();
164 if (count($mail->getCC()) > 0) $this->recipients = array_merge($this->recipients, $mail->getCC());
165 if (count($mail->getBCC())> 0) $this->recipients = array_merge($this->recipients, $mail->getBCC());
166
167 // apply connection
168 if ($this->connection === null) {
169 $this->connect();
170 }
171
172 // send mail
173 $this->write('MAIL FROM:<'.$mail->getFrom().'>');
174 $this->getSMTPStatus();
175 if ($this->statusCode != 250) {
176 $this->abort();
177 throw new SystemException($this->formatError("wrong from format '".$mail->getFrom()."'"));
178 }
179
180 // recipients
181 $recipientCounter = 0;
182 foreach ($this->recipients as $recipient) {
183 $this->write('RCPT TO:<'.$recipient.'>');
184 $this->getSMTPStatus();
185 if ($this->statusCode != 250 && $this->statusCode != 251) {
186 if ($this->statusCode < 550) {
187 $this->abort();
188 throw new SystemException($this->formatError("wrong recipient format '".$recipient."'"));
189 }
190 continue;
191 }
192 $recipientCounter++;
193 }
194 if (!$recipientCounter) {
195 $this->abort();
196 return;
197 }
198
199 // data
200 $this->write("DATA");
201 $this->getSMTPStatus();
202 if ($this->statusCode != 354 && $this->statusCode != 250) {
203 $this->abort();
204 throw new SystemException($this->formatError("smtp error"));
205 }
206
207 $serverName = (isset($_SERVER['SERVER_NAME'])) ? $_SERVER['SERVER_NAME'] : '';
208 if (empty($serverName)) {
209 $serverName = gethostname();
210 if ($serverName === false) {
211 $serverName = 'localhost';
212 }
213 }
214
215 $header =
216 "Date: ".gmdate('r').Mail::$lineEnding
217 ."To: ".$mail->getToString().Mail::$lineEnding
218 ."Message-ID: <".md5(uniqid())."@".$serverName.">".Mail::$lineEnding
219 ."Subject: ".Mail::encodeMIMEHeader($mail->getSubject()).Mail::$lineEnding
220 .$mail->getHeader();
221
222 $this->write($header);
223 $this->write("");
224 $lines = explode(Mail::$lineEnding, $mail->getBody());
225 foreach ($lines as $line) {
226 // 4.5.2 Transparency
227 // o Before sending a line of mail text, the SMTP client checks the
228 // first character of the line. If it is a period, one additional
229 // period is inserted at the beginning of the line.
230 if (StringUtil::startsWith($line, '.')) $line = '.'.$line;
231 $this->write($line);
232 }
233 $this->write(".");
234
235 $this->getSMTPStatus();
236 if ($this->statusCode != 250) {
237 $this->abort();
238 throw new SystemException($this->formatError("message sending failed"));
239 }
240 }
241
242 /**
243 * Disconnects the Client-Server connection
244 */
245 public function disconnect() {
246 if ($this->connection === null) {
247 return;
248 }
249
250 $this->write("QUIT");
251 $this->read();
252 $this->connection->close();
253 $this->connection = null;
254 }
255
256 /**
257 * Reads the Information wich the Server sends back.
258 *
259 * @return string
260 */
261 protected function read() {
262 $result = '';
263 while ($read = $this->connection->gets()) {
264 $result .= $read;
265 if (substr($read, 3, 1) == " ") break;
266 }
267
268 return $result;
269 }
270
271 /**
272 * Aborts the current process. This is needed in case a new mail should be
273 * sent after a exception has occured
274 */
275 protected function abort() {
276 $this->write("RSET");
277 $this->read(); // read response, but do not care about status here
278 }
279
280 /**
281 * Gets error code and message from a server message.
282 *
283 * @param string $data
284 */
285 protected function getSMTPStatus($data = null) {
286 if ($data === null) $data = $this->read();
287 $this->statusCode = intval(substr($data, 0, 3));
288 $this->statusMsg = substr($data, 4);
289 }
290
291 /**
292 * Sends Information to the smtp-Server
293 *
294 * @param string $data
295 */
296 protected function write($data) {
297 $this->connection->puts($data.Mail::$lineEnding);
298 }
299 }