openrat-cms

OpenRat Content Management System
git clone http://git.code.weiherhei.de/openrat-cms.git
Log | Files | Refs | README

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 }