Commit | Line | Data |
---|---|---|
158bd3ca TD |
1 | <?php |
2 | namespace wcf\system\mail; | |
3 | use wcf\system\exception\SystemException; | |
4 | use wcf\system\io\RemoteFile; | |
dd1ff120 | 5 | use wcf\util\StringUtil; |
158bd3ca TD |
6 | |
7 | /** | |
8 | * Sends a Mail with a connection to a smtp server. | |
9 | * | |
4a92e525 | 10 | * @author Tim Duesterhus, Alexander Ebert |
ca4ba303 | 11 | * @copyright 2001-2014 WoltLab GmbH |
158bd3ca TD |
12 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
13 | * @package com.woltlab.wcf | |
14 | * @subpackage data.mail | |
9f959ced | 15 | * @category Community Framework |
158bd3ca TD |
16 | */ |
17 | class SMTPMailSender extends MailSender { | |
3a674fcc MW |
18 | /** |
19 | * smtp connection | |
0ad90fc3 | 20 | * @var \wcf\system\io\RemoteFile |
3a674fcc | 21 | */ |
158bd3ca | 22 | protected $connection = null; |
3a674fcc MW |
23 | |
24 | /** | |
25 | * last received status code | |
06355ec3 | 26 | * @var string |
3a674fcc | 27 | */ |
158bd3ca | 28 | protected $statusCode = ''; |
3a674fcc MW |
29 | |
30 | /** | |
31 | * last received status message | |
06355ec3 | 32 | * @var string |
3a674fcc | 33 | */ |
158bd3ca | 34 | protected $statusMsg = ''; |
3a674fcc MW |
35 | |
36 | /** | |
37 | * mail recipients | |
06355ec3 | 38 | * @var array |
3a674fcc MW |
39 | */ |
40 | protected $recipients = array(); | |
158bd3ca TD |
41 | |
42 | /** | |
43 | * Creates a new SMTPMailSender object. | |
44 | */ | |
45 | public function __construct() { | |
3a674fcc | 46 | Mail::$lineEnding = "\r\n"; |
158bd3ca TD |
47 | } |
48 | ||
49 | /** | |
50 | * Destroys the SMTPMailSender object. | |
51 | */ | |
52 | public function __destruct() { | |
53 | $this->disconnect(); | |
54 | } | |
9f959ced | 55 | |
158bd3ca TD |
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) { | |
4fe0b42b | 64 | throw new SystemException($this->formatError("can not connect to '".MAIL_SMTP_HOST.":".MAIL_SMTP_PORT."'")); |
158bd3ca TD |
65 | } |
66 | ||
e2b37031 AE |
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 | ||
158bd3ca | 75 | // send ehlo |
e2b37031 | 76 | $this->write('EHLO '.$host); |
4a92e525 TD |
77 | $extensions = explode(Mail::$lineEnding, $this->read()); |
78 | $this->getSMTPStatus(array_shift($extensions)); | |
158bd3ca | 79 | if ($this->statusCode == 250) { |
4a92e525 TD |
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 | ||
158bd3ca TD |
106 | // do authentication |
107 | if (MAIL_SMTP_USER != '' || MAIL_SMTP_PASSWORD != '') { | |
108 | $this->auth(); | |
109 | } | |
110 | } | |
111 | else { | |
112 | // send helo | |
e2b37031 | 113 | $this->write('HELO '.$host); |
158bd3ca TD |
114 | $this->getSMTPStatus(); |
115 | if ($this->statusCode != 250) { | |
4fe0b42b | 116 | throw new SystemException($this->formatError("can not connect to '".MAIL_SMTP_HOST.":".MAIL_SMTP_PORT."'")); |
158bd3ca TD |
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 | /** | |
a17de04e | 132 | * Does the authentification of the client on the server |
158bd3ca TD |
133 | */ |
134 | protected function auth() { | |
a17de04e | 135 | // init authentication |
158bd3ca TD |
136 | $this->write('AUTH LOGIN'); |
137 | $this->getSMTPStatus(); | |
138 | ||
139 | // checks if auth is supported | |
140 | if ($this->statusCode != 334) { | |
4fe0b42b | 141 | throw new SystemException($this->formatError("smtp mail server '".MAIL_SMTP_HOST.":".MAIL_SMTP_PORT."' does not support user authentication")); |
158bd3ca TD |
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) { | |
4fe0b42b | 148 | throw new SystemException($this->formatError("unknown smtp user '".MAIL_SMTP_USER."'")); |
158bd3ca | 149 | } |
a17de04e | 150 | |
158bd3ca TD |
151 | $this->write(base64_encode(MAIL_SMTP_PASSWORD)); |
152 | $this->getSMTPStatus(); | |
153 | if ($this->statusCode != 235) { | |
4fe0b42b | 154 | throw new SystemException($this->formatError("invalid password for smtp user '".MAIL_SMTP_USER."'")); |
158bd3ca TD |
155 | } |
156 | } | |
157 | ||
158 | /** | |
0ad90fc3 | 159 | * @see \wcf\system\mail\MailSender::sendMail() |
158bd3ca TD |
160 | */ |
161 | public function sendMail(Mail $mail) { | |
162 | $this->recipients = array(); | |
15fa2802 MS |
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()); | |
158bd3ca TD |
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) { | |
5e43e307 | 176 | $this->abort(); |
4fe0b42b | 177 | throw new SystemException($this->formatError("wrong from format '".$mail->getFrom()."'")); |
158bd3ca TD |
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) { | |
5e43e307 | 187 | $this->abort(); |
4fe0b42b | 188 | throw new SystemException($this->formatError("wrong recipient format '".$recipient."'")); |
158bd3ca TD |
189 | } |
190 | continue; | |
191 | } | |
192 | $recipientCounter++; | |
193 | } | |
194 | if (!$recipientCounter) { | |
5e43e307 | 195 | $this->abort(); |
158bd3ca TD |
196 | return; |
197 | } | |
198 | ||
199 | // data | |
200 | $this->write("DATA"); | |
201 | $this->getSMTPStatus(); | |
202 | if ($this->statusCode != 354 && $this->statusCode != 250) { | |
5e43e307 | 203 | $this->abort(); |
4fe0b42b | 204 | throw new SystemException($this->formatError("smtp error")); |
158bd3ca | 205 | } |
a17de04e | 206 | |
e2b37031 AE |
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 | ||
158bd3ca | 215 | $header = |
3a674fcc MW |
216 | "Date: ".gmdate('r').Mail::$lineEnding |
217 | ."To: ".$mail->getToString().Mail::$lineEnding | |
e2b37031 | 218 | ."Message-ID: <".md5(uniqid())."@".$serverName.">".Mail::$lineEnding |
3a674fcc | 219 | ."Subject: ".Mail::encodeMIMEHeader($mail->getSubject()).Mail::$lineEnding |
158bd3ca | 220 | .$mail->getHeader(); |
9f959ced | 221 | |
158bd3ca TD |
222 | $this->write($header); |
223 | $this->write(""); | |
dd1ff120 TD |
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 | } | |
158bd3ca | 233 | $this->write("."); |
a17de04e | 234 | |
158bd3ca TD |
235 | $this->getSMTPStatus(); |
236 | if ($this->statusCode != 250) { | |
5e43e307 | 237 | $this->abort(); |
4fe0b42b | 238 | throw new SystemException($this->formatError("message sending failed")); |
158bd3ca TD |
239 | } |
240 | } | |
241 | ||
242 | /** | |
243 | * Disconnects the Client-Server connection | |
244 | */ | |
81c5087a | 245 | public function disconnect() { |
158bd3ca TD |
246 | if ($this->connection === null) { |
247 | return; | |
248 | } | |
9f959ced | 249 | |
158bd3ca TD |
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 | * | |
39bea7dd | 259 | * @return string |
158bd3ca TD |
260 | */ |
261 | protected function read() { | |
262 | $result = ''; | |
263 | while ($read = $this->connection->gets()) { | |
264 | $result .= $read; | |
265 | if (substr($read, 3, 1) == " ") break; | |
266 | } | |
4a92e525 | 267 | |
92fd47d9 | 268 | return $result; |
158bd3ca TD |
269 | } |
270 | ||
5e43e307 | 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 | ||
158bd3ca TD |
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)); | |
01bd2eff | 288 | $this->statusMsg = substr($data, 4); |
158bd3ca TD |
289 | } |
290 | ||
291 | /** | |
292 | * Sends Information to the smtp-Server | |
293 | * | |
294 | * @param string $data | |
295 | */ | |
296 | protected function write($data) { | |
3a674fcc | 297 | $this->connection->puts($data.Mail::$lineEnding); |
158bd3ca | 298 | } |
dcb3a44c | 299 | } |