_layout = $layout; } return $this; } /** * Sets wrap length. * * @param int $length Must not be more than CakeEmail::LINE_LENGTH_MUST * @return $this */ public function setWrapLength($length) { $this->_wrapLength = $length; return $this; } /** * Gets wrap length. * * @return int */ public function getWrapLength() { return $this->_wrapLength; } /** * Set/Get wrapLength * * @deprecated Use setWrapLenght()/getWrapLength() instead. * * @param int|null $length Must not be more than CakeEmail::LINE_LENGTH_MUST * @return int|$this */ public function wrapLength($length = null) { if ($length === null) { return $this->getWrapLength(); } return $this->setWrapLength($length); } /** * Set/Get priority * * @deprecated Since CakePHP 3.4.0-RC4 in core as setPriority()/getPriority() * * @param int|null $priority 1 (highest) to 5 (lowest) * @return int|$this */ public function priority($priority = null) { if ($priority === null) { return $this->_priority; } $this->_priority = $priority; return $this; } /** * Fix line length * * @override To wrap by must length by default. * * @param string $message Message to wrap * @param int $wrapLength * @return array Wrapped message */ protected function _wrap($message, $wrapLength = CakeEmail::LINE_LENGTH_MUST) { if ($this->_wrapLength !== null) { $wrapLength = $this->_wrapLength; } return parent::_wrap($message, $wrapLength); } /** * EmailLib::resetAndSet() * * @return void */ public function reset() { parent::reset(); $this->_priority = null; $this->_wrapLength = null; $this->_error = null; $this->_debug = null; } /** * Ovewrite to allow custom enhancements * * @param array|string $config * @return $this */ public function setProfile($config) { if (!is_array($config)) { $config = (string)$config; } $this->_applyConfig($config); $fromEmail = Configure::read('Config.systemEmail'); if ($fromEmail) { $fromName = Configure::read('Config.systemName'); } else { $fromEmail = Configure::read('Config.adminEmail'); $fromName = Configure::read('Config.adminName'); } if ($fromEmail) { $this->from($fromEmail, $fromName); } $xMailer = Configure::read('Config.xMailer'); if ($xMailer) { $this->addHeaders(['X-Mailer' => $xMailer]); } return $this; } /** * @deprecated Since CakePHP 3.4.0 - use setProfile()/getProfile() instead. * * @param mixed $config * @return string|null|$this */ public function profile($config = null) { if ($config === null) { return $this->_profile; } return $this->setProfile($config); } /** * Overwrite to allow mimetype detection * * @param string|array $attachments String with the filename or array with filenames * @return $this * @throws \InvalidArgumentException */ public function setAttachments($attachments) { $attach = []; foreach ((array)$attachments as $name => $fileInfo) { if (!is_array($fileInfo)) { $fileInfo = ['file' => $fileInfo]; } if (!isset($fileInfo['file'])) { if (!isset($fileInfo['data'])) { throw new InvalidArgumentException('No file or data specified.'); } if (is_int($name)) { throw new InvalidArgumentException('No filename specified.'); } } else { $fileName = $fileInfo['file']; if (!preg_match('~^https?://~i', $fileInfo['file'])) { $fileInfo['file'] = realpath($fileInfo['file']); } if ($fileInfo['file'] === false || !Utility::fileExists($fileInfo['file'])) { throw new InvalidArgumentException(sprintf('File not found: "%s"', $fileName)); } if (is_int($name)) { $name = basename($fileInfo['file']); } } if (!isset($fileInfo['mimetype'])) { $ext = pathinfo($name, PATHINFO_EXTENSION); $fileInfo['mimetype'] = $this->_getMimeByExtension($ext); } $attach[$name] = $fileInfo; } $this->_attachments = $attach; return $this; } /** * @deprecated Since CakePHP 3.4.0 - use setAttachments()/getAttachments() instead. * * @param mixed|null $attachments * @return array|$this */ public function attachments($attachments = null) { if ($attachments === null) { return $this->_attachments; } return $this->setAttachments($attachments); } /** * Add an attachment from file * * @param string $file Absolute path * @param string|null $name * @param array $fileInfo * @return $this */ public function addAttachment($file, $name = null, $fileInfo = []) { $fileInfo['file'] = $file; if (!empty($name)) { $fileInfo = [$name => $fileInfo]; } else { $fileInfo = [$fileInfo]; } return $this->addAttachments($fileInfo); } /** * Add an attachment as blob * * @param string $content Blob data * @param string $filename to attach it * @param string|null $mimeType (leave it empty to get mimetype from $filename) * @param array $fileInfo * @return $this */ public function addBlobAttachment($content, $filename, $mimeType = null, $fileInfo = []) { if ($mimeType === null) { $ext = pathinfo($filename, PATHINFO_EXTENSION); $mimeType = $this->_getMimeByExtension($ext); } $fileInfo['data'] = $content; $fileInfo['mimetype'] = $mimeType; $file = [$filename => $fileInfo]; return $this->addAttachments($file); } /** * Adds an inline attachment from file. * * Options: * - mimetype * - contentDisposition * * @param string $contentId * @param string $file * @param string|null $name * @param array $options * @return $this */ public function addEmbeddedAttachmentByContentId($contentId, $file, $name = null, array $options = []) { if (empty($name)) { $name = basename($file); } $name = pathinfo($name, PATHINFO_FILENAME) . '_' . md5($file) . '.' . pathinfo($name, PATHINFO_EXTENSION); $options['file'] = $file; if (empty($options['mimetype'])) { $options['mimetype'] = $this->_getMime($file); } $options['contentId'] = $contentId; $file = [$name => $options]; return $this->addAttachments($file); } /** * Adds an inline attachment from file. * * Options: * - mimetype * - contentDisposition * * @param string $file Absolute path * @param string|null $name (optional) * @param array|string|null $options Options - string CID is deprecated * @param array $notUsed Former Options @deprecated 4th param is now 3rd since CakePHP 3.4.0 - Use addEmbeddedAttachmentByContentId() otherwise. * @return string CID ($this is deprecated!) */ public function addEmbeddedAttachment($file, $name = null, $options = null, array $notUsed = []) { if (empty($name)) { $name = basename($file); } $contentId = null; // Deprecated $contentId here if (!is_array($options)) { $contentId = $options; $options = $notUsed; } $name = pathinfo($name, PATHINFO_FILENAME) . '_' . md5($file) . '.' . pathinfo($name, PATHINFO_EXTENSION); if ($contentId === null && ($cid = $this->_isEmbeddedAttachment($file, $name))) { return $cid; } $options['file'] = $file; if (empty($options['mimetype'])) { $options['mimetype'] = $this->_getMime($file); } $options['contentId'] = $contentId ?: str_replace('-', '', Text::uuid()) . '@' . $this->_domain; $file = [$name => $options]; $res = $this->addAttachments($file); if ($contentId === null) { return $options['contentId']; } // Deprecated return $res; } /** * Adds an inline attachment from file. * * Options: * - mimetype * - contentDisposition * * @param string $contentId * @param string $content Blob data * @param string $file File File path to file * @param string|null $mimeType (leave it empty to get mimetype from $filename) * @param array $options * @return $this */ public function addEmbeddedBlobAttachmentByContentId($contentId, $content, $file, $mimeType = null, array $options = []) { if ($mimeType === null) { $ext = pathinfo($file, PATHINFO_EXTENSION); $mimeType = $this->_getMimeByExtension($ext); } $filename = pathinfo($file, PATHINFO_FILENAME) . '_' . md5($content) . '.' . pathinfo($file, PATHINFO_EXTENSION); $options['data'] = $content; $options['mimetype'] = $mimeType; $options['contentId'] = $contentId; $file = [$filename => $options]; $res = $this->addAttachments($file); return $res; } /** * Add an inline attachment as blob * * Options: * - contentDisposition * * @param string $content Blob data * @param string $filename to attach it * @param string|null $mimeType (leave it empty to get mimetype from $filename) * @param array|string|null $options Options - string CID is deprecated * @param array $notUsed * @return string CID CcontentId ($this is deprecated) */ public function addEmbeddedBlobAttachment($content, $filename, $mimeType = null, $options = null, array $notUsed = []) { if ($mimeType === null) { $ext = pathinfo($filename, PATHINFO_EXTENSION); $mimeType = $this->_getMimeByExtension($ext); } $contentId = null; // Deprecated $contentId here if (!is_array($options)) { $contentId = $options; $options = $notUsed; } $filename = pathinfo($filename, PATHINFO_FILENAME) . '_' . md5($content) . '.' . pathinfo($filename, PATHINFO_EXTENSION); if ($contentId === null && ($cid = $this->_isEmbeddedBlobAttachment($content, $filename))) { return $cid; } $options['data'] = $content; $options['mimetype'] = $mimeType; $options['contentId'] = $contentId ? $contentId : str_replace('-', '', Text::uuid()) . '@' . $this->_domain; $file = [$filename => $options]; $res = $this->addAttachments($file); if ($contentId === null) { return $options['contentId']; } // Deprecated return $res; } /** * Returns if this particular file has already been attached as embedded file with this exact name * to prevent the same image to overwrite each other and also to only send this image once. * Allows multiple usage of the same embedded image (using the same cid) * * @param string $file * @param string $name * @return bool|string CID of the found file or false if no such attachment can be found */ protected function _isEmbeddedAttachment($file, $name) { foreach ($this->_attachments as $filename => $fileInfo) { if ($filename !== $name) { continue; } return $fileInfo['contentId']; } return false; } /** * Returns if this particular file has already been attached as embedded file with this exact name * to prevent the same image to overwrite each other and also to only send this image once. * Allows multiple usage of the same embedded image (using the same cid) * * @param string $content * @param string $name * @return bool|string CID of the found file or false if no such attachment can be found */ protected function _isEmbeddedBlobAttachment($content, $name) { foreach ($this->_attachments as $filename => $fileInfo) { if ($filename !== $name) { continue; } return $fileInfo['contentId']; } return false; } /** * @param string $ext * @param string $default * @return mixed */ protected function _getMimeByExtension($ext, $default = 'application/octet-stream') { if (!isset($this->_Mime)) { $this->_Mime = new Mime(); } $mime = $this->_Mime->getMimeTypeByAlias($ext); if (!$mime) { $mime = $default; } return $mime; } /** * Try to find mimetype by file extension * * @param string $filename File name * @param string $default default MimeType * @return string Mimetype (falls back to `application/octet-stream`) */ protected function _getMime($filename, $default = 'application/octet-stream') { if (!isset($this->_Mime)) { $this->_Mime = new Mime(); } $mime = $this->_Mime->detectMimeType($filename); // Some environments falsely return the default too fast, better fallback to extension here if (!$mime || $mime === $default) { $ext = pathinfo($filename, PATHINFO_EXTENSION); $mime = $this->_Mime->getMimeTypeByAlias($ext); } return $mime; } /** * Read the file contents and return a base64 version of the file contents. * Overwrite parent to avoid File class and file_exists to false negative existent * remove images. * Also fixes file_get_contents (used via File class) to close the connection again * after getting remote files. So far it would have kept the connection open in HTTP/1.1. * * @param string $path The absolute path to the file to read. * @return string File contents in base64 encoding */ protected function _readFile($path) { $context = stream_context_create( ['http' => ['header' => 'Connection: close']]); $content = file_get_contents($path, 0, $context); if (!$content) { trigger_error('No content found for ' . $path); } return chunk_split(base64_encode($content)); } /** * Validate if the email has the required fields necessary to make send() work. * Assumes layouting (does not check on content to be present or if view/layout files are missing). * * @return bool Success */ public function validates() { if (!empty($this->_subject) && !empty($this->_to)) { return true; } return false; } /** * Set the body of the mail as we send it. * Note: the text can be an array, each element will appear as a seperate line in the message body. * * Do NOT pass a message if you use $this->set() in combination with templates * * @override * @param string|array|null $message Message * @return bool Success */ public function send($message = null) { $this->_log = [ 'to' => $this->_to, 'from' => $this->_from, 'sender' => $this->_sender, 'replyTo' => $this->_replyTo, 'cc' => $this->_cc, 'subject' => $this->_subject, 'bcc' => $this->_bcc, 'transport' => get_class($this->_transport), ]; /** @deprecated Since CakePHP 3.4.0-RC4 in core */ if ($this->_priority) { $this->_headers['X-Priority'] = $this->_priority; } // if not live, just log but do not send any mails //TODO: remove and use Debug Transport! if (!Configure::read('Config.live')) { $this->_logEmail(); return true; } // Security measure to not sent to the actual addressee in debug mode while email sending is live if (Configure::read('debug') && Configure::read('Config.live')) { $adminEmail = Configure::read('Config.adminEmail'); if (!$adminEmail) { $adminEmail = Configure::read('Config.systemEmail'); } foreach ($this->_to as $k => $v) { if ($k === $adminEmail) { continue; } unset($this->_to[$k]); $this->_to[$adminEmail] = $v; } foreach ($this->_cc as $k => $v) { if ($k === $adminEmail) { continue; } unset($this->_cc[$k]); $this->_cc[$adminEmail] = $v; } foreach ($this->_bcc as $k => $v) { if ($k === $adminEmail) { continue; } unset($this->_bcc[$k]); $this->_bcc[] = $v; } } try { $this->_debug = parent::send($message); } catch (\Exception $e) { $this->_error = $e->getMessage(); $this->_error .= ' (line ' . $e->getLine() . ' in ' . $e->getFile() . ')' . PHP_EOL . $e->getTraceAsString(); // always log report $this->_logEmail(LogLevel::ERROR); // log error $this->log($this->_error, LogLevel::ERROR); return false; } if (!empty($this->_profile['logReport'])) { $this->_logEmail(); } return true; } /** * @param string $level * @return void */ protected function _logEmail($level = LogLevel::INFO) { $content = $this->_log['transport'] . (!Configure::read('Config.live') ? ' (simulated)' : '') . ' - ' . 'TO:' . implode(',', array_keys($this->_log['to'])) . '||FROM:' . implode(',', array_keys($this->_log['from'])) . '||REPLY:' . implode(',', array_keys($this->_log['replyTo'])) . '||S:' . $this->_log['subject']; $this->log($content, $level); } /** * Attach inline/embedded files to the message. * * CUSTOM FIX: blob data support * * @override * @param string|null $boundary Boundary to use. If null, will default to $this->_boundary * @return array An array of lines to add to the message */ protected function _attachInlineFiles($boundary = null) { if ($boundary === null) { $boundary = $this->_boundary; } $msg = []; foreach ($this->_attachments as $filename => $fileInfo) { if (empty($fileInfo['contentId'])) { continue; } if (!empty($fileInfo['data'])) { $data = $fileInfo['data']; $data = chunk_split(base64_encode($data)); } elseif (!empty($fileInfo['file'])) { $data = $this->_readFile($fileInfo['file']); } else { continue; } $msg[] = '--' . $boundary; $msg[] = 'Content-Type: ' . $fileInfo['mimetype']; $msg[] = 'Content-Transfer-Encoding: base64'; $msg[] = 'Content-ID: <' . $fileInfo['contentId'] . '>'; $msg[] = 'Content-Disposition: inline; filename="' . $filename . '"'; $msg[] = ''; $msg[] = $data; $msg[] = ''; } return $msg; } /** * Attach non-embedded files by adding file contents inside boundaries. * * CUSTOM FIX: blob data support * * @override * @param string|null $boundary Boundary to use. If null, will default to $this->_boundary * @return array An array of lines to add to the message */ protected function _attachFiles($boundary = null) { if ($boundary === null) { $boundary = $this->_boundary; } $msg = []; foreach ($this->_attachments as $filename => $fileInfo) { if (!empty($fileInfo['contentId'])) { continue; } if (!empty($fileInfo['data'])) { $data = $fileInfo['data']; $data = chunk_split(base64_encode($data)); } elseif (!empty($fileInfo['file'])) { $data = $this->_readFile($fileInfo['file']); } else { continue; } $msg[] = '--' . $boundary; $msg[] = 'Content-Type: ' . $fileInfo['mimetype']; $msg[] = 'Content-Transfer-Encoding: base64'; if ( !isset($fileInfo['contentDisposition']) || $fileInfo['contentDisposition'] ) { $msg[] = 'Content-Disposition: attachment; filename="' . $filename . '"'; } $msg[] = ''; $msg[] = $data; $msg[] = ''; } return $msg; } /** * Returns the error if existent * * @return string */ public function getError() { return $this->_error; } }