Simplify validation to check DNS records instead of MX records Remove redundant port 80 checks that fail for mail-only subdomains Maintain compatibility with services like Mailgun and SendGrid
1670 lines
61 KiB
PHP
1670 lines
61 KiB
PHP
<?php
|
|
|
|
/**
|
|
* PHPMailer.Lite.php
|
|
*
|
|
* @see https://sourceforge.net/projects/phpmailer-lite/
|
|
*
|
|
* @category Email Transport
|
|
* @package PHPMailer Lite
|
|
* @author Andy Prevost <andy@codeworxtech.com>
|
|
* @copyright 2004-2023 (C) Andy Prevost - All Rights Reserved
|
|
* @version 2024.1.3.0
|
|
* @requires PHP version 8.0.0 (and up)
|
|
* @license MIT - Distributed under the MIT License
|
|
* shown here:
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the 'Software'), to deal in the Software without
|
|
* restriction, including without limitation the rights to use, copy,
|
|
* modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
* of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
* DEALINGS IN THE SOFTWARE.
|
|
*
|
|
**/
|
|
/* Last updated: 21 Mar 2024 @ 16:34 (EST) */
|
|
|
|
namespace codeworxtech\PHPMailerLite;
|
|
|
|
if (version_compare(PHP_VERSION, "8.0.0", "<=")) {
|
|
exit("Sorry, this version of PHPMailer Lite will only run on PHP version 8.0.0 or greater!\n");
|
|
}
|
|
|
|
class PHPMailerLite
|
|
{
|
|
|
|
/* CONSTANTS */
|
|
const VERSION = "2024.1.3.0";
|
|
const CRLF = "\r\n";
|
|
const MAILSEP = ", ";
|
|
|
|
/* SMTP CONSTANTS */
|
|
const TIMEVAL = 30; // seconds
|
|
|
|
/* PROPERTIES, PRIVATE & PROTECTED */
|
|
private array $attachments = [];
|
|
private string $bcc = "";
|
|
private array $boundary = [];
|
|
private string $charSet = "utf-8";
|
|
private string $cc = "";
|
|
private string $confirm_read = "";
|
|
private array $customHeader = [];
|
|
private string $encode_hdr = "base64";
|
|
private string $msgType = "";
|
|
private string $recipients = "";
|
|
private string $recipients_rt = "";
|
|
private string $replyTo = "";
|
|
private string $sender = "";
|
|
private string $senderEmail = "root@localhost";
|
|
private string $senderName = "No Reply";
|
|
private string $sendmailPath = "";
|
|
private string $transport = "";
|
|
private array $transports = ["smtp", "sendmail", "imap"];
|
|
private int $wordWrapLen = 70;
|
|
private string $timeStart = "";
|
|
|
|
/* PROPERTIES, PUBLIC */
|
|
/**
|
|
* debug has a cap
|
|
*/
|
|
public string $debug = "";
|
|
public string $hostname = "";
|
|
public bool $imapAddToSent = false;
|
|
public string $imapHost = "";
|
|
public string $imapPort = "143/imap/notls";
|
|
public string $imapUsername = "";
|
|
public string $imapPassword = "";
|
|
public string $messageICal = "";
|
|
public string $messageHTML = "";
|
|
public string $messageText = "";
|
|
public string $mimeMail = "";
|
|
public int $priority = 0;
|
|
public string $returnPath = "";
|
|
public string $subject = "";
|
|
public bool $useIMAP = false;
|
|
|
|
/* SMTP PROPERTIES, PUBLIC PRIVATE & PROTECTED */
|
|
public array $smtpAccount = [];
|
|
public bool $smtpDebug = false;
|
|
public string $smtpDomain = "";
|
|
private $smtperr = "";
|
|
private $smtperrstr = "";
|
|
public string $smtpFrom = "";
|
|
public string $smtpHost = "";
|
|
public bool $smtpKeepAlive = false;
|
|
public array $smtpOptions = []; //["ssl"=>["verify_peer"=>false,"verify_peer_name"=>false,"allow_self_signed"=>true]];
|
|
public string $smtpPassword = "";
|
|
public string $smtpPort = "";
|
|
private $smtpStream = 0;
|
|
public string $smtpUsername = "";
|
|
public bool $smtpUseVerp = false;
|
|
|
|
/* METHODS ************/
|
|
|
|
function __construct()
|
|
{
|
|
$this->timeStart = microtime(true);
|
|
// Set boundaries
|
|
$uniqId = md5(uniqid(time()));
|
|
$this->boundary[0] = "P0_" . $uniqId;
|
|
$this->boundary[1] = "P1_" . $uniqId;
|
|
$this->boundary[2] = "P2_" . $uniqId;
|
|
}
|
|
|
|
function __destruct()
|
|
{
|
|
unset($this->cc);
|
|
unset($this->bcc);
|
|
unset($this->recipients);
|
|
unset($this->attachments);
|
|
unset($this->messageICal);
|
|
unset($this->messageHTML);
|
|
unset($this->messageText);
|
|
}
|
|
|
|
/**
|
|
* Adds an attachment from a path on the filesystem.
|
|
* Returns false if the file could not be found
|
|
* or accessed.
|
|
* @param string $path Path to the attachment.
|
|
* @param string $name Overrides the attachment name.
|
|
* @param string $encoding File encoding (see $Encoding).
|
|
* @param string $type File extension (MIME) type.
|
|
* @return bool
|
|
*/
|
|
public function AddAttachment($path, $name = "", $encoding = "base64", $type = "")
|
|
{
|
|
self::IsExploitPath($path, true);
|
|
if ($type == "") {
|
|
$type = self::GetMimeType($path);
|
|
}
|
|
$filename = basename($path);
|
|
if ($name == "") {
|
|
$name = $filename;
|
|
}
|
|
$this->attachments[] = [0 => $path, 1 => $filename, 2 => $name, 3 => $encoding, 4 => $type, 5 => false, 6 => "attachment", 7 => 0];
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add a BCC
|
|
* @param string $bcc
|
|
*/
|
|
public function AddBCC($param)
|
|
{
|
|
$sep = (trim($this->bcc) != "") ? self::MAILSEP : "";
|
|
$this->bcc .= $sep . self::EmailFormatRFC($param);
|
|
$sep = (trim($this->recipients_rt) != "") ? self::MAILSEP : "";
|
|
$this->recipients_rt .= $sep . self::EmailExtractEmail($param);
|
|
}
|
|
|
|
/**
|
|
* Add a CC
|
|
* @param string $cc
|
|
*/
|
|
public function AddCC($param)
|
|
{
|
|
$sep = (trim($this->cc) != "") ? self::MAILSEP : "";
|
|
$this->cc .= $sep . self::EmailFormatRFC($param);
|
|
$sep = (trim($this->recipients_rt) != "") ? self::MAILSEP : "";
|
|
$this->recipients_rt .= $sep . self::EmailExtractEmail($param);
|
|
}
|
|
|
|
/**
|
|
* Adds a custom header
|
|
*/
|
|
public function AddCustomHeader($custom_header)
|
|
{
|
|
$this->customHeader = explode(":", $custom_header, 2);
|
|
}
|
|
|
|
/**
|
|
* Adds an embedded attachment. This can include images (backgrounds,etc).
|
|
* @param string $path Path (location) of attachment.
|
|
* @param string $cid Content ID of the attachment. Use to identify
|
|
* the Id for accessing the image in an HTML doc.
|
|
* @param string $name Overrides the attachment name.
|
|
* @param string $encoding Mime encoding.
|
|
* @param string $type File extension.
|
|
* @return bool
|
|
*/
|
|
public function AddEmbeddedImage($path, $cid, $name = "", $encoding = "", $type = "", $attach = "")
|
|
{
|
|
if ($encoding == "") {
|
|
$encoding = "base64";
|
|
}
|
|
self::IsExploitPath($path, true);
|
|
if (!@is_file($path)) {
|
|
return false;
|
|
}
|
|
if ($type == "") {
|
|
$type = mime_content_type($path);
|
|
}
|
|
$filename = basename($path);
|
|
if ($name == "") {
|
|
$name = $filename;
|
|
}
|
|
if (!self::IsImageDuplicate(0, $path)) {
|
|
// Append to $attachments array
|
|
$this->attachments[] = [0 => $path, 1 => $filename, 2 => $name, 3 => $encoding, 4 => $type, 5 => false, 6 => "inline", 7 => $cid];
|
|
// Add second copy as downloadable attachment
|
|
if ($attach != "") {
|
|
$mimeType = mime_content_type($path);
|
|
$this->attachments[] = [0 => $path, 1 => $attach, 2 => $attach, 3 => $encoding, 4 => $mimeType, 5 => false, 6 => "attachment", 7 => 0];
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Adds email message to IMAP INBOX.Sent
|
|
* @param string $message (optional)
|
|
* @param string $folder (optional)
|
|
* @param string $options (optional)
|
|
*/
|
|
public function AddMessageToSent($folder = "INBOX.Sent", $options = null)
|
|
{
|
|
if (!$this->imapAddToSent) {
|
|
return;
|
|
}
|
|
$mailbox = "{" . $this->imapHost . ":" . $this->imapPort . "}" . $folder;
|
|
if (!empty($this->imapHost) && !empty($this->imapUsername) && !empty($this->imapPassword)) {
|
|
$cnx = @imap_open($mailbox, $this->imapUsername, $this->imapPassword);
|
|
$res = imap_append($cnx, $mailbox, $this->mimeMail, null, date("r"));
|
|
@imap_errors();
|
|
@imap_alerts();
|
|
@imap_close($cnx);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a recipient
|
|
* @param string $email
|
|
*/
|
|
public function AddRecipient($param)
|
|
{
|
|
$sep = (trim($this->recipients) != "") ? self::MAILSEP : "";
|
|
$this->recipients .= $sep . self::EmailFormatRFC($param);
|
|
$sep = (trim($this->recipients_rt) != "") ? self::MAILSEP : "";
|
|
$this->recipients_rt .= $sep . self::EmailExtractEmail(self::EmailFormatRFC($param));
|
|
}
|
|
|
|
/**
|
|
* Adds a string or binary attachment (non-filesystem) to the list.
|
|
* This method can be used to attach ascii or binary data,
|
|
* such as a BLOB record from a database.
|
|
* @param string $string String attachment data.
|
|
* @param string $filename Name of the attachment.
|
|
* @param string $encoding File encoding (see $Encoding).
|
|
* @param string $type File extension (MIME) type.
|
|
* @return void
|
|
*/
|
|
public function AddStringAttachment($string, $filename, $encoding = "base64", $type = "")
|
|
{
|
|
if ($type == "") {
|
|
self::GetMimeType($string, "string");
|
|
}
|
|
$this->attachments[] = [0 => $string, 1 => $filename, 2 => basename($filename), 3 => $encoding, 4 => $type, 5 => true, 6 => "attachment", 7 => 0];
|
|
}
|
|
|
|
/**
|
|
* Build attachment
|
|
* @param string $attachment
|
|
* @return string
|
|
*/
|
|
private function BuildAttachment($attachment = "", $bkey = 0)
|
|
{
|
|
$mime = $cidUniq = $incl = [];
|
|
// add parameter passed in function
|
|
if (!empty($attachment) && is_string($attachment)) {
|
|
if (self::IsPathSafe($attachment) !== true) {
|
|
return false;
|
|
}
|
|
$mimeType = mime_content_type($attachment);
|
|
$fileContent = file_get_contents($attachment);
|
|
$fileContent = (!empty($this->encode_hdr)) ? chunk_split(base64_encode($fileContent)) : $fileContent;
|
|
$data = "Content-Type: " . $mimeType . "; name=" . basename($attachment) . self::CRLF;
|
|
$data .= (!empty($this->encode_hdr)) ? "Content-Transfer-Encoding: " . $this->encode_hdr . self::CRLF : "";
|
|
$data .= "Content-ID: <" . basename($attachment) . ">" . self::CRLF;
|
|
$data .= self::CRLF . $fileContent . self::CRLF;
|
|
$data = self::GetBoundary($bkey) . $data;
|
|
return $data;
|
|
}
|
|
// Add all other attachments and check for string attachment
|
|
$bString = $attachment[5];
|
|
if ($bString) {
|
|
$string = $attachment[0];
|
|
} else {
|
|
$path = $attachment[0];
|
|
if (self::IsPathSafe($path) !== true) {
|
|
return false;
|
|
}
|
|
}
|
|
if (in_array($attachment[0], $incl)) {
|
|
return;
|
|
}
|
|
if (@in_array($path, $incl)) {
|
|
return;
|
|
}
|
|
|
|
$filename = $attachment[1];
|
|
$name = $attachment[2];
|
|
$encoding = $attachment[3];
|
|
$type = $attachment[4];
|
|
$disposition = $attachment[6];
|
|
$cid = $attachment[7];
|
|
$incl[] = $attachment[0];
|
|
|
|
if ($disposition == "inline" && isset($cidUniq[$cid])) {
|
|
return;
|
|
}
|
|
$cidUniq[$cid] = true;
|
|
|
|
$mime[] = "Content-Type: " . $type . "; name=\"" . $name . "\"" . self::CRLF;
|
|
if (!empty($encoding)) {
|
|
$mime[] = "Content-Transfer-Encoding: " . $encoding . self::CRLF;
|
|
}
|
|
|
|
if ($disposition == "inline") {
|
|
$mime[] = "Content-ID: <" . $cid . ">" . self::CRLF;
|
|
}
|
|
$mime[] = "Content-Disposition: " . $disposition . "; filename=\"" . $name . "\"" . self::CRLF . self::CRLF;
|
|
|
|
// Encode as string attachment
|
|
if ($bString) {
|
|
$str = (!empty($encoding)) ? chunk_split(base64_encode($string), $this->wordWrapLen, self::CRLF) : chunk_split($string, $this->wordWrapLen, self::CRLF);
|
|
$mime[] = $str;
|
|
$mime[] = self::CRLF . self::CRLF;
|
|
} else {
|
|
$str = (!empty($encoding)) ? chunk_split(base64_encode(file_get_contents($path)), $this->wordWrapLen, self::CRLF) : chunk_split(file_get_contents($path), $this->wordWrapLen, self::CRLF);
|
|
$mime[] = chunk_split(base64_encode(file_get_contents($path)), $this->wordWrapLen, self::CRLF);
|
|
$mime[] = self::CRLF . self::CRLF;
|
|
}
|
|
$data = implode("", $mime);
|
|
$data = self::GetBoundary($bkey) . $data;
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Build message body
|
|
* @return string
|
|
*/
|
|
private function BuildBody()
|
|
{
|
|
self::GetMsgType();
|
|
$body = self::GetMsgPart();
|
|
// $body .= self::CRLF;
|
|
|
|
if ($this->msgType == "message") {
|
|
return $body;
|
|
}
|
|
// attachment only
|
|
elseif ($this->msgType == "attachment") {
|
|
foreach ($this->attachments as $attachment) {
|
|
if ($attachment[6] === "attachment") {
|
|
$body .= self::BuildAttachment($attachment, 0);
|
|
}
|
|
}
|
|
return $body;
|
|
}
|
|
// message with attachment
|
|
elseif ($this->msgType == "attachment|message") {
|
|
$body .= self::CRLF;
|
|
foreach ($this->attachments as $attachment) {
|
|
if ($attachment[6] === "attachment") {
|
|
$body .= self::BuildAttachment($attachment, 0);
|
|
}
|
|
}
|
|
$body .= self::GetBoundary(0, "--");
|
|
}
|
|
// message with attachment
|
|
elseif ($this->msgType == "attachment|inline|message") {
|
|
foreach ($this->attachments as $attachment) {
|
|
if ($attachment[6] === "inline") {
|
|
$body .= self::BuildAttachment($attachment, 1);
|
|
}
|
|
}
|
|
$body .= self::GetBoundary(1, "--");
|
|
$body .= self::CRLF;
|
|
foreach ($this->attachments as $attachment) {
|
|
if ($attachment[6] === "attachment") {
|
|
$body .= self::BuildAttachment($attachment, 0);
|
|
}
|
|
}
|
|
$body .= self::GetBoundary(0, "--");
|
|
$body .= self::CRLF;
|
|
}
|
|
// message with inline (iCalendar option)
|
|
elseif ($this->msgType == "inline|message") {
|
|
$body .= self::CRLF;
|
|
// inline
|
|
foreach ($this->attachments as $attachment) {
|
|
if ($attachment[6] === "inline") {
|
|
$body .= self::BuildAttachment($attachment, 0);
|
|
}
|
|
}
|
|
$body .= self::GetBoundary(0, "--");
|
|
$body .= self::CRLF;
|
|
}
|
|
$body = str_replace(self::CRLF . self::CRLF . self::CRLF, self::CRLF . self::CRLF, $body);
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Builds message header
|
|
* @return string
|
|
*/
|
|
public function BuildHeader($type = "smtp")
|
|
{
|
|
$mimeTxt = "This is a multipart message in MIME format." . self::CRLF;
|
|
$messageID = md5((idate("U") - 1000000000) . uniqid()) . "@" . self::ServerHostname();
|
|
if (empty($this->sender)) {
|
|
if ($this->senderEmail == "root@localhost") {
|
|
$this->senderEmail = "noreply@" . self::GetMailServer();
|
|
}
|
|
self::SetSender([$this->senderEmail => $this->senderName]);
|
|
}
|
|
$hdr = "Return-Path: " . ((!empty($this->returnPath)) ? $this->returnPath : $this->sender) . self::CRLF;
|
|
$hdr .= "Date: " . date("r") . self::CRLF;
|
|
$hdr .= "From: " . $this->sender . self::CRLF;
|
|
$hdr .= "Reply-To: " . ((!empty($this->replyTo)) ? $this->replyTo : $this->sender) . self::CRLF;
|
|
$hdr .= (!empty($this->cc)) ? "Cc: " . $this->cc . self::CRLF : "";
|
|
$hdr .= (!empty($this->bcc)) ? "Bcc: " . $this->bcc . self::CRLF : "";
|
|
$hdr .= "Message-Id: <" . $messageID . ">" . self::CRLF;
|
|
$hdr .= "X-Originating-IP: " . $_SERVER['SERVER_ADDR'] . self::CRLF;
|
|
$hdr .= "X-Mailer: PHPMailer Lite v" . PHPMailerLite::VERSION . " " . $this->transport . " (https://phpmailer.pro/)" . self::CRLF;
|
|
$hdr .= ($this->priority !== 0) ? "X-Priority: " . $this->priority . self::CRLF : "";
|
|
for ($index = 0; $index < count($this->customHeader); $index++) {
|
|
$hdr .= trim($this->customHeader[$index][0]) . ": " . self::MbEncode(trim($this->customHeader[$index][1])) . self::CRLF;
|
|
}
|
|
if ($this->transport == "smtp" || $this->transport == "sendmail") {
|
|
$hdr .= "To: " . $this->recipients . self::CRLF;
|
|
$hdr .= "Subject: " . self::MbEncode($this->subject) . self::CRLF;
|
|
}
|
|
$hdr .= "MIME-Version: 1.0" . self::CRLF;
|
|
if (stripos($this->msgType, "attachment") !== false) {
|
|
$hdr .= self::GetContentTypeHdr("multipart/mixed", 0) . self::CRLF;
|
|
$hdr .= $mimeTxt;
|
|
if (stripos($this->msgType, "inline") !== false) {
|
|
// sub header for inline
|
|
$hdr .= self::GetBoundary(0);
|
|
$hdr .= self::GetContentTypeHdr("multipart/related", 1) . self::CRLF;
|
|
$hdr .= self::CRLF;
|
|
$hdr .= self::GetBoundary(1);
|
|
$hdr .= self::GetContentTypeHdr("multipart/alternative", 2);
|
|
$hdr .= self::CRLF;
|
|
} else {
|
|
// sub header for message
|
|
$hdr .= self::GetBoundary(0);
|
|
$hdr .= self::GetContentTypeHdr("multipart/alternative", 1);
|
|
$hdr .= self::CRLF;
|
|
}
|
|
} elseif (stripos($this->msgType, "inline") !== false) {
|
|
$hdr .= self::GetContentTypeHdr("multipart/related", 0) . self::CRLF;
|
|
$hdr .= $mimeTxt;
|
|
// sub header for message
|
|
$hdr .= self::GetBoundary(0);
|
|
$hdr .= self::GetContentTypeHdr("multipart/alternative", 1) . self::CRLF;
|
|
$hdr .= self::CRLF;
|
|
} elseif ($this->msgType == "message") {
|
|
$hdr .= self::GetContentTypeHdr("multipart/alternative", 0) . self::CRLF;
|
|
$hdr .= $mimeTxt;
|
|
}
|
|
// message only
|
|
return $hdr;
|
|
}
|
|
|
|
/**
|
|
* Sets the HTML message and returns modifications for inline images and backgrounds
|
|
* will also set text message if it does not exist (can over ride)
|
|
* @param string $content content of the HTML message
|
|
* @param string $basedir directory to the location of the images (relative to file)
|
|
*/
|
|
private function DataToHTML($content, $basedir = "")
|
|
{
|
|
if (is_file($content)) {
|
|
self::IsExploitPath($content, true);
|
|
$thisdir = (dirname($content) != "") ? rtrim(dirname($content), '/') . '/' : "";
|
|
$basedir = ($basedir == "") ? $thisdir : "";
|
|
$content = file_get_contents($content);
|
|
}
|
|
preg_match_all("/(src|background)=\"(.*)\"/Ui", $content, $images);
|
|
if (!empty($images[2])) {
|
|
foreach ($images[2] as $i => $url) {
|
|
if (!preg_match('#^[A-z]+://#', $url)) {
|
|
if ($basedir != "") {
|
|
$url = rtrim($basedir, '/') . '/' . $url;
|
|
}
|
|
$filename = basename($url);
|
|
$directory = dirname($url);
|
|
$cid = "cid:" . md5($filename);
|
|
if ($directory == ".") {
|
|
$directory = "";
|
|
}
|
|
$mimeType = mime_content_type($url);
|
|
if (strlen($directory) > 1 && substr($directory, -1) != '/') {
|
|
$directory .= '/';
|
|
}
|
|
self::IsExploitPath($directory . $filename, true);
|
|
if (self::AddEmbeddedImage($directory . $filename, md5($filename), $filename, "base64", $mimeType)) {
|
|
$content = preg_replace("/" . $images[1][$i] . "=\"" . preg_quote($images[2][$i], '/') . "\"/Ui", $images[1][$i] . "=\"" . $cid . "\"", $content);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$this->messageHTML = $content;
|
|
}
|
|
|
|
/**
|
|
* input string or array containing email addresses (separated by comma)
|
|
* in almost any format - can be single address or multiple
|
|
* with or without correct spacing, quote marks
|
|
* removes items without emails
|
|
* returns RFC 5322 formatted string
|
|
* @var string or array
|
|
* @return string
|
|
*/
|
|
private function EmailFormatRFC($data)
|
|
{
|
|
$rz = "";
|
|
if (is_string($data)) {
|
|
foreach ((explode(",", $data)) as $key => $val) {
|
|
$val = trim($val);
|
|
preg_match('/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,9}/', $val, $bk);
|
|
if (filter_var($bk[0], FILTER_VALIDATE_EMAIL)) {
|
|
$email = trim($bk[0]);
|
|
$name = $val;
|
|
if (!empty($email)) {
|
|
if (!empty($name)) {
|
|
$rz .= self::EmailExtractName($name) . " ";
|
|
}
|
|
$rz .= "<" . self::EmailExtractEmail($email) . ">, ";
|
|
}
|
|
}
|
|
}
|
|
return rtrim($rz, ", ");
|
|
} else {
|
|
foreach ($data as $key => $val) {
|
|
$name = $email = "";
|
|
if (is_array($val)) {
|
|
$kkey = trim(str_replace(["<", ">"], "", key($val)));
|
|
$vval = trim(str_replace(["<", ">"], "", $val[$kkey]));
|
|
if (filter_var($kkey, FILTER_VALIDATE_EMAIL)) {
|
|
$email = "<" . $kkey . ">";
|
|
$name = ($vval != "") ? $vval : "";
|
|
} elseif (filter_var($vval, FILTER_VALIDATE_EMAIL)) {
|
|
$email = "<" . $vval . ">";
|
|
$name = ($kkey != "") ? $kkey : "";
|
|
}
|
|
if (!empty($email)) {
|
|
if (!empty($name)) {
|
|
$rz .= self::EmailExtractName($name) . " ";
|
|
}
|
|
$rz .= "<" . self::EmailExtractEmail($email) . ">, ";
|
|
}
|
|
} else {
|
|
if (is_numeric($key) && str_contains($val, "<")) {
|
|
$t = explode("<", $val);
|
|
$key = trim($t[0]);
|
|
$val = trim(str_replace(["<", ">"], "", $t[1]));
|
|
}
|
|
$key = trim($key);
|
|
$val = trim($val);
|
|
if (filter_var($key, FILTER_VALIDATE_EMAIL)) {
|
|
$email = $key;
|
|
$name = ($val != "") ? $val : "";
|
|
} elseif (filter_var($val, FILTER_VALIDATE_EMAIL)) {
|
|
$email = $val;
|
|
$name = (!is_numeric($key) && $key != "") ? $key : "";
|
|
}
|
|
if (!empty($email)) {
|
|
if (!empty($name)) {
|
|
$rz .= self::EmailExtractName($name) . " ";
|
|
}
|
|
$rz .= "<" . self::EmailExtractEmail($email) . ">, ";
|
|
}
|
|
}
|
|
}
|
|
return rtrim($rz, ", ");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* extracts email address from a string
|
|
* returns clean (shell safe) email address (WITHOUT TOKENS)
|
|
* @var string
|
|
* @return string
|
|
*/
|
|
private function EmailExtractEmail($str)
|
|
{
|
|
if (!empty($str) && is_string($str)) {
|
|
preg_match('/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,9}/', $str, $bk);
|
|
if (empty($bk[0])) {
|
|
return false;
|
|
}
|
|
$email = $bk[0];
|
|
$email = str_ireplace(["\r", "\n", "\t", '"', ",", "<", ">"], "", $email);
|
|
if (filter_var($email, FILTER_VALIDATE_EMAIL) && self::IsValidEmail($email)) {
|
|
return $email;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* extracts name portion from string email address
|
|
* returns clean (shell safe) name
|
|
* @var string
|
|
* @return string
|
|
*/
|
|
private function EmailExtractName($str)
|
|
{
|
|
if (trim($str) == "") {
|
|
return;
|
|
}
|
|
$pattern = '/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,9}/';
|
|
preg_match($pattern, $str, $match);
|
|
$addy = (!empty($match[0])) ? $match[0] : "";
|
|
return trim(str_ireplace([$addy, "<", ">", "[", "]", "\r", "\n", "\t"], "", $str));
|
|
}
|
|
|
|
/**
|
|
* Changes all CRLF or CR to LF
|
|
* @return string
|
|
*/
|
|
private function FixCRLF($str)
|
|
{
|
|
return str_ireplace(["\r\n", "\r", "\n"], self::CRLF, $str);
|
|
}
|
|
|
|
/**
|
|
* Creates the boundary line / end boundary line
|
|
* @param string $type = wrap, body, spec, none
|
|
* @param string $end (optional, triggers adding two dashes at end)
|
|
* @return string (boundary line)
|
|
*/
|
|
private function GetBoundary($bkey, $end = "")
|
|
{
|
|
return "--" . $this->boundary[$bkey] . $end . self::CRLF;
|
|
}
|
|
|
|
/**
|
|
* Creates the Content-Type directive for the body
|
|
* @param string $type = multipart/mixed / multipart/related / multipart/alternative
|
|
* @param string $charset
|
|
* @param string $encoding
|
|
* @param string $cid (optional)
|
|
* @return string (content type line)
|
|
*/
|
|
private function GetContentTypeBody($type, $charset, $encoding, $cid = "")
|
|
{
|
|
$data = "Content-Type: " . $type . ";" . self::CRLF;
|
|
$data .= "\t" . $charset . self::CRLF;
|
|
$data .= "Content-Transfer-Encoding: " . $encoding . self::CRLF;
|
|
if ($cid != "") {
|
|
$data .= "Content-ID: <" . $cid . ">" . self::CRLF;
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Creates the Content-Type directive for the header
|
|
* type = multipart/mixed / multipart/related / multipart/alternative
|
|
* bkey = boundary (wrap / body / spec)
|
|
* @return string (content type line)
|
|
*/
|
|
private function GetContentTypeHdr($type, $bkey)
|
|
{
|
|
$data = "Content-Type: " . $type . ";" . self::CRLF;
|
|
return $data . "\t" . "boundary=\"" . $this->boundary[$bkey] . "\"";
|
|
}
|
|
|
|
/**
|
|
* dual use method
|
|
* 1- with only $url passed, either a host or path (string) and returns the MX record domain name
|
|
* 2- with a fully qualified mail server passed, returns true/false if an MX record matches
|
|
* @param string $url
|
|
* @param string $validate
|
|
* @return string (mail server) (if $validate is 'no_test' and mail server found)
|
|
* @return bool (if no mail server found)
|
|
*/
|
|
private function GetMailServer($url = "", $validate = "no_test")
|
|
{
|
|
if (empty($url)) {
|
|
$url = $_SERVER['SERVER_NAME'];
|
|
}
|
|
$tld = substr($url, strpos($url, ".") + 1);
|
|
if ($validate === "is_valid") {
|
|
// Just check if the SMTP host is reachable via DNS (A record)
|
|
// The original check compared SMTP host to MX records which breaks
|
|
// third-party SMTP services like Mailgun, SendGrid, etc.
|
|
return checkdnsrr($url, 'A') || checkdnsrr($url, 'AAAA');
|
|
}
|
|
if (preg_match('/(?P<domain>[a-z0-9][a-z0-9\-]{1,63}\.[a-z\.]{2,6})$/i', $tld, $match)) {
|
|
getmxrr($match['domain'], $mx_details);
|
|
if (is_array($mx_details) && count($mx_details) > 0) {
|
|
return reset($mx_details);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Gets MIME type of file or string
|
|
* if file: USE ONLY AFTER VERIFYING FILE EXISTS
|
|
* if string: designed for file data read in as string, will not
|
|
* properly detect html vs text
|
|
* returns 'application/octet-stream' if not found (or file encoded)
|
|
* @param string $resource (filename or string)
|
|
* @param string $type ('string' or 'file', defaults to 'file')
|
|
* @return string
|
|
*/
|
|
private function GetMimeType($resource, $type = "file")
|
|
{
|
|
if ($type == "string") {
|
|
if (function_exists('finfo_buffer') && function_exists('finfo_open') && defined('FILEINFO_MIME_TYPE')) {
|
|
return finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $resource);
|
|
}
|
|
} else {
|
|
if (function_exists('finfo_file') && function_exists('finfo_open') && defined('FILEINFO_MIME_TYPE')) {
|
|
return finfo_file(finfo_open(FILEINFO_MIME_TYPE), $resource);
|
|
}
|
|
return mime_content_type($resource);
|
|
}
|
|
return "application/octet-stream";
|
|
}
|
|
|
|
/**
|
|
* Builds plain text and HTML portion of message
|
|
* @return string
|
|
*/
|
|
protected function GetMsgPart($bkey = "")
|
|
{
|
|
$data = $html = "";
|
|
$bkey = 0;
|
|
if (!empty($this->messageICal)) {
|
|
if (@is_file($this->messageICal)) {
|
|
self::IsExploitPath($this->messageICal, true);
|
|
$thisdir = (dirname($this->messageICal) != "") ? rtrim(dirname($this->messageICal), '/') . '/' : "";
|
|
$string = file_get_contents($this->messageICal);
|
|
$filename = basename($this->messageICal);
|
|
} else {
|
|
$string = $this->messageICal;
|
|
$filename = basename("calendar.ics");
|
|
}
|
|
self::AddStringAttachment($string, $filename, "base64", "text/calendar");
|
|
}
|
|
if (trim($this->messageHTML) != "") {
|
|
if (is_file($this->messageHTML)) {
|
|
self::IsExploitPath($this->messageHTML, true);
|
|
$thisdir = (dirname($this->messageHTML) != "") ? rtrim(dirname($this->messageHTML), '/') . '/' : "";
|
|
self::DataToHTML(file_get_contents($this->messageHTML), $thisdir);
|
|
}
|
|
self::GetMsgType();
|
|
if (
|
|
stripos($this->msgType, "attachment") !== false &&
|
|
stripos($this->msgType, "inline") !== false
|
|
) {
|
|
$bkey = 2;
|
|
} elseif (
|
|
stripos($this->msgType, "attachment") !== false ||
|
|
stripos($this->msgType, "inline") !== false
|
|
) {
|
|
$bkey = 1;
|
|
}
|
|
$html .= self::GetBoundary($bkey);
|
|
$html .= self::GetContentTypeBody("text/html", "charset=\"" . $this->charSet . "\"", "base64") . self::CRLF;
|
|
$html .= base64_encode($this->messageHTML) . self::CRLF . self::CRLF;
|
|
}
|
|
$data .= self::CRLF;
|
|
$data .= self::GetBoundary($bkey);
|
|
$data .= self::GetContentTypeBody("text/plain", "charset=\"" . $this->charSet . "\"", "7bit");
|
|
$data .= self::CRLF;
|
|
$data .= ((trim($this->messageText) != "") ? self::WrapText($this->messageText, $this->wordWrapLen) : "") . self::CRLF;
|
|
$data .= self::CRLF;
|
|
$data .= self::CRLF;
|
|
$data .= $html;
|
|
return $data . self::GetBoundary($bkey, "--");
|
|
}
|
|
|
|
/**
|
|
* Gets email message type
|
|
* @return string
|
|
*/
|
|
protected function GetMsgType($type = "")
|
|
{
|
|
if (is_string($type) && !empty($type)) {
|
|
$type = explode("|", rtrim($type, "|") . "|");
|
|
} else {
|
|
$type = [];
|
|
}
|
|
if (!in_array("message", $type) && ($this->messageHTML != "" || $this->messageText != "")) {
|
|
$type[] = "message";
|
|
}
|
|
foreach ($this->attachments as $attachment) {
|
|
if ($attachment[6] === "inline" && !in_array("inline", $type)) {
|
|
$type[] = "inline";
|
|
} elseif ($attachment[6] === "attachment" && !in_array("attachment", $type)) {
|
|
$type[] = "attachment";
|
|
}
|
|
}
|
|
if (count($type) == 0) {
|
|
$type[] = "inline";
|
|
}
|
|
sort($type);
|
|
$this->msgType = implode("|", $type);
|
|
}
|
|
|
|
/**
|
|
* Check the attachments array for a duplicate image
|
|
* will not add if duplicate exists
|
|
* @param string $id (attachments[0])
|
|
* @param string $param value
|
|
* @return bool
|
|
*/
|
|
private function IsImageDuplicate($id, $param)
|
|
{
|
|
if (isset($this->attachments) && ($this->attachments) > 0) {
|
|
foreach ($this->attachments as $key => $val) {
|
|
if ($val[$id] === $param) {
|
|
return $key;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks string for multibyte characters
|
|
* @param $str string
|
|
* @return bool (true if multibyte)
|
|
*/
|
|
private function IsMultibyte($str)
|
|
{
|
|
return(mb_strlen($str) != strlen($str)) ? true : false;
|
|
}
|
|
|
|
/**
|
|
* Check if file path is safe (real, accessible).
|
|
* @param string $path Relative or absolute path to a file
|
|
* @return bool
|
|
*/
|
|
protected function IsPathSafe($path, $opt_exit = false)
|
|
{
|
|
// path decode (note %00 - null - removed in decode)
|
|
for ($i = 0; $i <= substr_count($path, "%"); $i++) {
|
|
$path = rawurldecode($path);
|
|
}
|
|
// convert all slashes to system default
|
|
$path = str_replace(["\\", "/"], DIRECTORY_SEPARATOR, $path);
|
|
if (!is_dir($path) && file_exists($path)) {
|
|
return true;
|
|
}
|
|
// check for other exploits
|
|
if (self::IsExploitPath($path)) {
|
|
return false;
|
|
}
|
|
// check for path traversal
|
|
if (strpos(realpath($path), $_SERVER['DOCUMENT_ROOT']) === false) {
|
|
if ($opt_exit) {
|
|
exit('Cannot execute<br>');
|
|
}
|
|
return false;
|
|
}
|
|
// check for valid path, path/file
|
|
if (is_file($path)) {
|
|
$path = str_replace(basename($path), "", $path);
|
|
}
|
|
$realPath = str_replace(rtrim($_SERVER['DOCUMENT_ROOT'] . dirname($_SERVER['PHP_SELF']), '/') . '/', "", realpath($path));
|
|
if (strpos($path, "/")) {
|
|
$realPath = rtrim($realPath, '/') . '/';
|
|
}
|
|
if (($path === false) || (strcmp(rtrim($path, '/'), rtrim($realPath, '/')) !== 0)) {
|
|
return false;
|
|
}
|
|
return(file_exists($path) && is_readable($path) && is_dir($path)) ? true : false;
|
|
}
|
|
|
|
/**
|
|
* Prevent attacks by disallowing unsafe shell characters.
|
|
* Modified version (Thanks to Paul Buonopane <paul@namepros.com>)
|
|
* Modification: CRITICAL STOP on NOT safe
|
|
* @param string $email (the string to be tested for shell safety)
|
|
* @return bool
|
|
*/
|
|
protected function IsShellSafe($email)
|
|
{
|
|
$safe = true;
|
|
if (empty(trim($email))) {
|
|
$safe = false;
|
|
}
|
|
if ($safe && function_exists('ctype_alnum')) {
|
|
for ($i = 0; $i < strlen($email); $i++) {
|
|
$chr = $email[$i];
|
|
if (!ctype_alnum($chr) && strpos('@_-.', $chr) === false) {
|
|
$safe = false;
|
|
}
|
|
}
|
|
}
|
|
if ($safe) {
|
|
$safe = (bool) preg_match('/\A[\pL\pN._@-]*\z/ui', $email);
|
|
}
|
|
if ($safe === false) {
|
|
exit("Cannot Process: Email Address failed shell safe validation<br>");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if path (file) exists and is readable
|
|
* @var string $path
|
|
* @return bool
|
|
*/
|
|
private function IsExploitPath($path, $optExit = false)
|
|
{
|
|
$isExploit = (is_file($path) && is_readable($path)) ? false : true;
|
|
if ($isExploit && $optExit !== false) {
|
|
echo 'path: ' . $path . '<br>';
|
|
exit("Unable to send, access not allowed");
|
|
}
|
|
return $isExploit;
|
|
}
|
|
|
|
/**
|
|
* Validate an email address, probably the most robust validator available
|
|
* @param string $email The email address to check
|
|
* @return boolean
|
|
*/
|
|
private function IsValidEmail($email)
|
|
{
|
|
$temp = explode("@", $email);
|
|
$domn = array_pop($temp);
|
|
$rz = true;
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
$rz = false;
|
|
}
|
|
if ($rz) {
|
|
// Check DNS records instead of port 80 (mail-only subdomains don't have web servers)
|
|
$rz = (checkdnsrr($domn, 'MX') || checkdnsrr($domn, 'A'));
|
|
}
|
|
if ($rz) {
|
|
$rz = (bool) preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD', $email);
|
|
}
|
|
return $rz;
|
|
}
|
|
|
|
/**
|
|
* Encodes and wraps long multibyte strings for mail headers
|
|
* without breaking lines within a character
|
|
* validates $str as multibyte
|
|
* @param string $str multi-byte string to encode
|
|
* @return string
|
|
*/
|
|
private function MbEncode($str, $len = 76)
|
|
{
|
|
$str = self::SafeStr($str);
|
|
if (mb_strlen($str) != strlen($str)) {
|
|
return $str;
|
|
}
|
|
if (function_exists('mb_internal_encoding') && function_exists('mb_encode_mimeheader')) {
|
|
mb_internal_encoding("UTF-8");
|
|
return mb_encode_mimeheader($str, "UTF-8");
|
|
} else {
|
|
$prefs = ["scheme" => "Q", "input-charset" => "utf-8", "output-charset" => "utf-8", "line-length" => $len];
|
|
return iconv_mime_encode($str, $prefs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filter data (ascii and url-encoded) to prevent header injection
|
|
* @param string $str String
|
|
* @return string (trimmed)
|
|
*/
|
|
private function SafeStr($str)
|
|
{
|
|
return trim(str_ireplace(["\r", "\n", "%0d", "%0a", "Content-Type:", "bcc:", "to:", "cc:"], "", $str));
|
|
}
|
|
|
|
/**
|
|
* Send the email
|
|
* @return bool
|
|
*/
|
|
public function Send($via = "smtp")
|
|
{
|
|
if ($via != "") {
|
|
$via = strtolower($via);
|
|
}
|
|
if (!in_array($via, $this->transports)) {
|
|
exit($via . " Transport Not Available");
|
|
}
|
|
$this->transport = $via;
|
|
if ($this->recipients == "" && $this->bcc != "") {
|
|
$this->recipients = "undisclosed-recipients:";
|
|
}
|
|
$ret = false;
|
|
if (empty($this->sender)) {
|
|
if ($this->senderEmail == "root@localhost") {
|
|
$this->senderEmail = "noreply@" . self::GetMailServer();
|
|
}
|
|
self::SetSender([$this->senderEmail => $this->senderName]);
|
|
}
|
|
if (trim($this->returnPath) == "") {
|
|
$this->returnPath = self::EmailExtractEmail($this->sender);
|
|
}
|
|
if (empty(trim($this->sender)) || empty(trim($this->recipients))) {
|
|
exit("✗ Critical error: missing Sender and/or recipients.<br>" . self::CRLF);
|
|
}
|
|
$body = self::BuildBody();
|
|
// order of Sending preference: SMTP, Sendmail, IMAP, PHP Mail()
|
|
if ($this->transport == "smtp") {
|
|
$hdr = self::BuildHeader("smtp");
|
|
$ret = self::TransportSMTP($hdr, $body);
|
|
$this->mimeMail = $hdr . self::CRLF . $body;
|
|
}
|
|
if (
|
|
$this->transport == "sendmail" ||
|
|
($ret === false && $this->transport == "smtp")
|
|
) {
|
|
$ret = self::TransportSendmail($body);
|
|
}
|
|
if (
|
|
$this->transport == "imap" ||
|
|
($ret === false && ($this->transport == "sendmail" || $this->transport == "smtp"))
|
|
) {
|
|
$hdr = self::BuildHeader("imap");
|
|
$ret = self::TransportIMAP($hdr, $body);
|
|
$this->mimeMail = $hdr . self::CRLF . $body;
|
|
}
|
|
if ($ret === true) {
|
|
self::AddMessageToSent("INBOX.Sent");
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* IMAP transport ONLY
|
|
* Security to ALL the data and email addresses
|
|
* must occur BEFORE calling this function
|
|
* @return bool
|
|
*/
|
|
protected function TransportIMAP($hdr, $body)
|
|
{
|
|
if (empty(trim($this->sender)) || empty(trim($this->recipients))) {
|
|
return false;
|
|
}
|
|
self::GetMsgType($this->msgType);
|
|
if ($this->mimeMail == "") {
|
|
$this->mimeMail = $hdr . self::CRLF . $body;
|
|
}
|
|
$to = $this->recipients;
|
|
$subject = self::MbEncode($this->subject);
|
|
$cc = (!empty($this->cc)) ? $this->cc : null;
|
|
$bcc = (!empty($this->bcc)) ? $this->bcc : null;
|
|
$rpath = $this->sender;
|
|
$ret = imap_mail($to, $subject, $body, $hdr, $cc, $bcc, $rpath);
|
|
return $ret;
|
|
}
|
|
|
|
/* Sendmail transport ONLY
|
|
* Security to ALL the data and email addresses
|
|
* must occur BEFORE calling this function
|
|
* @return bool
|
|
*/
|
|
protected function TransportSendmail($body)
|
|
{
|
|
$ret = false;
|
|
$this->sendmailPath = ini_get("sendmail_path");
|
|
if (!empty($this->sendmailPath)) {
|
|
$opt = [0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]];
|
|
$this->sendmailPath = str_replace([" -o "," -i "]," -oi ", $this->sendmailPath);
|
|
$command = $this->sendmailPath . " -f" . self::EmailExtractEmail($this->sender);
|
|
if (function_exists('proc_open')) {
|
|
$hdr = self::BuildHeader('proc');
|
|
$hndl = proc_open($command, $opt, $bk);
|
|
if (is_resource($hndl)) {
|
|
fwrite($bk[0], $hdr);
|
|
fwrite($bk[0], $body . self::CRLF);
|
|
fclose($bk[0]);
|
|
if (proc_close($hndl) != -1) {
|
|
$ret = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$this->mimeMail = $hdr . self::CRLF . $body;
|
|
return $ret;
|
|
}
|
|
|
|
/* SMTP transport ONLY
|
|
* Security to ALL the data and email addresses
|
|
* must occur BEFORE calling this function
|
|
* @return bool
|
|
*/
|
|
protected function TransportSMTP($hdr, $body)
|
|
{
|
|
if (!is_null($this->debug)) {
|
|
$this->smtpDebug = $this->debug;
|
|
}
|
|
if ($this->smtpDebug == 1) {
|
|
$this->smtpDebug = true;
|
|
}
|
|
if (empty($this->smtpDomain)) {
|
|
$this->smtpDomain = self::GetMailServer();
|
|
}
|
|
if (empty(trim($this->sender)) || empty(trim($this->recipients))) {
|
|
return false;
|
|
}
|
|
if (self::SMTPconnect() === false) {
|
|
return false;
|
|
}
|
|
if (self::SMTPenvelope() === false) {
|
|
return false;
|
|
}
|
|
if (self::SMTPdata($hdr, $body) === false) {
|
|
return false;
|
|
}
|
|
if (is_resource($this->smtpStream)) {
|
|
if ($this->smtpKeepAlive == true) {
|
|
self::SMTPreset();
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
/* */
|
|
|
|
/**
|
|
* Returns server hostname or 'localhost.localdomain' if unknown
|
|
* @return string
|
|
*/
|
|
private function ServerHostname()
|
|
{
|
|
if (!empty($this->hostname)) {
|
|
return $this->hostname;
|
|
} elseif (isset($_SERVER['SERVER_NAME'])) {
|
|
return $_SERVER['SERVER_NAME'];
|
|
}
|
|
return "localhost.localdomain";
|
|
}
|
|
|
|
/**
|
|
* Set plain text
|
|
* @param string $content
|
|
*/
|
|
public function SetBodyText($content)
|
|
{
|
|
$this->messageText = $content;
|
|
}
|
|
|
|
/**
|
|
* Set email address for confirm email is read
|
|
* @param string $email The email address
|
|
*/
|
|
public function SetConfirmRead($param)
|
|
{
|
|
$this->confirm_read = self::EmailFormatRFC($param);
|
|
}
|
|
|
|
/**
|
|
* Set email address for confirm email is received
|
|
* @param string $email The email address
|
|
*/
|
|
public function SetConfirmReceipt($param)
|
|
{
|
|
$this->confirm_rcpt = self::EmailFormatRFC($param);
|
|
}
|
|
|
|
/*
|
|
* Set priority
|
|
* @param integer $param - from 1 (highest) to 5 (lowest)
|
|
* @return bool
|
|
*/
|
|
public function SetPriority($param)
|
|
{
|
|
return(!intval($param)) ? false : $this->priority = intval($param);
|
|
}
|
|
|
|
/**
|
|
* Set replyTo
|
|
* @param string $email
|
|
* @return bool
|
|
*/
|
|
public function AddReplyTo($param)
|
|
{
|
|
$this->replyTo = self::EmailFormatRFC($param);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set sender
|
|
* @param mixed (string or array) $email/$name
|
|
*/
|
|
public function SetSender($param)
|
|
{
|
|
$tmpAddy = self::EmailFormatRFC($param);
|
|
$senderName = self::EmailExtractName($tmpAddy);
|
|
$senderEmail = self::IsShellSafe(self::EmailExtractEmail($tmpAddy));
|
|
$tmpAddy = self::EmailFormatRFC([$senderName => $senderEmail]);
|
|
$this->sender = $this->replyTo = $this->returnPath = self::EmailFormatRFC($param);
|
|
unset($tmpAddy);
|
|
|
|
|
|
|
|
$this->sender = self::EmailFormatRFC($param);
|
|
$this->smtpFrom = self::EmailExtractEmail($this->sender);
|
|
}
|
|
|
|
/**
|
|
* Set subject
|
|
* @param string $param The subject of the email
|
|
*/
|
|
public function SetSubject($param)
|
|
{
|
|
$this->subject = $param;
|
|
}
|
|
|
|
/**
|
|
* Uses SMTP transport by default, set to false to use Sendmail as default
|
|
* @param bool
|
|
*/
|
|
public function useIMAP($param = true)
|
|
{
|
|
$this->useIMAP = ($param === true) ? true : false;
|
|
}
|
|
|
|
/**
|
|
* Wraps message for use with mailers that do not automatically
|
|
* perform wrapping
|
|
* @param string $message The message to wrap
|
|
* @param integer $length The line length to wrap to
|
|
* @return string
|
|
*/
|
|
public function WrapText($message, $length)
|
|
{
|
|
$message = self::FixCRLF($message);
|
|
if (substr($message, -1) == self::CRLF) {
|
|
$message = substr($message, 0, -1);
|
|
}
|
|
$line = explode(self::CRLF, $message);
|
|
$message = "";
|
|
for ($i = 0; $i < count($line); $i++) {
|
|
$line_part = explode(" ", $line[$i]);
|
|
$buf = "";
|
|
for ($e = 0; $e < count($line_part); $e++) {
|
|
$word = $line_part[$e];
|
|
$buf_o = $buf;
|
|
$buf .= ($e == 0) ? $word : (" " . $word);
|
|
|
|
if (strlen($buf) > $length and $buf_o != "") {
|
|
$message .= $buf_o . "\n";
|
|
$buf = $word;
|
|
}
|
|
}
|
|
$message .= $buf . self::CRLF;
|
|
}
|
|
return $message;
|
|
}
|
|
|
|
/* END - METHODS ************/
|
|
|
|
/* SMTP METHODS ************/
|
|
|
|
/**
|
|
* Sets SMTP Account (Username and password)
|
|
* @return mixed
|
|
*/
|
|
public function SetSMTPaccount($array)
|
|
{
|
|
$pwd = trim(reset($array));
|
|
$uname = (is_numeric(key($array))) ? $pwd : trim(key($array));
|
|
$this->smtpUsername = $uname;
|
|
$this->smtpPassword = $pwd;
|
|
}
|
|
|
|
/**
|
|
* Set SMTP host
|
|
* @param string $param
|
|
*/
|
|
public function SetSMTPhost($param)
|
|
{
|
|
$this->smtpHost = self::SafeStr($param);
|
|
}
|
|
|
|
/**
|
|
* Set SMTP port
|
|
* @param integer $param
|
|
*/
|
|
public function SetSMTPport($param)
|
|
{
|
|
$this->smtpPort = self::SafeStr($param);
|
|
}
|
|
|
|
/**
|
|
* Set SMTP password
|
|
* @param string $param
|
|
*/
|
|
public function SetSMTPpass($param)
|
|
{
|
|
$this->smtpPassword = $param;
|
|
}
|
|
|
|
/**
|
|
* Set SMTP username
|
|
* @param string $param
|
|
*/
|
|
public function SetSMTPuser($param)
|
|
{
|
|
$this->smtpUsername = self::SafeStr($param);
|
|
}
|
|
|
|
/**
|
|
* Connect to the server
|
|
* return code: 220 success
|
|
* @return bool
|
|
*/
|
|
private function SMTPconnect()
|
|
{
|
|
// check if already connected
|
|
if ($this->smtpStream) {
|
|
return false;
|
|
}
|
|
// check for host
|
|
if (!empty($this->smtpHost)) {
|
|
if (self::GetMailServer($this->smtpHost, "is_valid") === false) {
|
|
exit(__LINE__ . " " . "✗ Critical error: invalid SMTP server.<br>" . self::CRLF);
|
|
}
|
|
$host_name = $this->smtpHost;
|
|
$server_arr = [$this->smtpHost];
|
|
} else {
|
|
$host_name = $this->smtpDomain;
|
|
$server_arr = [$this->smtpDomain];
|
|
}
|
|
// check for port
|
|
if (!empty($this->smtpPort)) {
|
|
$srv_ports = [$this->smtpPort];
|
|
} else {
|
|
$srv_ports = [587, 2525, 25];
|
|
}
|
|
// connect to the smtp server
|
|
$connect_options = $this->smtpOptions;
|
|
$create_options = (!empty($connect_options)) ? stream_context_create($connect_options) : null;
|
|
foreach ($server_arr as $host) {
|
|
if (empty($code) || $code != "220") {
|
|
foreach ($srv_ports as $port) {
|
|
if (function_exists('stream_socket_client')) {
|
|
$this->smtpStream = @stream_socket_client($host . ":" . $port, $errno, $errstr, self::TIMEVAL, STREAM_CLIENT_CONNECT, $create_options);
|
|
} else {
|
|
$this->smtpStream = @fsockopen($host, $port, $errno, $errstr, self::TIMEVAL);
|
|
}
|
|
if ($errno != "") {
|
|
$this->smtperr = $errno;
|
|
}
|
|
if ($errstr != "") {
|
|
$this->smtperrstr = $errstr;
|
|
}
|
|
if (!$this->smtpStream) {
|
|
return false;
|
|
}
|
|
$this->smtpHost = $host;
|
|
$this->smtpPort = $port;
|
|
$code = self::SMTPgetResponse(['220'], 'SMTP CONNECT');
|
|
if ($code == "220") {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if ($code == "220") {
|
|
stream_set_timeout($this->smtpStream, self::TIMEVAL);
|
|
return $this->smtpStream;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Sends header and message to SMTP Server
|
|
* return code: 250 success (possible 251, have to allow for this)
|
|
* @return bool
|
|
*/
|
|
private function SMTPdata($hdr, $body)
|
|
{
|
|
if (is_resource($this->smtpStream)) {
|
|
self::SMTPisStreamConnected();
|
|
// initiate DATA stream
|
|
fwrite($this->smtpStream, "DATA" . self::CRLF);
|
|
$code = self::SMTPgetResponse(['354'], 'DATA HEADER');
|
|
// send the header
|
|
$hdr_arr = self::StrSplitUnicode($hdr);
|
|
foreach ($hdr_arr as $line) {
|
|
fwrite($this->smtpStream, $line . self::CRLF);
|
|
}
|
|
// send the body
|
|
$body_arr = self::StrSplitUnicode($body);
|
|
foreach ($body_arr as $line) {
|
|
fwrite($this->smtpStream, $line . self::CRLF);
|
|
}
|
|
// end DATA stream
|
|
fwrite($this->smtpStream, "." . self::CRLF);
|
|
$code = self::SMTPgetResponse(['250'], 'DATA');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Send envelope to SMTP Server
|
|
* @return bool
|
|
*/
|
|
private function SMTPenvelope()
|
|
{
|
|
if (is_resource($this->smtpStream)) {
|
|
// send EHLO command
|
|
fwrite($this->smtpStream, "EHLO " . $this->smtpHost . self::CRLF);
|
|
$code = self::SMTPgetResponse(['250'], 'EHLO');
|
|
self::SMTPisStreamConnected();
|
|
// send STARTTLS command
|
|
fwrite($this->smtpStream, "STARTTLS" . self::CRLF);
|
|
$code = self::SMTPgetResponse(['220'], 'STARTTLS');
|
|
// initiate secure tls encryption
|
|
$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
|
|
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
|
|
$crypto_method = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
|
|
} elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT')) {
|
|
$crypto_method = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
|
|
} elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT')) {
|
|
$crypto_method = STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT;
|
|
}
|
|
stream_socket_enable_crypto($this->smtpStream, true, $crypto_method);
|
|
// resend EHLO after tls negotiation
|
|
fwrite($this->smtpStream, "EHLO " . $this->smtpHost . self::CRLF);
|
|
$code = self::SMTPgetResponse(['250'], 'EHLO');
|
|
self::SMTPisStreamConnected();
|
|
if (!empty($this->smtpUsername) && !empty($this->smtpUsername)) {
|
|
// Authenticate
|
|
fwrite($this->smtpStream, "AUTH LOGIN" . self::CRLF);
|
|
$code = self::SMTPgetResponse(['334'], 'AUTH LOGIN');
|
|
// Send encoded username
|
|
fwrite($this->smtpStream, base64_encode($this->smtpUsername) . self::CRLF);
|
|
$code = self::SMTPgetResponse(['334'], 'USER');
|
|
// Send encoded password
|
|
fputs($this->smtpStream, base64_encode($this->smtpPassword) . self::CRLF);
|
|
$code = self::SMTPgetResponse(['235'], 'PASSWORD');
|
|
}
|
|
self::SMTPisStreamConnected();
|
|
fwrite($this->smtpStream, "MAIL FROM: <" . $this->smtpFrom . ">" . (($this->smtpUseVerp) ? "XVERP" : "") . self::CRLF);
|
|
$code = self::SMTPgetResponse(['250'], 'MAIL FROM');
|
|
self::SMTPisStreamConnected();
|
|
if (strpos($this->recipients_rt, ",") !== false) {
|
|
$emails = explode(",", $this->recipients_rt);
|
|
foreach ($emails as $email) {
|
|
fwrite($this->smtpStream, "RCPT TO: <" . trim($email) . '>' . self::CRLF);
|
|
$code = self::SMTPgetResponse(['250', '251'], 'RCPT TO');
|
|
}
|
|
} else {
|
|
fwrite($this->smtpStream, "RCPT TO: <" . trim($this->recipients_rt) . '>' . self::CRLF);
|
|
$code = self::SMTPgetResponse(['250', '251'], 'RCPT TO');
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get response code returned by SMTP server
|
|
* @return string $code
|
|
*/
|
|
private function SMTPgetResponse($expected, $desc = "")
|
|
{
|
|
$total = "";
|
|
if ($desc == "SMTP CONNECT" && @$errstr != "") {
|
|
$data = $this->smtperrstr;
|
|
$code = $this->smtperr;
|
|
} elseif ($desc == "CLOSE CONNECTION") {
|
|
$data = "Connection closed (true)";
|
|
$code = $this->smtpStream;
|
|
} else {
|
|
$data = fread($this->smtpStream, 8192);
|
|
$code = substr($data, 0, 3);
|
|
}
|
|
if ($this->smtpDebug === true) {
|
|
$data = str_ireplace(["\r\n", "\r", "\n"], "\n", $data);
|
|
$data = str_replace([$code . "-", $code . " "], "", $data);
|
|
$data = trim(str_replace(" ", " ", $data));
|
|
$data = str_ireplace("\n", "\n" . str_repeat(" ", 4), $data);
|
|
$data = trim(str_ireplace("\n", "<br>", $data));
|
|
}
|
|
|
|
if ($desc == "SMTP CONNECT" && $code == "220") {
|
|
if ($this->smtpDebug === true) {
|
|
echo self::ThrowResponse("TRUE", "", "Server: " . $this->smtpHost . ", Port: " . $this->smtpPort . "<br>", true);
|
|
}
|
|
}
|
|
|
|
if (in_array($code, $expected) && $this->smtpDebug === true) {
|
|
echo self::ThrowResponse($code, $desc, $data, true);
|
|
} elseif (!in_array($code, $expected)) {
|
|
echo self::ThrowResponse($code, $desc, $data, false);
|
|
}
|
|
if ($this->smtpDebug === true) {
|
|
if (trim($this->timeStart) != "") {
|
|
if ($desc == "CLOSE CONNECTION") {
|
|
$total = "Total ";
|
|
}
|
|
echo "<br><i>" . str_repeat(" ", 4) . $total . "Elapsed time: " . number_format(microtime(true) - $this->timeStart, 3) . " sec.</i><br>";
|
|
}
|
|
}
|
|
return $code;
|
|
}
|
|
|
|
/**
|
|
* Returns true if connected to a server otherwise false
|
|
* @access public
|
|
* @return bool
|
|
*/
|
|
private function SMTPisStreamConnected($error_msg = "Not connected to SMTP server, aborting.")
|
|
{
|
|
if (!empty($this->smtpStream)) {
|
|
$status = socket_get_status($this->smtpStream);
|
|
if ($status["eof"]) {
|
|
fclose($this->smtpStream);
|
|
$this->smtpStream = 0;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
self::ThrowResponse("0", "CRITICAL ERROR", $error_msg, false);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Sends QUIT to SMTP Server then closes the stream
|
|
* return code: 221 success
|
|
* @return bool
|
|
*/
|
|
private function SMTPquit()
|
|
{
|
|
self::SMTPisStreamConnected();
|
|
fwrite($this->smtpStream, "QUIT" . self::CRLF);
|
|
$code = self::SMTPgetResponse(['221'], 'QUIT');
|
|
// close the connection and reset the stream value
|
|
if (!empty($this->smtpStream)) {
|
|
fclose($this->smtpStream);
|
|
$code = self::SMTPgetResponse(['1'], 'CLOSE CONNECTION');
|
|
$this->smtpStream = 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Sends smtp command RCPT TO
|
|
* Returns true if recipient (email) accepted (false if not)
|
|
* return code: 250 success (possible 251, have to allow for this)
|
|
* @return bool
|
|
*/
|
|
public function SMTPrecipient($param)
|
|
{
|
|
$addresses = [];
|
|
if (is_string($param) && strpos($param, ',') !== false) {
|
|
$addresses = explode(',', $param);
|
|
} elseif (is_string($param)) {
|
|
$addresses[] = $param;
|
|
}
|
|
$emails = [];
|
|
foreach ($addresses as $key => $val) {
|
|
$emails[] = self::EmailExtractEmail($val);
|
|
}
|
|
foreach ($emails as $email) {
|
|
fwrite($this->smtpStream, "RCPT TO: <" . $email . ">" . self::CRLF);
|
|
$code = self::SMTPgetResponse(['250', '251'], 'RCPT TO');
|
|
}
|
|
}
|
|
|
|
/* Send RSET (aborts any transport in progress and keeps connection alive)
|
|
* Implements RFC 821: RSET <CRLF>
|
|
* return code 250 success
|
|
* @return bool
|
|
*/
|
|
private function SMTPreset()
|
|
{
|
|
self::SMTPisStreamConnected("Called SMTP_KeepAlive without connection.");
|
|
fwrite($this->smtpStream, "RSET" . self::CRLF);
|
|
$code = self::SMTPgetResponse(['250'], 'RSET');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* splits a string into an array of max length $l
|
|
* @return array
|
|
*/
|
|
private function StrSplitUnicode($str, $length = 998)
|
|
{
|
|
if ($length > 0) {
|
|
$ret = [];
|
|
$len = mb_strlen($str, "UTF-8");
|
|
for ($i = 0; $i < $len; $i += $length) {
|
|
$ret[] = mb_substr($str, $i, $length, "UTF-8");
|
|
}
|
|
return $ret;
|
|
}
|
|
return preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY);
|
|
}
|
|
|
|
public function ThrowResponse($code, $desc, $data, $success = true)
|
|
{
|
|
$msg_lang = "<span style=\"%sfont-weight:700;\">%s %s (%s)</span><br> " . trim("%s");
|
|
$icn = ($success === true) ? "✓" : "✗";
|
|
$clr = ($success === true) ? "" : "color:red;";
|
|
$typ = ($success === true) ? "Success" : "Error";
|
|
if (trim($desc) != "") {
|
|
$desc = " " . $desc;
|
|
}
|
|
$msg = sprintf($msg_lang, $clr, $icn, $typ, $code . $desc, $data);
|
|
if ($success !== true && $this->smtpDebug === true) {
|
|
throw new Exception($msg . $data . "<br></span>");
|
|
return;
|
|
}
|
|
return $msg . self::CRLF;
|
|
}
|
|
/* END - SMTP METHODS ************/
|
|
}
|
|
/* PHPMailer Lite part of PHP Exception error handling
|
|
* (note, namespace makes Exception unique)
|
|
*/
|
|
class Exception extends \Exception
|
|
{
|
|
public function errorMessage()
|
|
{
|
|
$msg = str_ireplace(["<br>", "<br/>", "<br />"], "\n", $this->getMessage());
|
|
$errorMsg = "<style>.bmh-alert {width:600px;max-width:600px;border-radius:5px;border-style:solid;border-width:1px;font-family:sans-serif;font-size:22px;font-weight:bold;margin:40px 20px;padding:12px 16px;width:80%;}.bmh-alert.bmh-danger {background-color:rgba(248, 215, 218, 1);border-color:rgba(220, 53, 69, 1);color:rgba(114, 28, 36,1);}.bmh-alert.bmh-info {background-color:rgba(217, 237, 247, 1);color:rgba(49, 112, 143, 1);border-color:rgba(126, 182, 193, 1);}</style>";
|
|
$errorMsg .= "<div class=\"bmh-alert bmh-danger\" role=\"alert\">";
|
|
$errorMsg .= htmlentities($msg);
|
|
$errorMsg .= "</div>\n";
|
|
return $errorMsg;
|
|
}
|
|
public function errorMessageRaw()
|
|
{
|
|
$msg = $this->getMessage();
|
|
if ($this->getCode() == PHPMailerLite::ERR_CRITICAL) {
|
|
return $msg;
|
|
exit();
|
|
}
|
|
return $msg;
|
|
}
|
|
}
|