'; /** * 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(s) 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_NONE)) { $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_NONE)) { $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) $output = Support::qfqExec($cmd, $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. $output = Support::qfqExec($cmd, $rc); } if ($rc != 0) { throw new UserFormException(json_encode([ERROR_MESSAGE_TO_USER => '"Error sendmail failed', ERROR_MESSAGE_SUPPORT => "[cmd=$cmd]$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_RECEIVER_CC] ?? ''; $log[] = $mailConfig[SENDMAIL_TOKEN_RECEIVER_BCC] ?? ''; $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`, `cc`, `bcc`, `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 explicitly 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 explicitly 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); } }