/** * build and transmit a complex e-mail messages * * This function allows for individual posts, textual and HTML messages, and attached files. * * For this to work, e-mail service has to be explicitly activated in the * main configuration panel, at [script]control/configure.php[/script]. * * You can refer to local images in HTML parts, and the function will automatically attach these * to the message, else mail clients would not display them correctly. * * The message actually sent has a complex structure, with several parts assembled together, * as [explained at altepeter.net|http://altepeter.net/tech/articles/html-emails]. * * @link http://altepeter.net/tech/articles/html-emails * * Several recipients can be provided as a list of addresses separated by * commas. For bulk posts, recipients can be transmitted as an array of strings. * In all cases, this function sends one separate message per recipient. * * This function will ensure that only one mail message is send to a recipient, * by maintaining an internal list of addresses that have been processed. * Therefore, if this function is called several times, with some repeated recipients, * those will receive only the first message, and other messages to the same address * will be dropped. * * Bracketed recipients, such as ##Foo Bar <*****@*****.**>##, are handled properly, * meaning ##foo@bar.com## is transmitted to the mailing function, while * the string ##To: Foo Bar <*****@*****.**>## is added to headers. * * If an array of messages is provided to the function, it is turned to a multi-part * message, as in the following example: * * [php] * $message = array(); * $message['text/plain; charset=utf-8'] = 'This is a plain message'; * $message['text/html'] = '<html><head><body>This is an HTML message</body></html>'; * Mailer::post($from, $to, $subject, $message); * [/php] * * It is recommended to begin with the bare text, and to have the rich format part coming * after, as in the example. Also, if you don't provide a charset, then UTF-8 is used. * * Long lines of text/plain parts are wrapped according to * [link=Dan's suggestion]http://mailformat.dan.info/body/linelength.html[/link]. * * @link http://mailformat.dan.info/body/linelength.html Dan's Mail Format Site: Body: Line Length * * Message parts are encoded either as quoted-printable (textual entities) or as base-64 (others). * * A list of files to be attached to the message can be provided as in the following example: * * [php] * $attachments = array(); * $attachments[] = 'special/report.pdf'; * $attachments[] = 'skins/my_skin/newsletters/image.png'; * Mailer::post($from, $to, $subject, $message, $attachments); * [/php] * * Files are named from the installation directory of yacs, as visible in the examples. * * This function returns the number of successful posts, * and populates the error context, where applicable. * * @param string sender address * @param mixed recipient address(es) * @param string subject * @param mixed actual message, either a string, or an array of message parts * @param array attachments, if any * @param mixed additional headers, if any * @return the number of actual posts, or 0 * * @see articles/mail.php * @see letters/new.php * @see users/mail.php */ public static function post($from, $to, $subject, $message, $attachments = NULL, $headers = '') { global $context; // ensure that we have a sender if (!$from) { $from = Mailer::get_from_recipient(); } // email services have to be activated if (!isset($context['with_email']) || $context['with_email'] != 'Y') { Logger::error(i18n::s('E-mail has not been enabled on this system.')); return 0; // check sender address } elseif (!$from) { Logger::error(i18n::s('Empty sender address')); return 0; // check recipient address } elseif (!$to) { Logger::error(i18n::s('Empty recipient address')); return 0; // check mail subject } elseif (!$subject) { Logger::error(i18n::s('No subject')); return 0; // check mail content } elseif (!$message) { Logger::error(i18n::s('No message')); return 0; } // the end of line string for mail messages if (!defined('M_EOL')) { define('M_EOL', "\n"); } // encode the subject line $subject = Mailer::encode_subject($subject); // make some text out of an array if (is_array($headers)) { $headers = implode(M_EOL, $headers); } // From: header if (!preg_match('/^From: /im', $headers)) { $headers .= M_EOL . 'From: ' . $from; } // Reply-To: header if (!preg_match('/^Reply-To: /im', $headers)) { $headers .= M_EOL . 'Reply-To: ' . $from; } // Return-Path: header --to process errors if (!preg_match('/^Return-Path: /im', $headers)) { $headers .= M_EOL . 'Return-Path: ' . $from; } // Message-ID: header --helps to avoid spam filters if (!preg_match('/^Message-ID: /im', $headers)) { $headers .= M_EOL . 'Message-ID: <uniqid.' . uniqid() . '@' . $context['host_name'] . '>'; } // MIME-Version: header if (!preg_match('/^MIME-Version: /im', $headers)) { $headers .= M_EOL . 'MIME-Version: 1.0'; } // arrays are easier to manage if (is_string($message)) { // turn HTML entities to UTF-8 $message = Safe::html_entity_decode($message, ENT_QUOTES, 'UTF-8'); $copy = $message; $message = array(); $message['text/plain; charset=utf-8'] = $copy; unset($copy); } // turn attachments to some array too if (is_string($attachments)) { $attachments = array($attachments); } elseif (!is_array($attachments)) { $attachments = array(); } // we only consider objects from this server $my_prefix = $context['url_to_home'] . $context['url_to_root']; // transcode objects that will be transmitted along the message (i.e., images) foreach ($message as $type => $part) { // search throughout the full text $head = 0; while ($head = strpos($part, ' src="', $head)) { $head += strlen(' src="'); // a link has been found if ($tail = strpos($part, '"', $head + 1)) { $reference = substr($part, $head, $tail - $head); // remember local links only if (!strncmp($reference, $my_prefix, strlen($my_prefix))) { // local name $name = urldecode(substr($reference, strlen($my_prefix))); // content-id to be used instead of the link $cid = sprintf('%u@%s', crc32($name), $context['host_name']); // transcode the link in this part $part = substr($part, 0, $head) . 'cid:' . $cid . substr($part, $tail); // remember to put content in attachments of this message, if not done yet if (!in_array($name, $attachments)) { $attachments[] = $name; } } } } // remember the transcoded part $message[$type] = $part; } // we need some boundary string if (count($message) + count($attachments) > 1) { $boundary = md5(time()); } // wrapping threshold if (!defined('WRAPPING_LENGTH')) { define('WRAPPING_LENGTH', 70); } // combine message parts $content_type = ''; $body = ''; foreach ($message as $type => $part) { // quote textual entities if (!strncmp($type, 'text/', 5)) { $content_encoding = 'quoted-printable'; $part = quoted_printable_encode($part); // encode everything else } else { $content_encoding = 'base64'; $part = chunk_split(base64_encode($content), 76, M_EOL); } // only one part if (count($message) == 1) { $content_type = $type; $body = $part; // one part among several } else { // let user agent select among various alternatives if (!$content_type) { $content_type = 'multipart/alternative; boundary="' . $boundary . '-internal"'; } // introduction to assembled parts if (!$body) { $body = 'This is a multi-part message in MIME format.'; } // this part only --second EOL is part of the boundary chain $body .= M_EOL . M_EOL . '--' . $boundary . '-internal' . M_EOL . 'Content-Type: ' . $type . M_EOL . 'Content-Transfer-Encoding: ' . $content_encoding . M_EOL . M_EOL . $part; } } // finalize the body if (count($message) > 1) { $body .= M_EOL . M_EOL . '--' . $boundary . '-internal--'; } // a mix of things if (count($attachments)) { // encoding is irrelevant if there are multiple parts if (!strncmp($content_type, 'multipart/', 10)) { $content_encoding = ''; } else { $content_encoding = M_EOL . 'Content-Transfer-Encoding: ' . $content_encoding; } // identify the main part of the overall message $content_start = 'mainpart'; // the current body becomes the first part of a larger message $body = 'This is a multi-part message in MIME format.' . M_EOL . M_EOL . '--' . $boundary . '-external' . M_EOL . 'Content-Type: ' . $content_type . $content_encoding . M_EOL . 'Content-ID: <' . $content_start . '>' . M_EOL . M_EOL . $body; // message parts should be considered as an aggregate whole --see RFC 2387 $content_type = 'multipart/related; type="multipart/alternative"; boundary="' . $boundary . '-external"'; $content_encoding = ''; // process every file foreach ($attachments as $name => $content) { // read external file content if (preg_match('/^[0-9]+$/', $name)) { // only a file name has been provided $name = $content; // read file content from the file system if (!($content = Safe::file_get_contents($name))) { continue; } } // file name is the file type if (preg_match('/name="(.+)?"/', $name, $matches)) { $type = $name; $name = $matches[1]; } else { $type = Files::get_mime_type($name); } // a unique id for for this file $cid = sprintf('%u@%s', crc32($name), $context['host_name']); // set a name that avoids problems $basename = utf8::to_ascii(basename($name)); // headers for one file $body .= M_EOL . M_EOL . '--' . $boundary . '-external' . M_EOL . 'Content-Type: ' . $type . M_EOL . 'Content-Disposition: inline; filename="' . str_replace('"', '', $basename) . '"' . M_EOL . 'Content-ID: <' . $cid . '>'; // transfer textual entities as they are if (!strncmp($type, 'text/', 5)) { $body .= M_EOL . 'Content-Transfer-Encoding: quoted-printable' . M_EOL . M_EOL . quoted_printable_encode($content); // encode everything else } else { $body .= M_EOL . 'Content-Transfer-Encoding: base64' . M_EOL . M_EOL . chunk_split(base64_encode($content), 76, M_EOL); } } // the closing boundary $body .= M_EOL . M_EOL . '--' . $boundary . '-external--'; } // Content-Type: header if ($content_type && !preg_match('/^Content-Type: /im', $headers)) { $headers .= M_EOL . 'Content-Type: ' . $content_type; } // Content-Transfer-Encoding: header if (!isset($boundary) && $content_encoding && !preg_match('/^Content-Transfer-Encoding: /im', $headers)) { $headers .= M_EOL . 'Content-Transfer-Encoding: ' . $content_encoding; } // Start: header if (isset($boundary) && isset($content_start) && $content_start && !preg_match('/^Start: /im', $headers)) { $headers .= M_EOL . 'Start: ' . $content_start; } // X-Mailer: header --helps to avoid spam filters if (!preg_match('/^X-Mailer: /im', $headers)) { $headers .= M_EOL . 'X-Mailer: yacs'; } // strip leading spaces and newlines $headers = trim($headers); // make an array of recipients if (!is_array($to)) { $to = Mailer::explode_recipients($to); } // the list of recipients contacted during overall script execution if (!isset($context['mailer_recipients'])) { $context['mailer_recipients'] = array(); } // process every recipient $posts = 0; foreach ($to as $recipient) { // clean the provided string $recipient = trim(str_replace(array("\r\n", "\r", "\n", "\t"), ' ', $recipient)); // this e-mail address has already been processed if (in_array($recipient, $context['mailer_recipients'])) { if (isset($context['debug_mail']) && $context['debug_mail'] == 'Y') { Logger::remember('shared/mailer.php: Skipping recipient already processed', $recipient, 'debug'); } continue; // remember this recipient } else { $context['mailer_recipients'][] = $recipient; } // queue the message Mailer::queue($recipient, $subject, $body, $headers); $posts++; } // track last submission include_once $context['path_to_root'] . 'shared/values.php'; Values::set('mailer.last.queued', $subject . ' (' . $posts . ' recipients)'); // return the number of actual posts return $posts; }
/** * encode a sequence of HTML tags, or plain text, to PDF * * @param string the text to append * @return the content of PDF * @see articles/fetch_as_pdf.php */ function encode($text) { global $context; // // meta information // // encode it to iso8859 -- sorry // document title if ($context['page_title']) { $this->SetTitle(utf8::to_iso8859(Safe::html_entity_decode($context['page_title'], ENT_COMPAT, 'ISO-8859-15'))); } // document author if ($context['page_author']) { $this->SetAuthor(utf8::to_iso8859(Safe::html_entity_decode($context['page_author'], ENT_COMPAT, 'ISO-8859-15'))); } // document subject if ($context['subject']) { $this->SetSubject(utf8::to_iso8859(Safe::html_entity_decode($context['subject'], ENT_COMPAT, 'ISO-8859-15'))); } // document creator (typically, the tool used to produce the document) $this->SetCreator('yacs'); // // PDF content // // start the rendering engine $this->AliasNbPages(); $this->AddPage(); $this->SetFont('Arial', 'B', 16); // reference view.php instead of ourself to achieve correct links $text = str_replace('/fetch_as_pdf.php', '/view.php', $text); // remove all unsupported tags $text = strip_tags($text, "<a><b><blockquote><br><code><div><em><font><h1><h2><h3><h4><hr><i><img><li><p><pre><strong><table><tr><tt><u><ul>"); // spaces instead of carriage returns $text = str_replace("\n", ' ', $text); // transcode to ISO8859-15 characters $text = utf8::to_iso8859(Safe::html_entity_decode($text, ENT_COMPAT, 'ISO-8859-15')); // locate every HTML/XML tag $areas = preg_split('/<(.*)>/U', $text, -1, PREG_SPLIT_DELIM_CAPTURE); $height = 5; $link = ''; foreach ($areas as $index => $entity) { // a tag entity if ($index % 2) { @(list($tag, $attributes) = explode(' ', $entity, 2)); switch (strtolower($tag)) { case 'a': if (preg_match('/href="(.*)"/i', $attributes, $matches)) { $link = $matches[1]; // suppress local references (eg, in table of content) if (preg_match('/(#.*)/', $link)) { $link = ''; } elseif ($link[0] == '/') { $link = $context['url_to_home'] . $link; } } break; case 'b': $this->SetFont('', 'B'); break; case '/b': $this->SetFont('', ''); break; case 'blockquote': $this->Ln($height); break; case '/blockquote': $this->Ln($height); break; case 'br': $this->Ln($height); break; case 'code': $this->SetFont('Courier', '', 11); $this->SetFontSize(11); break; case '/code': $this->SetFont('Times', '', 12); $this->SetFontSize(12); break; case 'div': case '/div': $this->Ln($height); break; case 'em': $this->SetFont('', 'I'); break; case '/em': $this->SetFont('', ''); break; case 'font': if (preg_match('/color="#(.{6})"/i', $attributes, $matches)) { $color = $matches[1]; $r = hexdec($color[0] . $color[1]); $g = hexdec($color[2] . $color[3]); $b = hexdec($color[4] . $color[5]); $this->SetTextColor($r, $g, $b); } break; case 'font': $this->SetFont('Times', '', 12); $this->SetTextColor(0, 0, 0); $this->SetFontSize(12); break; case 'h1': $this->Ln(10); $this->SetTextColor(150, 0, 0); $this->SetFontSize(22); $height = 8; break; case 'h2': $this->Ln(8); $this->SetFontSize(18); $height = 6; break; case 'h3': $this->Ln(6); $this->SetFontSize(16); $height = 5; break; case 'h4': $this->Ln(6); $this->SetTextColor(102, 0, 0); $this->SetFontSize(14); $height = 5; break; case '/h1': case '/h2': case '/h3': case '/h4': $this->Ln($height); $this->SetFont('Times', '', 12); $this->SetTextColor(0, 0, 0); $this->SetFontSize(12); $height = 5; break; case 'hr': $this->Ln($height + 2); $this->Line($this->GetX(), $this->GetY(), $this->GetX() + 187, $this->GetY()); $this->Ln(3); break; case 'i': $this->SetFont('', 'I'); break; case '/i': $this->SetFont('', ''); break; case 'img': // only accept JPG and PNG if (preg_match('/src="([^"]+\\.(jpg|jpeg|png))"/i', $attributes, $matches)) { $image = $matches[1]; // map on a file $image = preg_replace('/^' . preg_quote($context['url_to_home'] . $context['url_to_root'], '/') . '/', $context['path_to_root'], $image); // include the image only if the file exists if ($attributes = Safe::GetImageSize($image)) { // insert an image at 72 dpi -- the k factor $this->Image($image, $this->GetX(), $this->GetY(), $attributes[0] / $this->k, $attributes[1] / $this->k); // make room for the image $this->y += 3 + $attributes[1] / $this->k; } } break; case 'li': $this->Ln($height); break; case '/li': break; case 'p': case '/p': $this->Ln($height); break; case 'pre': $this->SetFont('Courier', '', 11); $this->SetFontSize(11); $preformatted = TRUE; break; case '/pre': $this->SetFont('Times', '', 12); $this->SetFontSize(12); $preformatted = FALSE; break; case 'strong': $this->SetFont('', 'B'); break; case '/strong': $this->SetFont('', ''); break; case 'table': $this->Ln($height); break; case '/table': $this->Ln($height); break; case 'tr': $this->Ln($height + 2); $this->Line($this->GetX(), $this->GetY(), $this->GetX() + 187, $this->GetY()); $this->Ln(3); break; case 'tt': $this->SetFont('Courier', '', 11); $this->SetFontSize(11); break; case '/tt': $this->SetFont('Times', '', 12); $this->SetFontSize(12); break; case 'u': $this->SetFont('', 'U'); break; case '/u': $this->SetFont('', ''); break; case 'ul': break; case '/ul': break; } // a textual entity } else { // we have to write a link if ($link) { // a blue underlined link $this->SetTextColor(0, 0, 255); $this->SetFont('', 'U'); $this->Write($height, $entity, $link); $link = ''; $this->SetTextColor(0, 0, 0); $this->SetFont('', ''); // regular text } else { $this->Write($height, $entity); } } } // return the PDF content as a string return $this->Output('dummy', 'S'); }
return; } // keep only titles and ISO8859-1 labels $titles = array(); $links = array(); $count = 0; foreach ($items as $url => $label) { // we are not interested into all attributes if (is_array($label)) { $label = $label[1]; } // strip codes include_once '../../codes/codes.php'; $label = Codes::strip($label); // remove every html tag $label = strip_tags(Safe::html_entity_decode($label)); // remember this $titles[$count] = $label; $links[$count] = $url; $count++; } // cache handling --except on scripts/validate.php if (!headers_sent() && (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'GET')) { // this is a schockwave object Safe::header('Content-Type: application/x-shockwave-flash'); // enable 30-minute caching (30*60 = 1800), even through https, to help IE6 on download http::expire(1800); // the original content $page = ''; for ($index = 0; $index < $count; $index++) { $page .= $titles[$index] . ':' . $links[$index] . ':';