Mail.class.php (16103B)
1 <?php 2 // OpenRat Content Management System 3 // Copyright (C) 2002-2012 Jan Dankert, cms@jandankert.de 4 // 5 // This program is free software; you can redistribute it and/or 6 // modify it under the terms of the GNU General Public License 7 // as published by the Free Software Foundation; either version 2 8 // of the License, or (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU General Public License for more details. 14 // 15 // You should have received a copy of the GNU General Public License 16 // along with this program; if not, write to the Free Software 17 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 18 19 namespace util; 20 21 use cms\base\Configuration; 22 use cms\base\Language; 23 use cms\base\Startup; 24 use LogicException; 25 use util\text\TextMessage; 26 use util\text\variables\VariableResolver; 27 28 /** 29 * Erzeugen und Versender einer E-Mail gemaess RFC 822.<br> 30 * <br> 31 * Die E-Mail kann entweder �ber 32 * - die interne PHP-Funktion "mail()" versendet werden oder 33 * - direkt per SMTP-Protokoll an einen SMTP-Server.<br> 34 * Welcher Weg gew�hlt wird, kann konfiguriert werden.<br> 35 * <br> 36 * Prinzipiell spricht nichts gegen die interne PHP-Funktion mail(), wenn diese 37 * aber nicht zu Verf�gung steht oder PHP ungeeignet konfiguriert ist, so kann 38 * SMTP direkt verwendet werden. Hierbei sollte wenn m�glich ein Relay-Host 39 * eingesetzt werden. Die Mail kann zwar auch direkt an Mail-Exchanger (MX) des 40 * Empf�ngers geschickt werden, falls dieser aber Greylisting einsetzt ist eine 41 * Zustellung nicht m�glich.<br> 42 * <br> 43 * 44 * @author Jan Dankert 45 */ 46 class Mail 47 { 48 private $from = ''; 49 private $to = ''; 50 private $bcc = ''; 51 private $cc = ''; 52 private $subject = ''; 53 private $text = ''; 54 private $header = array(); 55 private $nl = ''; 56 private $vars = []; 57 58 /** 59 * Falls beim Versendern der E-Mail etwas schiefgeht, steht hier drin 60 * die technische Fehlermeldung. 61 * 62 * @var array Fehler 63 */ 64 public $debugLog = array(); 65 66 /** 67 * Set to true for debugging. 68 * If true, All SMTP-Commands are written to error log. 69 * 70 * @var bool 71 */ 72 public $debug = true; 73 /** 74 * @var string|string 75 */ 76 private $mailKey; 77 78 79 /** 80 * Konstruktor. 81 * Es werden folgende Parameter erwartet 82 * @param String $to Empf�nger 83 * @param string $subjectKey 84 * @param string $mailKey 85 */ 86 function __construct($to, $subjectKey, $mailKey) 87 { 88 $mailConfig = Configuration::subset('mail'); 89 90 91 // Zeilenumbruch CR/LF gem. RFC 822. 92 $this->nl = chr(13) . chr(10); 93 94 if ( $mailConfig->has('from')) 95 $this->from = $this->header_encode($mailConfig->get('from')); 96 97 // Priorit�t definieren (sofern konfiguriert) 98 if ($mailConfig->has('priority')) 99 $this->header[] = 'X-Priority: ' . $mailConfig->get('priority'); 100 101 $this->header[] = 'X-Mailer: ' . $this->header_encode(Startup::TITLE . ' ' . Startup::VERSION); 102 $this->header[] = 'Content-Type: text/plain; charset=UTF-8'; 103 $this->subject = $this->header_encode(Language::lang($subjectKey)); 104 $this->to = $this->header_encode($to); 105 106 $this->text = $this->nl . wordwrap(Language::lang($mailKey), 70, $this->nl) . $this->nl; 107 108 // Signatur anhaengen (sofern konfiguriert) 109 $signature = $mailConfig->get('signature',''); 110 if ( ! $signature) 111 $signature = Configuration::get(['application','name']); 112 113 if ( $signature ) { 114 $this->text .= $this->nl . '-- ' . $this->nl; 115 $this->text .= $this->nl . $signature; 116 $this->text .= $this->nl; 117 } 118 119 // Kopie-Empf�nger 120 if ( $mailConfig->has('cc')) 121 $this->cc = $this->header_encode( implode(', ',$mailConfig->get('cc',[]))); 122 123 // Blindkopie-Empf�nger 124 if ( $mailConfig->has('bcc')) 125 $this->bcc = $this->header_encode( implode(', ',$mailConfig->get('bcc',[]))); 126 127 $this->mailKey = $mailKey; 128 } 129 130 131 /** 132 * Kodiert einen Text in das Format "Quoted-printable".<br> 133 * See RFC 2045. 134 * @param string $text Eingabe 135 * @return string Text im quoted-printable-Format 136 */ 137 private function quoted_printable_encode($text) 138 { 139 $text = str_replace(' ', '=20', $text); 140 141 for ($i = 128; $i <= 255; $i++) { 142 $text = str_replace(chr($i), '=' . dechex($i), $text); 143 } 144 145 return $text; 146 } 147 148 149 /** 150 * Setzen einer Variablen in den Mail-Inhalt. 151 */ 152 public function setVar($varName, $varInhalt) 153 { 154 $this->vars[ $varName ] = $varInhalt; 155 } 156 157 158 /** 159 * Mail absenden. 160 * Die E-Mail wird versendet. 161 */ 162 public function send() 163 { 164 $mailConfig = Configuration::subset('mail'); 165 166 if (strpos($this->to, '@') === FALSE) 167 throw new LogicException("E-Mail-Adress does not contain a domain name: " . $this->to); 168 169 $to_domain = explode('@', $this->to)[1]; 170 171 // Prüfen gegen die Whitelist 172 $white = $mailConfig->get('whitelist',[]); 173 174 if ($white) { 175 if (!$this->containsDomain($to_domain, $white)) { 176 // Wenn Domain nicht in Whitelist gefunden, dann Mail nicht verschicken. 177 throw new LogicException( TextMessage::create('Mail-Domain ${0} is not whitelisted',[$to_domain])); 178 } 179 } 180 181 // Prüfen gegen die Blacklist 182 $black = $mailConfig->get('blacklist',[]); 183 184 if ($black) { 185 if ($this->containsDomain($to_domain, $black)) { 186 // Wenn Domain in Blacklist gefunden, dann Mail nicht verschicken. 187 throw new LogicException( TextMessage::create('Mail-Domain ${0} is blacklisted',[$to_domain])); 188 } 189 } 190 191 // Header um Adressangaben erg�nzen. 192 if (!empty($this->from)) 193 $this->header[] = 'From: ' . $this->from; 194 195 if (!empty($this->cc)) 196 $this->header[] = 'Cc: ' . $this->cc; 197 198 if (!empty($this->bcc)) 199 $this->header[] = 'Bcc: ' . $this->bcc; 200 201 // Evaluate variables in mail data 202 $resolver = new VariableResolver(); 203 $resolver->addDefaultResolver( function($key) { 204 return $this->vars[$key]; 205 } ); 206 $text = $resolver->resolveVariables( $this->text ); 207 208 // Mail versenden 209 if (strtolower($mailConfig->get('client','php')) == 'php') { 210 // PHP-interne Mailfunktion verwenden. 211 $result = @mail($this->to, // Empf�nger 212 $this->subject, // Betreff 213 $text, // Inhalt 214 // Lt. RFC822 müssen Header mit CRLF enden. 215 // ABER: Der Parameter "additional headers" verlangt offenbar \n am Zeilenende. 216 implode("\n", $this->header)); 217 if (!$result) 218 // Die E-Mail wurde nicht akzeptiert. 219 // Genauer geht es leider nicht, da mail() nur einen boolean-Wert 220 // zur�ck liefert. 221 throw new LogicException('Mail was NOT accepted by mail(), no further information available. Please look into your system logs.'); 222 223 } else { 224 // eigenen SMTP-Dialog verwenden. 225 $smtpConf = $mailConfig->subset('smtp'); 226 227 if ( $smtpConf->has('host')) { 228 // Eigenen Relay-Host verwenden. 229 $mxHost = $smtpConf->get('host'); 230 $mxPort = $smtpConf->get('port',25); 231 } else { 232 // Mail direkt zustellen. 233 $mxHost = $this->getMxHost($this->to); 234 235 if (empty($mxHost)) 236 throw new LogicException( TextMessage::create('No MX-Entry found for ${0}',[$this->to])); 237 238 if ($smtpConf->is('ssl',false)) 239 $mxPort = 465; 240 else 241 $mxPort = 25; 242 } 243 244 245 if ($smtpConf->has('localhost')) { 246 $myHost = $smtpConf->get('localhost'); 247 } else { 248 $myHost = gethostbyaddr(getenv('REMOTE_ADDR')); 249 } 250 251 if ($smtpConf->is('ssl',false)) 252 $proto = 'ssl'; 253 else 254 $proto = 'tcp'; 255 256 //connect to the host and port 257 $smtpSocket = fsockopen($proto . '://' . $mxHost, $mxPort, $errno, $errstr, intval($smtpConf['timeout'])); 258 259 if (!is_resource($smtpSocket)) { 260 throw new LogicException('Connection failed to: ' . $proto . '://' . $mxHost . ':' . $mxPort . ' (' . $errstr . '/' . $errno . ')'); 261 } 262 263 $smtpResponse = fgets($smtpSocket, 4096); 264 if ($this->debug) 265 $this->debugLog[] = trim($smtpResponse); 266 267 if (substr($smtpResponse, 0, 3) != '220') { 268 throw new LogicException('No 220: ' . trim($smtpResponse) ); 269 } 270 271 if (!is_resource($smtpSocket)) { 272 throw new LogicException('Connection failed to: ' . $smtpConf['host'] . ':' . $smtpConf['port'] . ' (' . $smtpResponse . ')' ); 273 } 274 275 //you have to say HELO again after TLS is started 276 $smtpResponse = $this->sendSmtpCommand($smtpSocket, 'HELO ' . $myHost); 277 278 if (substr($smtpResponse, 0, 3) != '250') { 279 $this->sendSmtpQuit($smtpSocket); 280 throw new LogicException("No 2xx after HELO, server says: " . $smtpResponse); 281 } 282 283 if ($smtpConf['tls']) { 284 $smtpResponse = $this->sendSmtpCommand($smtpSocket, 'STARTTLS'); 285 if (substr($smtpResponse, 0, 3) == '220') { 286 // STARTTLS ist gelungen. 287 //you have to say HELO again after TLS is started 288 $smtpResponse = $this->sendSmtpCommand($smtpSocket, 'HELO ' . $myHost); 289 290 if (substr($smtpResponse, 0, 3) != '250') { 291 $this->sendSmtpQuit($smtpSocket); 292 throw new LogicException("No 2xx after HELO, server says: " . $smtpResponse ); 293 } 294 } else { 295 // STARTTLS ging in die Hose. Einfach weitermachen. 296 } 297 } 298 299 // request for auth login 300 if ( $smtpConf->has('auth_username') && $smtpConf->has('host') ) { 301 $smtpResponse = $this->sendSmtpCommand($smtpSocket, "AUTH LOGIN"); 302 if (substr($smtpResponse, 0, 3) != '334') { 303 $this->sendSmtpQuit($smtpSocket); 304 throw new LogicException("No 334 after AUTH_LOGIN, server says: " . $smtpResponse); 305 } 306 307 if ($this->debug) 308 $this->debugLog[] = 'Login for ' . $smtpConf->get('auth_username'); 309 310 //send the username 311 $smtpResponse = $this->sendSmtpCommand($smtpSocket, base64_encode($smtpConf->get('auth_username'))); 312 if (substr($smtpResponse, 0, 3) != '334') { 313 $this->sendSmtpQuit($smtpSocket); 314 throw new LogicException("No 3xx after setting username, server says: " . $smtpResponse); 315 } 316 317 //send the password 318 $smtpResponse = $this->sendSmtpCommand($smtpSocket, base64_encode($smtpConf->get('auth_password'))); 319 if (substr($smtpResponse, 0, 3) != '235') { 320 $this->sendSmtpQuit($smtpSocket); 321 throw new LogicException("No 235 after sending password, server says: " . $smtpResponse ); 322 } 323 } 324 325 //email from 326 $smtpResponse = $this->sendSmtpCommand($smtpSocket, 'MAIL FROM: <' . $mailConfig->get('from') . '>'); 327 if (substr($smtpResponse, 0, 3) != '250') { 328 $this->sendSmtpQuit($smtpSocket); 329 throw new LogicException("No 2xx after MAIL_FROM, server says: " . $smtpResponse); 330 } 331 332 //email to 333 $smtpResponse = $this->sendSmtpCommand($smtpSocket, 'RCPT TO: <' . $this->to . '>'); 334 if (substr($smtpResponse, 0, 3) != '250') { 335 $this->sendSmtpQuit($smtpSocket); 336 throw new LogicException("No 2xx after RCPT_TO, server says: " . $smtpResponse); 337 } 338 339 //the email 340 $smtpResponse = $this->sendSmtpCommand($smtpSocket, "DATA"); 341 if (substr($smtpResponse, 0, 3) != '354') { 342 $this->sendSmtpQuit($smtpSocket); 343 throw new LogicException("No 354 after DATA, server says: " . $smtpResponse); 344 } 345 346 $this->header[] = 'To: ' . $this->to; 347 $this->header[] = 'Subject: ' . $this->subject; 348 $this->header[] = 'Date: ' . date('r'); 349 $this->header[] = 'Message-Id: ' . '<' . getenv('REMOTE_ADDR') . '.' . time() . '.openrat@' . getenv('SERVER_NAME') . '.' . getenv('HOSTNAME') . '>'; 350 351 //observe the . after the newline, it signals the end of message 352 $smtpResponse = $this->sendSmtpCommand($smtpSocket, implode($this->nl, $this->header) . $this->nl . $this->nl . $text . $this->nl . '.'); 353 if (substr($smtpResponse, 0, 3) != '250') { 354 $this->sendSmtpQuit($smtpSocket); 355 throw new LogicException("No 2xx after putting DATA, server says: " . $smtpResponse); 356 } 357 358 // say goodbye 359 $this->sendSmtpQuit($smtpSocket); 360 } 361 } 362 363 364 /** 365 * Sendet ein SMTP-Kommando zum SMTP-Server. 366 * 367 * @access private 368 * @param Resource $socket TCP/IP-Socket zum SMTP-Server 369 * @param string $cmd SMTP-Kommando 370 * @return Server-Antwort 371 */ 372 private function sendSmtpCommand($socket, $cmd) 373 { 374 if ($this->debug) 375 $this->debugLog[] = 'CLIENT: >>> ' . trim($cmd); 376 if (!is_resource($socket)) { 377 // Die Verbindung ist geschlossen. Dies kann bei dieser 378 // Implementierung eigentlich nur dann passieren, wenn 379 // der Server die Verbindung schlie�t. 380 // Dieser Client trennt die Verbindung nur nach einem "QUIT". 381 $this->debugLog[] = "Connection lost"; 382 return; 383 } 384 385 fputs($socket, $cmd . $this->nl); 386 $response = trim(fgets($socket, 4096)); 387 if ($this->debug) 388 $this->debugLog[] = 'SERVER: <<< ' . $response; 389 return $response; 390 } 391 392 393 /** 394 * Sendet ein QUIT zum SMTP-Server, wartet die Antwort ab und 395 * schlie�t danach die Verbindung. 396 * 397 * @param Resource Socket 398 */ 399 private function sendSmtpQuit($socket) 400 { 401 402 if ($this->debug) 403 $this->debugLog[] = "CLIENT: >>> QUIT"; 404 if (!is_resource($socket)) 405 return; 406 // Wenn die Verbindung nicht mehr da ist, brauchen wir 407 // auch kein QUIT mehr :) 408 409 410 fputs($socket, 'QUIT' . $this->nl); 411 $response = trim(fgets($socket, 4096)); 412 if ($this->debug) 413 $this->debugLog[] = 'SERVER: <<< ' . $response; 414 415 if (substr($response, 0, 3) != '221') 416 $this->debugLog[] = 'QUIT FAILED: ' . $response; 417 418 fclose($socket); 419 } 420 421 422 /** 423 * Umwandlung von 8-bit-Zeichen in MIME-Header gemaess RFC 2047.<br> 424 * Header d�rfen nur 7-bit-Zeichen enthalten. 8-bit-Zeichen m�ssen kodiert werden. 425 * 426 * @param String $text 427 * @return String 428 */ 429 private function header_encode($text) 430 { 431 $mailConfig = Configuration::subset('mail'); 432 433 if (! $mailConfig->has('header_encoding')) 434 return $text; 435 436 $woerter = explode(' ', $text); 437 $neu = array(); 438 439 440 foreach ($woerter as $wort) { 441 $type = strtolower(substr($mailConfig->get('header_encoding','Quoted-printable'), 0, 1)); 442 $neu_wort = ''; 443 444 if ($type == 'b') 445 $neu_wort = base64_encode($wort); 446 elseif ($type == 'q') 447 $neu_wort = $this->quoted_printable_encode($wort); 448 else 449 throw new LogicException('Mail-Configuration broken: UNKNOWN Header-Encoding type: ' . $type); 450 451 if (strlen($wort) == strlen($neu_wort)) 452 $neu[] = $wort; 453 else 454 $neu[] = '=?UTF-8?' . $type . '?' . $neu_wort . '?='; 455 } 456 457 return implode(' ', $neu); 458 } 459 460 461 /** 462 * Ermittelt den MX-Eintrag zu einer E-Mail-Adresse.<br> 463 * Es wird der Eintrag mit der h�chsten Priorit�t ermittelt. 464 * 465 * @param String E-Mail-Adresse des Empf�ngers. 466 * @return MX-Eintrag 467 */ 468 private function getMxHost($to) 469 { 470 list($user, $host) = explode('@', $to . '@'); 471 472 if (empty($host)) { 473 throw new LogicException( TextMessage::create('Illegal mail address ${0}: No hostname found',[$to]) ); 474 } 475 476 list($host) = explode('>', $host); 477 478 $mxHostsName = array(); 479 $mxHostsPrio = array(); 480 getmxrr($host, $mxHostsName, $mxHostsPrio); 481 482 $mxList = array(); 483 foreach ($mxHostsName as $id => $mxHostName) { 484 $mxList[$mxHostName] = $mxHostsPrio[$id]; 485 } 486 asort($mxList); 487 return key($mxList); 488 } 489 490 491 /** 492 * Stellt fest, ob die E-Mail-Adresse eine gueltige Syntax besitzt. 493 * 494 * Es wird nur die Syntax geprüft. Ob die Adresse wirklich existiert, steht dadurch noch lange 495 * nicht fest. Dazu müsste man die MX-Records auflösen und einen Zustellversuch unternehmen. 496 * 497 * @param $email_address string Adresse 498 * @return true, falls Adresse OK, sonst false 499 */ 500 public static function checkAddress($email_address) 501 { 502 // Source: de.php.net/ereg 503 return \preg_match("/^[_a-z0-9-+]+(\.[_a-z0-9-+]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/i", $email_address); 504 } 505 506 507 /** 508 * Prüft, ob eine Domain in einer List von Domains enthalten ist. 509 * 510 * @param $checkDomain string zu prüfende Domain 511 * @param $domains string Liste von Domains als kommaseparierte Liste 512 * @return true, falls vorhanden, sonst false 513 */ 514 private static function containsDomain($checkDomain, $domains) 515 { 516 foreach ($domains as $domain) { 517 $domain = trim($domain); 518 519 if (empty($domain)) 520 continue; 521 522 if ($domain == substr($checkDomain, -strlen($domain))) { 523 return true; 524 } 525 } 526 return false; 527 } 528 529 public function __toString() 530 { 531 return TextMessage::create('Mail to ${0} with subject key ${1}',[$this->to,$this->mailKey]); 532 } 533 }