/** * construct mime email and send it out. * * @param string transport backend */ public function send($via = '') { //! allow temporarly override backend. Only url allowed, not array if (!empty($via)) { $this->getBackEnd(@parse_url($via)); } //! sanity checks if (empty($this->via)) { throw new EmailException(L('Mailer backend not configured!')); } if (empty($this->message)) { throw new EmailException(L('Empty message!')); } if (empty($this->header['Subject'])) { throw new EmailException(L('No subject given!')); } if (empty($this->header['To'])) { throw new EmailException(L('No recipient given!')); } if (count($this->header['To']) > 64) { // @codeCoverageIgnoreStart throw new EmailException(L('Too many recipients!')); } // @codeCoverageIgnoreEnd $this->address(self::$sender, 'From'); $local = @explode('@', array_keys($this->header['From'])[0])[1]; if (empty($local)) { $local = 'localhost'; } $id = sha1(uniqid()) . '_' . microtime(true) . '@' . $local; //! message type $isHtml = preg_match('/<html/i', $this->message); //! *** handle transport backends that does not require mime message *** if ($this->via == 'db') { //! mail queue in database if (empty(DS::db())) { throw new EmailException(L('DB queue backend without configured datasource!')); } return DS::exec('INSERT INTO email_queue (data,created) VALUES (?,?);', [$this->get(), Core::$core->now]) > 0 ? true : false; } elseif ($this->via == 'phpmailer') { //! PHP Mailer if (!ClassMap::has('PHPMailer')) { throw new EmailException(L('PHPMailer not installed!')); } // @codeCoverageIgnoreStart $mail = new \PHPMailer(); $mail->Subject = $this->header['Subject']; $mail->SetFrom(implode(', ', $this->header['From'])); if (!empty($this->header['Reply-To'])) { $mail->AddReplyTo($this->header['Reply-To']); } foreach (['To', 'Cc', 'Bcc'] as $type) { foreach ($this->header[$type] as $rcpt => $full) { list($name) = explode('<', $full); $mail->SetAddress(self::$forge ? self::$forge : $rcpt, trim($name)); } } foreach ($this->attach as $attach) { $mail->AddAttachment($attach['file']); } $mail->MsgHTML($this->message); return $mail->Send(); // @codeCoverageIgnoreEnd } //! *** build mime message *** //! mime headers $headers['MIME-Version'] = '1.0'; $headers['Content-Class'] = 'urn:content-classes:message'; $headers['Content-Type'] = 'text/plain;charset=utf-8'; $headers['Content-Transfer-Encoding'] = '8bit'; $headers['Sender'] = implode(', ', $this->header['From']); $headers['Message-ID'] = '<' . $id . '>'; $headers['Date'] = date('r', Core::$core->now); $headers['X-Mailer'] = 'PHPPE ' . VERSION; foreach ($this->header as $k => $v) { $headers[$k] = is_array($v) ? implode(', ', $v) : $v; } //! mime body if (!$isHtml) { //! plain text email $message = wordwrap($this->message, 78); } else { $boundary = uniqid(); //! html email with a plain text alternative $headers['Content-Type'] = "multipart/alternative;\n boundary=\"" . $boundary . '"'; $message = "This is a multi-part message in MIME format.\r\n"; $message .= '--' . $boundary . "\n" . "Content-type: text/plain;charset=utf-8\n" . "Content-Transfer-Encoding: 8bit\n\n" . wordwrap(preg_replace("/\\<.*?\\>/m", '', strtr($this->message, ['</h1>' => "\n\n", '</h2>' => "\n\n", '</h3>' => "\n\n", '</h4>' => "\n\n", '</h5>' => "\n\n", '</h6>' => "\n\n", '</p>' => "\n\n", '</td>' => "\t", '</tr>' => "\n", '</table>' => "\n", '<br>' => "\n", '<br/>' => "\n"])), 78) . "\r\n"; //! look for images in html, if found, we have to create a multipart/related block if (preg_match_all("/(http|images\\/|data\\/).*?\\.(gif|png|jpe?g)/mis", $this->message, $m, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { $boundary2 = uniqid(); $diff = 0; foreach ($m as $k => $c) { //if it's a absolute url, don't replace it if ($c[1][0] == 'http') { unset($m[$k]); continue; } //generate a cid $m[$k][3] = uniqid(); //get local path for filename if ($c[1][0] == 'data/' && file_exists($c[0][0])) { $m[$k][4] = $c[0][0]; } elseif (file_exists('public/' . $c[0][0])) { $m[$k][4] = 'public/' . $c[0][0]; } else { foreach (['vendor/phppe/*/', 'vendor/*/', 'vendor/*/*/'] as $d) { if ($m[$k][4] = @glob($d . $c[0][0])[0]) { break; } } } //replace image url in message $new = 'cid:' . $m[$k][3]; $this->message = substr($this->message, 0, $c[0][1] + $diff) . $new . substr($this->message, $c[0][1] + $diff + strlen($c[0][0])); $diff -= strlen($c[0][0]) - strlen($new); } } if (!empty($m)) { //! add the html part as related $message .= '--' . $boundary . "\n" . "Content-type: multipart/related;\n boundary=\"" . $boundary2 . "\"\n\n" . "This is a multi-part message in MIME format.\r\n--" . $boundary2 . "\n" . "Content-Type: text/html;charset=utf-8\n" . "Content-Transfer-Encoding: 8bit\n\n" . wordwrap($this->message, 78) . "\r\n"; foreach ($m as $c) { $data = empty($c[4]) ? '' : (substr($c[4], 0, 4) == 'http' ? Core::get($c[4]) : file_get_contents($c[4])); if (!$data) { continue; } //get content $message .= '--' . $boundary2 . "\n" . 'Content-Type: image/' . ($c[2][0] == 'jpg' ? 'jpeg' : $c[2][0]) . "\n" . "Content-Transfer-Encoding: base64\n" . "Content-Disposition: inline\n" . 'Content-ID: <' . $c[3] . ">\n\n" . chunk_split(base64_encode($data), 78, "\n"); } $message .= '--' . $boundary2 . "--\n"; } else { $message .= '--' . $boundary . "\n" . "Content-type: text/html;charset=utf-8\n" . "Content-Transfer-Encoding: 8bit\n\n" . wordwrap($this->message, 78) . "\r\n"; } $message .= '--' . $boundary . "--\n"; } if (!empty($this->attach)) { $boundary = uniqid(); $headers['Content-Type'] = "multipart/mixed;\n boundary=\"" . $boundary . '"'; $message = "This is a multi-part message in MIME format.\r\n--" . $boundary . "\n" . $message; foreach ($this->attach as $attach) { $data = !empty($attach['data']) ? $attach['data'] : (substr($attach['file'], 0, 4) == 'http' ? Core::get($attach['file']) : file_get_contents(substr($attach['file'], 0, 6) == 'images' ? @glob('vendor/phppe/*/' . $attach['file'])[0] : $attach['file'])); if (!$data) { continue; } $message .= '--' . $boundary . "\n" . 'Content-type: ' . (!empty($attach['mime']) ? $attach['mime'] : 'application-octet-stream') . "\n" . 'Content-Disposition: attachment' . (!empty($attach['file']) ? ";\n filename=\"" . basename($attach['file']) . '"' : '') . "\n" . "Content-Transfer-Encoding: base64\n\n" . chunk_split(base64_encode($data), 78, "\n"); } $message .= '--' . $boundary . "--\n"; } //! flat headers $header = ''; //! redirect message to a specific address (for testing) if (!empty(self::$forge)) { // @codeCoverageIgnoreStart $headers['To'] = self::$forge; $headers['Cc'] = ''; $headers['Bcc'] = ''; // @codeCoverageIgnoreEnd } foreach ($headers as $k => $v) { $header .= $k . ': ' . $v . "\r\n"; } //! log that we are sending a mail Core::log('I', 'To: ' . $headers['To'] . ', Subject: ' . $headers['Subject'] . ', ID: ' . $id, 'email'); //if email directory exists, save the full mime message as well for debug @file_put_contents('data/log/email/' . $id, 'Backend: ' . $this->via . ' ' . self::$user . ':' . self::$pass . '@' . self::$host . ':' . self::$port . "\r\n\r\n" . $header . "\r\n" . $message); //! *** handle transport backends *** switch ($this->via) { //! only log and possibly save message in file, do not send for real. Nothing left to do case 'log': break; //! return constructed mime message //! return constructed mime message case 'mime': return $header . "\r\n" . $message; break; //! use php's mail() //! use php's mail() case 'mail': $to = $headers['To']; $subj = $headers['Subject']; unset($headers['To']); unset($headers['Subject']); $header = ''; foreach ($headers as $k => $v) { $header .= $k . ': ' . $v . "\r\n"; } if (!mail($to, $subj, $message, $header)) { Core::log('E', 'mail() failed, To: ' . $to . ', Subject: ' . $subj . ', ID: ' . $id, 'email'); return false; } // @codeCoverageIgnoreStart break; //! sendmail through pipe //! sendmail through pipe case 'sendmail': $f = @popen('/usr/sbin/sendmail -t -i', 'w'); if ($f) { fputs($f, $header . "\r\n" . $message); pclose($f); } else { Core::log('E', 'mail() failed, To: ' . $headers['To'] . ', Subject: ' . $headers['Subject'] . ', ID: ' . $id, 'email'); return false; } break; //! this is how real programmers do it, let's speak smtp directly! //! this is how real programmers do it, let's speak smtp directly! default: //open socket $s = @fsockopen(self::$host, self::$port, $en, $es, 5); $l = ''; //get welcome message if ($s) { stream_set_timeout($s, 5); $l = fgets($s, 1024); } if (!$s || substr($l, 0, 3) != '220') { Core::log('E', 'connection error to ' . self::$host . ':' . self::$port . ', ' . trim($l), 'email'); return false; } //we silently assume we got 8BITMIME here, it's a safe assumption as of 2016 while ($l[3] == '-') { $l = fgets($s, 1024); } //greet remote fputs($s, 'EHLO ' . $local . "\r\n"); $l = fgets($s, 1024); while ($l[3] == '-') { $l = fgets($s, 1024); } //tell who are sending fputs($s, 'MAIL FROM: <' . array_keys($this->header['From'])[0] . ">\r\n"); $l = fgets($s, 1024); if (substr($l, 0, 3) != '250') { PPHPE3::log('E', 'from error: ' . trim($l), 'email'); return false; } //to whom $addresses = array_merge(array_keys($this->header['To']), array_keys($this->header['Cc']), array_keys($this->header['Bcc'])); foreach ($addresses as $a) { fputs($s, 'RCPT TO: <' . $a . ">\r\n"); $l = fgets($s, 1024); if (substr($l, 0, 3) != '250') { Core::log('E', 'recipient error: ' . trim($l), 'email'); } } //the message fputs($s, "DATA\r\n"); $l = fgets($s, 1024); if (substr($l, 0, 3) != '250') { Core::log('E', 'data error: ' . trim($l), 'email'); return false; } fputs($header . "\r\n" . str_replace(array("\n.\n", "\n.\r"), array("\n..\n", "\n..\r"), $message) . "\r\n.\r\n"); $l = fgets($s, 1024); if (substr($l, 0, 3) != '250') { Core::log('E', 'data send error: ' . trim($l), 'email'); return false; } //say bye fputs($s, "QUIT\r\n"); fclose($s); // @codeCoverageIgnoreEnd } return true; }
public static function req2arr($p, $V = [], $a = 1) { if ($a) { $o = array(); } else { $o = new \stdClass(); } if (!empty(self::$v)) { $V += self::$v; } $R = $_REQUEST; foreach ($V as $K => $v) { if (substr($K, 0, strlen($p) + 1) == $p . '.') { $r = $p . '_' . substr($K, strlen($p) + 1); if (empty($R[$r])) { $R[$r] = ''; } } } foreach ($R as $k => $v) { if (substr($k, 0, strlen($p) + 1) == $p . '_' && $k[strlen($k) - 2] != ':') { $d = substr($k, strlen($p) + 1); $K = $p . '.' . $d; if (isset($V[$K])) { foreach ($V[$K] as $T => $C) { $t = '\\PHPPE\\AddOn\\' . $T; if ((!empty($v) || $T == "check" || $T == "file") && ClassMap::has($t, 'validate')) { list($r, $m) = $t::validate($K, $v, $C[1], $C[2]); if (!$r && $m) { $O = explode('.', $K); self::error(L(ucfirst(!empty($O[1]) ? $O[1] : $O[0])) . ' ' . L($m), $K); } } if (!empty($C[0]) && empty($v)) { $v = null; self::error(L(ucfirst($d)) . ' ' . L('is a required field.'), $K); } } } $v = $v == 'true' ? true : ($v == 'false' ? false : $v); if ($a) { $o[$d] = $v; } else { $o->{$d} = $v; } } } return $o; }