';
/**
* Class SendMail
* @package qfq
*/
class SendMail {
/**
* @var Store
*/
private $store = null;
/**
* Sends a mail as specified in $mailarr.
* If there is no receiver specified as 'TO': no mail is sent. This is ok and no error.
* Logs every send mail as a record in table `MailLog`. Additionally a `grId`, `xId` , `xId2` and `xId3` can be specified
* to assign the log entry to a specific action.
* The log record also contains some information who/where generates the mail (form/formelement or QFQ query).
*
* @throws CodeException
* @throws UserFormException
* @throws UserReportException
*/
public function __construct() {
$this->store = Store::getInstance('');
}
/**
* @param array $mailConfig
* @throws CodeException
* @throws DbException
* @throws DownloadException
* @throws UserFormException
* @throws UserReportException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
*/
public function process(array $mailConfig) {
// If there is no 'Receiver': do not send a mail.
if (!isset($mailConfig[SENDMAIL_TOKEN_RECEIVER]) || $mailConfig[SENDMAIL_TOKEN_RECEIVER] === '') {
return;
}
if ($mailConfig[SENDMAIL_TOKEN_SENDER] === '' || $mailConfig[SENDMAIL_TOKEN_SUBJECT] === '' || $mailConfig[SENDMAIL_TOKEN_BODY] === '') {
throw new UserFormException("Error sendmail missing one of: sender, subject or body", ERROR_SENDMAIL_MISSING_VALUE);
}
$redirectAllMail = $this->store->getVar(SYSTEM_REDIRECT_ALL_MAIL_TO, STORE_SYSTEM);
if (!empty($redirectAllMail)) {
$addBody = "All QFQ outgoing mails are caught and redirected to you." . PHP_EOL . "Original receiver are ..." . PHP_EOL;
$addBody .= 'TO: ' . $mailConfig[SENDMAIL_TOKEN_RECEIVER] . PHP_EOL;
$addBody .= 'CC: ' . $mailConfig[SENDMAIL_TOKEN_RECEIVER_CC] . PHP_EOL;
$addBody .= 'BCC: ' . $mailConfig[SENDMAIL_TOKEN_RECEIVER_BCC] . PHP_EOL;
$addBody .= 'SENDER: ' . $mailConfig[SENDMAIL_TOKEN_SENDER] . PHP_EOL;
$addBody .= PHP_EOL . "==========================================" . PHP_EOL . PHP_EOL;
// Check if the given body is a HTML body.
if (isset($mailConfig[SENDMAIL_TOKEN_BODY_MODE]) && $mailConfig[SENDMAIL_TOKEN_BODY_MODE] === SENDMAIL_TOKEN_BODY_MODE_HTML) {
$addBody = str_replace(PHP_EOL, '
', $addBody);
}
$mailConfig[SENDMAIL_TOKEN_BODY] = $addBody . $mailConfig[SENDMAIL_TOKEN_BODY];
$mailConfig[SENDMAIL_TOKEN_RECEIVER] = $redirectAllMail;
$mailConfig[SENDMAIL_TOKEN_RECEIVER_CC] = '';
$mailConfig[SENDMAIL_TOKEN_RECEIVER_BCC] = '';
$mailConfig[SENDMAIL_TOKEN_SENDER] = $redirectAllMail;
}
$mailConfig = $this->setDefault($mailConfig);
$logAttachments = $this->sendEmail($mailConfig);
$this->mailLog($mailConfig, $logAttachments);
}
/**
* @param array $mailConfig
* @return array
*/
private function setDefault(array $mailConfig) {
if (empty($mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT]) || $mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT] === '') {
$mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT] = 'on';
}
// Subject
if (empty($mailConfig[SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY]) || $mailConfig[SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY] !== MODE_ENCODE) {
$mailConfig[SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY] = MODE_DECODE;
}
// Body
if (empty($mailConfig[SENDMAIL_TOKEN_BODY_HTML_ENTITY]) || $mailConfig[SENDMAIL_TOKEN_BODY_HTML_ENTITY] !== MODE_ENCODE) {
$mailConfig[SENDMAIL_TOKEN_BODY_HTML_ENTITY] = MODE_DECODE;
}
return $mailConfig;
}
/**
* Use the programm 'sendEmail' - http://caspian.dotconf.net/menu/Software/SendEmail
* Body and Subject is UTF8 encoded. Append attachments with '-a '.
*
* @param array $mailConfig
* @return string
* @throws CodeException
* @throws DbException
* @throws DownloadException
* @throws UserFormException
* @throws UserReportException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
*/
private function sendEmail(array $mailConfig) {
$args = array();
$attachments = array();
$cmdAttachments = '';
$mailConfig[SENDMAIL_TOKEN_SUBJECT] = Support::htmlEntityEncodeDecode($mailConfig[SENDMAIL_TOKEN_SUBJECT_HTML_ENTITY], $mailConfig[SENDMAIL_TOKEN_SUBJECT]);
$mailConfig[SENDMAIL_TOKEN_BODY] = Support::htmlEntityEncodeDecode($mailConfig[SENDMAIL_TOKEN_BODY_HTML_ENTITY], $mailConfig[SENDMAIL_TOKEN_BODY]);
if (isset($mailConfig[SENDMAIL_TOKEN_BODY_MODE]) && $mailConfig[SENDMAIL_TOKEN_BODY_MODE] === SENDMAIL_TOKEN_BODY_MODE_HTML) {
$mailConfig[SENDMAIL_TOKEN_BODY] = Support::wrapTag(SENDMAIL_HTML_TOKEN, $mailConfig[SENDMAIL_TOKEN_BODY]);
}
foreach ($mailConfig as $key => $value) {
if (is_array($value)) {
continue;
}
if ($key != SENDMAIL_TOKEN_SUBJECT) { // do not escape double ticks in subject - this breaks the UTF8 encoding later
$mailConfig[$key] = Support::escapeDoubleTick($value);
}
}
$args[] = '-f "' . $mailConfig[SENDMAIL_TOKEN_SENDER] . '"';
$args[] = '-t "' . $mailConfig[SENDMAIL_TOKEN_RECEIVER] . '"';
$args[] = '-o message-charset="utf-8"';
$logFile = $this->store->getVar(SYSTEM_MAIL_LOG, STORE_SYSTEM);
if ($logFile != '' && $logFile !== false) {
$args[] = '-l "' . $logFile . '"';;
}
if (!empty($mailConfig[SENDMAIL_TOKEN_RECEIVER_CC])) {
$args[] = '-cc "' . $mailConfig[SENDMAIL_TOKEN_RECEIVER_CC] . '"';
}
if (!empty($mailConfig[SENDMAIL_TOKEN_RECEIVER_BCC])) {
$args[] = '-bcc "' . $mailConfig[SENDMAIL_TOKEN_RECEIVER_BCC] . '"';
}
if (!empty($mailConfig[SENDMAIL_TOKEN_SUBJECT])) {
// The subject needs to be encoded to UTF-8 separately - https://stackoverflow.com/questions/4389676/email-from-php-has-broken-subject-header-encoding/27648245#27648245
$preferences = ["scheme" => "Q", "input-charset" => "UTF-8", "output-charset" => "UTF-8"];
$encodedSubject = iconv_mime_encode("Subject", $mailConfig[SENDMAIL_TOKEN_SUBJECT], $preferences);
$encodedSubject = substr($encodedSubject, 9); // remove 'Subject: '
$encodedSubject = Support::escapeDoubleTick($encodedSubject); // now, escape double ticks.
$args[] = '-u "' . $encodedSubject . '"';;
}
if (!empty($mailConfig[SENDMAIL_TOKEN_BODY])) {
$args[] = '-m "' . $mailConfig[SENDMAIL_TOKEN_BODY] . '"';;
}
if (!empty($mailConfig[SENDMAIL_TOKEN_REPLY_TO])) {
$args[] = '-o reply-to="' . $mailConfig[SENDMAIL_TOKEN_REPLY_TO] . '"';;
}
if ($mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT] === 'on') {
$args[] = '-o message-header="Auto-Submitted: auto-send"';
}
if (!empty($mailConfig[SENDMAIL_TOKEN_ATTACHMENT])) {
$attachments = $this->attachmentsBuild($mailConfig[SENDMAIL_TOKEN_ATTACHMENT]);
if (!empty($attachments)) {
$cmdAttachments = '-a ' . implode(' -a ', $attachments);
$args[] = $cmdAttachments;
}
}
if (!empty($mailConfig[SENDMAIL_TOKEN_HEADER])) {
$args[] = '-o message-header="' . $mailConfig[SENDMAIL_TOKEN_HEADER] . '"';
}
$sendEmail = $this->store->getVar(SYSTEM_SEND_E_MAIL, STORE_SYSTEM);
if (empty($sendEmail)) {
throw new UserFormException("Missing 'sendEmail'", ERROR_SENDMAIL);
}
$sendEmailOptions = $this->store->getVar(SYSTEM_SEND_E_MAIL_OPTIONS, STORE_SYSTEM);
if (!empty($sendEmailOptions)) {
$args[] = $sendEmailOptions;
}
OnArray::arrayEscapeshellarg($args);
$cmd = $sendEmail . ' ' . implode(' ', $args);
$cmd = str_replace('`', '\`', $cmd); // escape backticks (escapeshellcmd would be too thorough)
exec($cmd, $arr, $rc);
if ($rc != 0) {
// After first installation of QFQ extension, the PERL script is not executable: is this the problem here?
$perms = fileperms($sendEmail);
if (!($perms & 0x0040)) {
chmod($sendEmail, 0755);
exec($cmd, $arr, $rc); // Give it a second try.
}
if ($rc != 0) {
$output = $rc . " - " . implode('
', $arr) . " - " . $cmd;
throw new UserFormException("Error sendmail failed: " . $output, ERROR_SENDMAIL);
}
}
HelperFile::cleanTempFiles($attachments);
return $cmdAttachments;
}
/**
* Creates a new MailLog Record based on $mailArr / $header.
*
* @param array $mailConfig
*
* @param string $attachmentsLine
* @throws CodeException
* @throws DbException
* @throws UserFormException
* @throws UserReportException
*/
private function mailLog(array $mailConfig, $attachmentsLine = '') {
$log = array();
$header = 'OoO:' . $mailConfig[SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT];
if (!empty($mailConfig[SENDMAIL_TOKEN_HEADER])) {
$header .= PHP_EOL . 'Custom: ' . $mailConfig[SENDMAIL_TOKEN_HEADER];
}
if (!empty($mailConfig[SENDMAIL_TOKEN_ATTACHMENT])) {
$header .= PHP_EOL . 'Attachment: ' . $mailConfig[SENDMAIL_TOKEN_ATTACHMENT];
}
// Log
$log[] = $mailConfig[SENDMAIL_TOKEN_RECEIVER];
$log[] = $mailConfig[SENDMAIL_TOKEN_SENDER];
$log[] = $mailConfig[SENDMAIL_TOKEN_SUBJECT];
$log[] = $mailConfig[SENDMAIL_TOKEN_BODY];
$log[] = $header;
$log[] = $attachmentsLine;
$log[] = empty($mailConfig[SENDMAIL_TOKEN_GR_ID]) ? 0 : $mailConfig[SENDMAIL_TOKEN_GR_ID];
$log[] = empty($mailConfig[SENDMAIL_TOKEN_X_ID]) ? 0 : $mailConfig[SENDMAIL_TOKEN_X_ID];
$log[] = empty($mailConfig[SENDMAIL_TOKEN_X_ID2]) ? 0 : $mailConfig[SENDMAIL_TOKEN_X_ID2];
$log[] = empty($mailConfig[SENDMAIL_TOKEN_X_ID3]) ? 0 : $mailConfig[SENDMAIL_TOKEN_X_ID3];
$log[] = empty($mailConfig[SENDMAIL_TOKEN_SRC]) ? 0 : $mailConfig[SENDMAIL_TOKEN_SRC];
$db = new Database();
$db->sql('INSERT INTO MailLog (`receiver`, `sender`, `subject`, `body`, `header`, `attach`, `grId`, `xId`, `xId2`, `xId3`, `src`, `modified`, `created`) VALUES ( ?, ? , ? , ? ,?, ?, ? ,?, ?, ?, ?, NOW(), NOW() )', ROW_REGULAR, $log);
}
/**
*
* @param array $attachments Array of attachments. Per attachment, different & multiple sources are possible.
* [ [ 0 -> 'F:/etc/hostname' ],
* [ 0 -> 'u:http://nzz.ch', 1 -> 'd:nzz.pdf' ],
* [ 0 -> 'p:id=detailPerson&form=Person&r=1&_sip=1', 1 -> 'F:/etc/hostname' ]
* [ 0 -> 'd:all.pdf', 1 -> 'U:?id=detailPerson&form=Person&r=1&_sip=1', 2 -> 'F:/etc/hostname' ] ]
* @return array Array of filenames. Those files has to be deleted later, if they are temporary files.
* @throws CodeException
* @throws DbException
* @throws DownloadException
* @throws UserFormException
* @throws UserReportException
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
*/
private function attachmentsBuild(array $attachments) {
$files = array();
$download = new Download();
$tmpDir = sys_get_temp_dir();
// Several attachments are possible. Process one by one.
foreach ($attachments as $attach) {
$vars = array();
// Extract Filename
$exportFilename = '';
$downloadMode = '';
// Per attachment: extract 'mode','filename'. Leave the 'sources' untouched.
foreach ($attach as $key => $element) {
$token = $element[0];
switch ($token) {
case SENDMAIL_TOKEN_DOWNLOAD_FILENAME:
$exportFilename = substr($element, 2);
unset($attach[$key]);
break;
case SENDMAIL_TOKEN_DOWNLOAD_MODE:
$downloadMode = substr($element, 2);
unset($attach[$key]);
break;
case SENDMAIL_TOKEN_ATTACHMENT_FILE:
if ($downloadMode == '') { // Set only if not explicit given.
$downloadMode = DOWNLOAD_MODE_FILE;
}
break;
case SENDMAIL_TOKEN_ATTACHMENT_URL:
case SENDMAIL_TOKEN_ATTACHMENT_URL_PARAM:
case SENDMAIL_TOKEN_ATTACHMENT_PAGE:
if ($downloadMode == '') { // Set only if not explicit given.
$downloadMode = DOWNLOAD_MODE_PDF;
}
break;
default:
throw new UserReportException('Unknown token in _sendmail: ' . $token, ERROR_UNKNOWN_TOKEN);
break;
}
}
$vars[SIP_DOWNLOAD_PARAMETER] = implode(PARAM_DELIMITER, $attach);
$vars[DOWNLOAD_MODE] = (count($attach) > 1) ? DOWNLOAD_MODE_PDF : $downloadMode;
$file = $download->process($vars, OUTPUT_MODE_FILE);
if (empty($exportFilename) && HelperFile::isQfqTemp($file)) {
$exportFilename = DOWNLOAD_OUTPUT_FILENAME;
if ($downloadMode == DOWNLOAD_MODE_PDF) {
$exportFilename .= '.pdf';
}
}
// In case an exportFilename is given: move/rename it, if it is necessary.
if (!empty($exportFilename)) {
$exportFilename = Sanitize::safeFilename($exportFilename, true);
$dir = HelperFile::mktempdir(); // Attachments might have the same filename - create one directory per attachment, to ensure same filenames do not conflict.
$exportFilename = $dir . '/' . $exportFilename;
if (HelperFile::isQfqTemp($file)) {
rename($file, $exportFilename);
} else {
copy($file, $exportFilename);
}
$file = $exportFilename;
}
$files[] = $file;
}
return $files;
}
/**
* Convert a token based sendMail string into an array.
* - Each attachment (single file or multiple concatenated files) is an array in the array.
*
* @param string $data E.g.: 't:john@doe.com|f:jane@miller.com|s:Latest|b:Dear John ...'
* @return array
* @throws UserFormException
*/
public function parseStringToArray($data) {
$args = array();
$attachment = array();
$flagConcat = false;
$flagSource = false;
$flagFilename = false;
$convertToShorthandToken = [
SENDMAIL_TOKEN_RECEIVER_LONG => SENDMAIL_TOKEN_RECEIVER,
SENDMAIL_TOKEN_SENDER_LONG => SENDMAIL_TOKEN_SENDER,
SENDMAIL_TOKEN_SUBJECT_LONG => SENDMAIL_TOKEN_SUBJECT,
SENDMAIL_TOKEN_BODY_LONG => SENDMAIL_TOKEN_BODY,
SENDMAIL_TOKEN_RECEIVER_CC_LONG => SENDMAIL_TOKEN_RECEIVER_CC,
SENDMAIL_TOKEN_RECEIVER_BCC_LONG => SENDMAIL_TOKEN_RECEIVER_BCC,
SENDMAIL_TOKEN_REPLY_TO_LONG => SENDMAIL_TOKEN_REPLY_TO,
SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT_LONG => SENDMAIL_TOKEN_FLAG_AUTO_SUBMIT,
SENDMAIL_TOKEN_GR_ID_LONG => SENDMAIL_TOKEN_GR_ID,
SENDMAIL_TOKEN_X_ID_LONG => SENDMAIL_TOKEN_X_ID,
SENDMAIL_TOKEN_X_ID2_LONG => SENDMAIL_TOKEN_X_ID2,
SENDMAIL_TOKEN_X_ID3_LONG => SENDMAIL_TOKEN_X_ID3,
SENDMAIL_TOKEN_HEADER_LONG => SENDMAIL_TOKEN_HEADER,
SENDMAIL_TOKEN_SRC_LONG => SENDMAIL_TOKEN_SRC,
SENDMAIL_TOKEN_BODY_MODE_LONG => SENDMAIL_TOKEN_BODY_MODE,
];
$param = explode(PARAM_DELIMITER, $data);
// Iterate over all parameter: use token as key. Collect corresponding attachments arguments in separate array elements.
foreach ($param AS $line) {
if (empty($line)) {
continue;
}
$tokenAndValue = explode(PARAM_TOKEN_DELIMITER, $line, 2);
if (count($tokenAndValue) < 2 && $tokenAndValue[0] !== SENDMAIL_TOKEN_CONCAT) {
throw new UserFormException('Missing token delimiter "' . PARAM_TOKEN_DELIMITER . '" in: ' . $line, ERROR_UNKNOWN_TOKEN);
}
$token = $tokenAndValue[0];
// convert speaking word tokens to shorthand
if (strlen($token) > 1) {
$token = strtolower($token); // speaking word tokens are all lowercase
if (isset($convertToShorthandToken[$token])) {
$token = $convertToShorthandToken[$token];
} else {
throw new UserFormException('Unknown token "' . $token . '" in: ' . $line, ERROR_UNKNOWN_TOKEN);
}
}
// Check for deprecated token.
if ($token == SENDMAIL_TOKEN_ATTACHMENT_FILE_DEPRECATED) {
throw new UserFormException('Sendmail: Option "a:" is deprecated, please use "' . SENDMAIL_TOKEN_ATTACHMENT_FILE . '" instead', ERROR_UNKNOWN_TOKEN);
}
switch ($token) {
case SENDMAIL_TOKEN_CONCAT:
$flagConcat = true;
if (!empty($attachment)) {
$args[SENDMAIL_TOKEN_ATTACHMENT][] = $attachment;
$attachment = array();
}
break;
case SENDMAIL_TOKEN_ATTACHMENT_FILE:
case SENDMAIL_TOKEN_ATTACHMENT_URL:
case SENDMAIL_TOKEN_ATTACHMENT_URL_PARAM:
case SENDMAIL_TOKEN_ATTACHMENT_PAGE:
if ($flagSource && !$flagConcat) {
$args[SENDMAIL_TOKEN_ATTACHMENT][] = $attachment;
$attachment = array();
$flagFilename = false;
}
$flagSource = true;
$attachment[] = $line;
break;
case SENDMAIL_TOKEN_DOWNLOAD_FILENAME:
if ($flagFilename && !$flagConcat) {
$args[SENDMAIL_TOKEN_ATTACHMENT][] = $attachment;
$attachment = array();
$flagSource = false;
}
$flagFilename = true;
$attachment[] = $line;
break;
default:
$args[$token] = $tokenAndValue[1];
break;
}
}
if (!empty($attachment)) {
$args[SENDMAIL_TOKEN_ATTACHMENT][] = $attachment;
}
return ($args);
}
}