diff --git a/htdocs/admin/mails.php b/htdocs/admin/mails.php index 1a25af49789..dfa0bccb06b 100644 --- a/htdocs/admin/mails.php +++ b/htdocs/admin/mails.php @@ -34,14 +34,15 @@ $langs->loadLangs(array("companies", "products", "admin", "mails", "other", "err $action = GETPOST('action', 'aZ09'); $cancel = GETPOST('cancel', 'aZ09'); +$trackid = GETPOST('trackid'); + if (!$user->admin) { accessforbidden(); } $usersignature = $user->signature; -// For action = test or send, we ensure that content is not html, even for signature, because this we want a test with NO html. - -if ($action == 'test' || $action == 'send') { +// For action = test or send, we ensure that content is not html, even for signature, because for this we want a test with NO html. +if ($action == 'test' || ($action == 'send' && $trackid = 'test')) { $usersignature = dol_string_nohtmltag($usersignature, 2); } @@ -130,7 +131,7 @@ $actiontypecode = ''; // Not an event for agenda $triggersendname = ''; // Disable triggers $paramname = 'id'; $mode = 'emailfortest'; -$trackid = (($action == 'testhtml') ? "testhtml" : "test"); +$trackid = ($action == 'send' ? GETPOST('trackid', 'aZ09') : $action); $sendcontext = ''; include DOL_DOCUMENT_ROOT.'/core/actions_sendmails.inc.php'; @@ -143,7 +144,6 @@ if ($action == 'presend' && GETPOST('trackid', 'alphanohtml') == 'testhtml') { - /* * View */ diff --git a/htdocs/core/actions_sendmails.inc.php b/htdocs/core/actions_sendmails.inc.php index 3749b403469..772d2b7ed5d 100644 --- a/htdocs/core/actions_sendmails.inc.php +++ b/htdocs/core/actions_sendmails.inc.php @@ -108,6 +108,10 @@ if (($action == 'send' || $action == 'relance') && !GETPOST('addfile') && !GETPO $trackid = GETPOST('trackid', 'aZ09'); } + // Set tmp user directory (used to convert images embedded as img src=data:image) + $vardir = $conf->user->dir_output."/".$user->id; + $upload_dir_tmp = $vardir.'/temp'; // TODO Add $keytoavoidconflict in upload_dir path + $subject = ''; //$actionmsg = ''; $actionmsg2 = ''; @@ -359,7 +363,7 @@ if (($action == 'send' || $action == 'relance') && !GETPOST('addfile') && !GETPO if (empty($sendcontext)) { $sendcontext = 'standard'; } - $mailfile = new CMailFile($subject, $sendto, $from, $message, $filepath, $mimetype, $filename, $sendtocc, $sendtobcc, $deliveryreceipt, -1, '', '', $trackid, '', $sendcontext); + $mailfile = new CMailFile($subject, $sendto, $from, $message, $filepath, $mimetype, $filename, $sendtocc, $sendtobcc, $deliveryreceipt, -1, '', '', $trackid, '', $sendcontext, '', $upload_dir_tmp); if (!empty($mailfile->error) || !empty($mailfile->errors)) { setEventMessages($mailfile->error, $mailfile->errors, 'errors'); diff --git a/htdocs/core/class/CMailFile.class.php b/htdocs/core/class/CMailFile.class.php index b3ff0aae930..ffd1d6e8e5d 100644 --- a/htdocs/core/class/CMailFile.class.php +++ b/htdocs/core/class/CMailFile.class.php @@ -120,6 +120,10 @@ class CMailFile * @var array filenames list (List of attached file name in message) */ public $mimefilename_list = array(); + /** + * @var array filenames cid + */ + public $cid_list = array(); // Image public $html; @@ -159,8 +163,9 @@ class CMailFile * @param string $moreinheader More in header. $moreinheader must contains the "\r\n" (TODO not supported for other MAIL_SEND_MODE different than 'mail' and 'smtps' for the moment) * @param string $sendcontext 'standard', 'emailing', ... (used to define which sending mode and parameters to use) * @param string $replyto Reply-to email (will be set to same value than From by default if not provided) + * @param string $upload_dir_tmp Temporary directory (used to convert images embedded as img src=data:image) */ - public function __construct($subject, $to, $from, $msg, $filename_list = array(), $mimetype_list = array(), $mimefilename_list = array(), $addr_cc = "", $addr_bcc = "", $deliveryreceipt = 0, $msgishtml = 0, $errors_to = '', $css = '', $trackid = '', $moreinheader = '', $sendcontext = 'standard', $replyto = '') + public function __construct($subject, $to, $from, $msg, $filename_list = array(), $mimetype_list = array(), $mimefilename_list = array(), $addr_cc = "", $addr_bcc = "", $deliveryreceipt = 0, $msgishtml = 0, $errors_to = '', $css = '', $trackid = '', $moreinheader = '', $sendcontext = 'standard', $replyto = '', $upload_dir_tmp = '') { global $conf, $dolibarr_main_data_root, $user; @@ -171,6 +176,8 @@ class CMailFile } } + $cid_list = array(); + $this->sendcontext = $sendcontext; // Define this->sendmode ('mail', 'smtps', 'siwftmailer', ...) according to $sendcontext ('standard', 'emailing', 'ticket') @@ -248,26 +255,41 @@ class CMailFile $findimg = 0; if (!empty($conf->global->MAIN_MAIL_ADD_INLINE_IMAGES_IF_IN_MEDIAS)) { + // Search into the body for ]*>/", " ", $strContent); - $strContentAltText = html_entity_decode(strip_tags($strContentAltText)); + // TODO We could replace with [Filename.ext] like Gmail do. + $strContentAltText = html_entity_decode(strip_tags($strContentAltText)); // Remove any HTML tags $strContentAltText = trim(wordwrap($strContentAltText, 75, empty($conf->global->MAIN_FIX_FOR_BUGGED_MTA) ? "\r\n" : "\n")); // Check if html header already in message, if not complete the message @@ -1447,9 +1470,10 @@ class CMailFile * @param array $filename_list Tableau * @param array $mimetype_list Tableau * @param array $mimefilename_list Tableau - * @return string Chaine fichiers encodes + * @param array $cidlist Array of CID if file must be completed with CID code + * @return string String with files encoded */ - public function write_files($filename_list, $mimetype_list, $mimefilename_list) + private function write_files($filename_list, $mimetype_list, $mimefilename_list, $cidlist) { // phpcs:enable $out = ''; @@ -1472,6 +1496,10 @@ class CMailFile $out .= "Content-Type: ".$mimetype_list[$i]."; name=\"".$filename_list[$i]."\"".$this->eol; $out .= "Content-Transfer-Encoding: base64".$this->eol; $out .= "Content-Description: ".$filename_list[$i].$this->eol; + if (!empty($cidlist) && is_array($cidlist) && $cidlist[$i]) { + $out .= "X-Attachment-Id: ".$cidlist[$i].$this->eol; + $out .= "Content-ID: <".$cidlist[$i].'>'.$this->eol; + } $out .= $this->eol; $out .= $encoded; $out .= $this->eol; @@ -1628,41 +1656,46 @@ class CMailFile /** * Seearch images into html message and init array this->images_encoded if found * - * @param string $images_dir Location of physical images files + * @param string $images_dir Location of physical images files. For example $dolibarr_main_data_root.'/medias' * @return int >0 if OK, <0 if KO */ - public function findHtmlImages($images_dir) + private function findHtmlImages($images_dir) { - // Build the list of image extensions + // Build the array of image extensions $extensions = array_keys($this->image_types); + // We search (into mail body this->html), if we find some strings like "... file=xxx.img" + // For example when: + // $matches = array(); preg_match_all('/(?:"|\')([^"\']+\.('.implode('|', $extensions).'))(?:"|\')/Ui', $this->html, $matches); // If "xxx.ext" or 'xxx.ext' found if (!empty($matches)) { $i = 0; + // We are interested in $matches[1] only (the second set of parenthesis into regex) foreach ($matches[1] as $full) { + $regs = array(); if (preg_match('/file=([A-Za-z0-9_\-\/]+[\.]?[A-Za-z0-9]+)?$/i', $full, $regs)) { // If xxx is 'file=aaa' $img = $regs[1]; if (file_exists($images_dir.'/'.$img)) { // Image path in src $src = preg_quote($full, '/'); - // Image full path $this->html_images[$i]["fullpath"] = $images_dir.'/'.$img; - // Image name $this->html_images[$i]["name"] = $img; - // Content type - if (preg_match('/^.+\.(\w{3,4})$/', $img, $reg)) { - $ext = strtolower($reg[1]); + $regext = array(); + if (preg_match('/^.+\.(\w{3,4})$/', $img, $regext)) { + $ext = strtolower($regext[1]); $this->html_images[$i]["content_type"] = $this->image_types[$ext]; } - // cid $this->html_images[$i]["cid"] = dol_hash(uniqid(time()), 3); // Force md5 hash (does not contains special chars) + // type + $this->html_images[$i]["type"] = 'cidfromurl'; + $this->html = preg_replace("/src=\"$src\"|src='$src'/i", "src=\"cid:".$this->html_images[$i]["cid"]."\"", $this->html); } $i++; @@ -1682,6 +1715,7 @@ class CMailFile // Read image file if ($image = file_get_contents($fullpath)) { // On garde que le nom de l'image + $regs = array(); preg_match('/([A-Za-z0-9_-]+[\.]?[A-Za-z0-9]+)?$/i', $img["name"], $regs); $imgName = $regs[1]; @@ -1706,6 +1740,85 @@ class CMailFile } } + /** + * Seearch images with data:image format into html message + * + * @param string $images_dir Location of where to store physicaly images files. For example $dolibarr_main_data_root.'/medias' + * @return int >0 if OK, <0 if KO + */ + private function findHtmlImagesIsSrcData($images_dir) + { + global $conf; + + // Build the array of image extensions + $extensions = array_keys($this->image_types); + + if ($images_dir && !dol_is_dir($images_dir)) { + dol_mkdir($images_dir, DOL_DATA_ROOT); + } + + // Uncomment this for debug + /* + global $dolibarr_main_data_root; + $outputfile = $dolibarr_main_data_root."/dolibarr_mail.log"; + $fp = fopen($outputfile, "w"); + fwrite($fp, $this->html); + fclose($fp); + */ + + // We search (into mail body this->html), if we find some strings like "... file=xxx.img" + // For example when: + // + $matches = array(); + preg_match_all('/src="data:image\/('.implode('|', $extensions).');base64,([^"]+)"/Ui', $this->html, $matches); // If "xxx.ext" or 'xxx.ext' found + + if (!empty($matches)) { + if (empty($images_dir)) { + // No temp directory provided, so we are not able to support convertion of data:image into physical images. + $this->error = 'NoTempDirProvidedInCMailConstructorSoCantConvertDataImgOnDisk'; + return -1; + } + + $i = 0; + foreach ($matches[1] as $key => $ext) { + // We save the image to send in disk + $filecontent = $matches[2][$key]; + $cid = dol_hash(uniqid(time()), 3); + $destfiletmp = $images_dir.'/'.$cid.'.'.$ext; + + $fhandle = @fopen($destfiletmp, 'w'); + if ($fhandle) { + $nbofbyteswrote = fwrite($fhandle, base64_decode($filecontent)); + fclose($fhandle); + @chmod($destfiletmp, octdec($conf->global->MAIN_UMASK)); + } else { + $this->errors[] = "Failed to open file '".$destfiletmp."' for write"; + return -1; + } + + if (file_exists($destfiletmp)) { + // Image full path + $this->html_images[$i]["fullpath"] = $destfiletmp; + // Image name + $this->html_images[$i]["name"] = basename($destfiletmp); + // Content type + $this->html_images[$i]["content_type"] = $this->image_types[strtolower($ext)]; + // cid + $this->html_images[$i]["cid"] = $cid; + // type + $this->html_images[$i]["type"] = 'cidfromdata'; + + $this->html = preg_replace('/src="data:image\/'.$ext.';base64,'.preg_quote($filecontent, '/').'"/', 'src="cid:'.$this->html_images[$i]["cid"].'"', $this->html); + } + $i++; + } + + return 1; + } else { + return 0; + } + } + /** * Return a formatted address string for SMTP protocol * diff --git a/htdocs/core/class/html.formmail.class.php b/htdocs/core/class/html.formmail.class.php index 16302dc3c1a..41cf2482f9a 100644 --- a/htdocs/core/class/html.formmail.class.php +++ b/htdocs/core/class/html.formmail.class.php @@ -1193,7 +1193,7 @@ class FormMail extends Form $out .= "\n"; } else { $out = ''.$langs->trans("MailErrorsTo").''; - $out .= ''; + $out .= ''; $out .= "\n"; } return $out; diff --git a/htdocs/core/class/smtps.class.php b/htdocs/core/class/smtps.class.php index 06ada5c4911..92b835caf7f 100644 --- a/htdocs/core/class/smtps.class.php +++ b/htdocs/core/class/smtps.class.php @@ -1577,11 +1577,14 @@ class SMTPs // loop through all attachments foreach ($_content as $_file => $_data) { $content .= "--".$this->_getBoundary('mixed')."\r\n" - . 'Content-Disposition: attachment; filename="'.$_data['fileName'].'"'."\r\n" - . 'Content-Type: '.$_data['mimeType'].'; name="'.$_data['fileName'].'"'."\r\n" - . 'Content-Transfer-Encoding: base64'."\r\n" - . 'Content-Description: '.$_data['fileName']."\r\n"; - + . 'Content-Disposition: attachment; filename="'.$_data['fileName'].'"'."\r\n" + . 'Content-Type: '.$_data['mimeType'].'; name="'.$_data['fileName'].'"'."\r\n" + . 'Content-Transfer-Encoding: base64'."\r\n" + . 'Content-Description: '.$_data['fileName']."\r\n"; + if (!empty($_data['cid'])) { + $content .= "X-Attachment-Id: ".$_data['cid']."\r\n"; + $content .= "Content-ID: <".$_data['cid'].">\r\n"; + } if ($this->getMD5flag()) { $content .= 'Content-MD5: '.$_data['md5']."\r\n"; } @@ -1595,9 +1598,9 @@ class SMTPs $content .= "--".$this->_getBoundary('related')."\r\n"; // always related for an inline image $content .= 'Content-Type: '.$_data['mimeType'].'; name="'.$_data['imageName'].'"'."\r\n" - . 'Content-Transfer-Encoding: base64'."\r\n" - . 'Content-Disposition: inline; filename="'.$_data['imageName'].'"'."\r\n" - . 'Content-ID: <'.$_data['cid'].'> '."\r\n"; + . 'Content-Transfer-Encoding: base64'."\r\n" + . 'Content-Disposition: inline; filename="'.$_data['imageName'].'"'."\r\n" + . 'Content-ID: <'.$_data['cid'].'> '."\r\n"; if ($this->getMD5flag()) { $content .= 'Content-MD5: '.$_data['md5']."\r\n"; @@ -1664,9 +1667,10 @@ class SMTPs * @param string $strContent File data to attach to message * @param string $strFileName File Name to give to attachment * @param string $strMimeType File Mime Type of attachment + * @param string $strCid File Cid of attachment (if defined, to be shown inline) * @return void */ - public function setAttachment($strContent, $strFileName = 'unknown', $strMimeType = 'unknown') + public function setAttachment($strContent, $strFileName = 'unknown', $strMimeType = 'unknown', $strCid = '') { if ($strContent) { $strContent = rtrim(chunk_split(base64_encode($strContent), 76, "\r\n")); // 76 max is defined into http://tools.ietf.org/html/rfc2047 @@ -1674,6 +1678,7 @@ class SMTPs $this->_msgContent['attachment'][$strFileName]['mimeType'] = $strMimeType; $this->_msgContent['attachment'][$strFileName]['fileName'] = $strFileName; $this->_msgContent['attachment'][$strFileName]['data'] = $strContent; + $this->_msgContent['attachment'][$strFileName]['cid'] = $strCid; // If defined, it means this attachment must be shown inline if ($this->getMD5flag()) { $this->_msgContent['attachment'][$strFileName]['md5'] = dol_hash($strContent, 3);