diff --git a/conf/dokuwiki.php b/conf/dokuwiki.php index 4ca29b4614570f8f248a69854c362be47702ec04..35f946b10ac74df4b797ff9dc14565bda4694d4d 100644 --- a/conf/dokuwiki.php +++ b/conf/dokuwiki.php @@ -110,6 +110,7 @@ $conf['notify'] = ''; //send change info to this email (leave $conf['registernotify'] = ''; //send info about newly registered users to this email (leave blank for nobody) $conf['mailfrom'] = ''; //use this email when sending mails $conf['mailprefix'] = ''; //use this as prefix of outgoing mails +$conf['htmlmail'] = 1; //send HTML multipart mails /* Syndication Settings */ $conf['sitemap'] = 0; //Create a google sitemap? How often? In days. diff --git a/inc/DifferenceEngine.php b/inc/DifferenceEngine.php index 01926b20cffc3bbbe8730d4e15dbabff315c947f..0a7ce8e7c421ccea3cb1ddf4dcc5ecd42821330d 100644 --- a/inc/DifferenceEngine.php +++ b/inc/DifferenceEngine.php @@ -818,6 +818,39 @@ class DiffFormatter { } } +/** + * Utilityclass for styling HTML formatted diffs + * + * Depends on global var $DIFF_INLINESTYLES, if true some minimal predefined + * inline styles are used. Useful for HTML mails and RSS feeds + * + * @author Andreas Gohr <andi@splitbrain.org> + */ +class HTMLDiff { + /** + * Holds the style names and basic CSS + */ + static public $styles = array( + 'diff-addedline' => 'background-color: #ddffdd;', + 'diff-deletedline' => 'background-color: #ffdddd;', + 'diff-context' => 'background-color: #f5f5f5;', + 'diff-mark' => 'color: #ff0000;', + ); + + /** + * Return a class or style parameter + */ + static function css($classname){ + global $DIFF_INLINESTYLES; + + if($DIFF_INLINESTYLES){ + if(!isset(self::$styles[$classname])) return ''; + return 'style="'.self::$styles[$classname].'"'; + }else{ + return 'class="'.$classname.'"'; + } + } +} /** * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3 @@ -838,11 +871,11 @@ class _HWLDF_WordAccumulator { function _flushGroup($new_tag) { if ($this->_group !== '') { if ($this->_tag == 'mark') - $this->_line .= '<strong>'.$this->_group.'</strong>'; + $this->_line .= '<strong '.HTMLDiff::css('diff-mark').'>'.$this->_group.'</strong>'; elseif ($this->_tag == 'add') - $this->_line .= '<span class="diff-addedline">'.$this->_group.'</span>'; + $this->_line .= '<span '.HTMLDiff::css('diff-addedline').'>'.$this->_group.'</span>'; elseif ($this->_tag == 'del') - $this->_line .= '<span class="diff-deletedline"><del>'.$this->_group.'</del></span>'; + $this->_line .= '<span '.HTMLDiff::css('diff-deletedline').'><del>'.$this->_group.'</del></span>'; else $this->_line .= $this->_group; } @@ -1020,8 +1053,8 @@ class TableDiffFormatter extends DiffFormatter { global $lang; $l1 = $lang['line'].' '.$xbeg; $l2 = $lang['line'].' '.$ybeg; - $r = '<tr><td class="diff-blockheader" colspan="2">'.$l1.":</td>\n". - '<td class="diff-blockheader" colspan="2">'.$l2.":</td>\n". + $r = '<tr><td '.HTMLDiff::css('diff-blockheader').' colspan="2">'.$l1.":</td>\n". + '<td '.HTMLDiff::css('diff-blockheader').' colspan="2">'.$l2.":</td>\n". "</tr>\n"; return $r; } @@ -1037,11 +1070,11 @@ class TableDiffFormatter extends DiffFormatter { } function addedLine($line) { - return '<td>+</td><td class="diff-addedline">' . $line.'</td>'; + return '<td>+</td><td '.HTMLDiff::css('diff-addedline').'>' . $line.'</td>'; } function deletedLine($line) { - return '<td>-</td><td class="diff-deletedline">' . $line.'</td>'; + return '<td>-</td><td '.HTMLDiff::css('diff-deletedline').'>' . $line.'</td>'; } function emptyLine() { @@ -1049,7 +1082,7 @@ class TableDiffFormatter extends DiffFormatter { } function contextLine($line) { - return '<td> </td><td class="diff-context">'.$line.'</td>'; + return '<td> </td><td '.HTMLDiff::css('diff-context').'>'.$line.'</td>'; } function _added($lines) { @@ -1115,9 +1148,9 @@ class InlineDiffFormatter extends DiffFormatter { $xbeg .= "," . $xlen; if ($ylen != 1) $ybeg .= "," . $ylen; - $r = '<tr><td colspan="'.$this->colspan.'" class="diff-blockheader">@@ '.$lang['line']." -$xbeg +$ybeg @@"; - $r .= ' <span class="diff-deletedline"><del>'.$lang['deleted'].'</del></span>'; - $r .= ' <span class="diff-addedline">'.$lang['created'].'</span>'; + $r = '<tr><td colspan="'.$this->colspan.'" '.HTMLDiff::css('diff-blockheader').'>@@ '.$lang['line']." -$xbeg +$ybeg @@"; + $r .= ' <span '.HTMLDiff::css('diff-deletedline').'><del>'.$lang['deleted'].'</del></span>'; + $r .= ' <span '.HTMLDiff::css('diff-addedline').'>'.$lang['created'].'</span>'; $r .= "</td></tr>\n"; return $r; } @@ -1134,19 +1167,19 @@ class InlineDiffFormatter extends DiffFormatter { function _added($lines) { foreach ($lines as $line) { - print('<tr><td colspan="'.$this->colspan.'" class="diff-addedline">'. $line . "</td></tr>\n"); + print('<tr><td colspan="'.$this->colspan.'" '.HTMLDiff::css('diff-addedline').'>'. $line . "</td></tr>\n"); } } function _deleted($lines) { foreach ($lines as $line) { - print('<tr><td colspan="'.$this->colspan.'" class="diff-deletedline"><del>' . $line . "</del></td></tr>\n"); + print('<tr><td colspan="'.$this->colspan.'" '.HTMLDiff::css('diff-deletedline').'><del>' . $line . "</del></td></tr>\n"); } } function _context($lines) { foreach ($lines as $line) { - print('<tr><td colspan="'.$this->colspan.'" class="diff-context">'.$line."</td></tr>\n"); + print('<tr><td colspan="'.$this->colspan.'" '.HTMLDiff::css('diff-context').'>'.$line."</td></tr>\n"); } } diff --git a/inc/Mailer.class.php b/inc/Mailer.class.php new file mode 100644 index 0000000000000000000000000000000000000000..507150d00402e3e0f917cd4af0cce882edec89ee --- /dev/null +++ b/inc/Mailer.class.php @@ -0,0 +1,652 @@ +<?php +/** + * A class to build and send multi part mails (with HTML content and embedded + * attachments). All mails are assumed to be in UTF-8 encoding. + * + * Attachments are handled in memory so this shouldn't be used to send huge + * files, but then again mail shouldn't be used to send huge files either. + * + * @author Andreas Gohr <andi@splitbrain.org> + */ + +// end of line for mail lines - RFC822 says CRLF but postfix (and other MTAs?) +// think different +if(!defined('MAILHEADER_EOL')) define('MAILHEADER_EOL',"\n"); +#define('MAILHEADER_ASCIIONLY',1); + +class Mailer { + + protected $headers = array(); + protected $attach = array(); + protected $html = ''; + protected $text = ''; + + protected $boundary = ''; + protected $partid = ''; + protected $sendparam= null; + + protected $validator = null; + protected $allowhtml = true; + + /** + * Constructor + * + * Initializes the boundary strings and part counters + */ + public function __construct(){ + global $conf; + + $server = parse_url(DOKU_URL,PHP_URL_HOST); + + $this->partid = md5(uniqid(rand(),true)).'@'.$server; + $this->boundary = '----------'.md5(uniqid(rand(),true)); + + $listid = join('.',array_reverse(explode('/',DOKU_BASE))).$server; + $listid = strtolower(trim($listid,'.')); + + $this->allowhtml = (bool) $conf['htmlmail']; + + // add some default headers for mailfiltering FS#2247 + $this->setHeader('X-Mailer','DokuWiki '.getVersion()); + $this->setHeader('X-DokuWiki-User', $_SERVER['REMOTE_USER']); + $this->setHeader('X-DokuWiki-Title', $conf['title']); + $this->setHeader('X-DokuWiki-Server', $server); + $this->setHeader('X-Auto-Response-Suppress', 'OOF'); + $this->setHeader('List-Id',$conf['title'].' <'.$listid.'>'); + } + + /** + * Attach a file + * + * @param $path Path to the file to attach + * @param $mime Mimetype of the attached file + * @param $name The filename to use + * @param $embed Unique key to reference this file from the HTML part + */ + public function attachFile($path,$mime,$name='',$embed=''){ + if(!$name){ + $name = basename($path); + } + + $this->attach[] = array( + 'data' => file_get_contents($path), + 'mime' => $mime, + 'name' => $name, + 'embed' => $embed + ); + } + + /** + * Attach a file + * + * @param $path The file contents to attach + * @param $mime Mimetype of the attached file + * @param $name The filename to use + * @param $embed Unique key to reference this file from the HTML part + */ + public function attachContent($data,$mime,$name='',$embed=''){ + if(!$name){ + list($junk,$ext) = split('/',$mime); + $name = count($this->attach).".$ext"; + } + + $this->attach[] = array( + 'data' => $data, + 'mime' => $mime, + 'name' => $name, + 'embed' => $embed + ); + } + + /** + * Callback function to automatically embed images referenced in HTML templates + */ + protected function autoembed_cb($matches){ + static $embeds = 0; + $embeds++; + + // get file and mime type + $media = cleanID($matches[1]); + list($ext, $mime) = mimetype($media); + $file = mediaFN($media); + if(!file_exists($file)) return $matches[0]; //bad reference, keep as is + + // attach it and set placeholder + $this->attachFile($file,$mime,'','autoembed'.$embeds); + return '%%autoembed'.$embeds.'%%'; + } + + /** + * Add an arbitrary header to the mail + * + * If an empy value is passed, the header is removed + * + * @param string $header the header name (no trailing colon!) + * @param string $value the value of the header + * @param bool $clean remove all non-ASCII chars and line feeds? + */ + public function setHeader($header,$value,$clean=true){ + $header = str_replace(' ','-',ucwords(strtolower(str_replace('-',' ',$header)))); // streamline casing + if($clean){ + $header = preg_replace('/[^\w \-\.\+\@]+/','',$header); + $value = preg_replace('/[^\w \-\.\+\@<>]+/','',$value); + } + + // empty value deletes + $value = trim($value); + if($value === ''){ + if(isset($this->headers[$header])) unset($this->headers[$header]); + }else{ + $this->headers[$header] = $value; + } + } + + /** + * Set additional parameters to be passed to sendmail + * + * Whatever is set here is directly passed to PHP's mail() command as last + * parameter. Depending on the PHP setup this might break mailing alltogether + */ + public function setParameters($param){ + $this->sendparam = $param; + } + + /** + * Set the text and HTML body and apply replacements + * + * This function applies a whole bunch of default replacements in addition + * to the ones specidifed as parameters + * + * If you pass the HTML part or HTML replacements yourself you have to make + * sure you encode all HTML special chars correctly + * + * @param string $text plain text body + * @param array $textrep replacements to apply on the text part + * @param array $htmlrep replacements to apply on the HTML part, leave null to use $textrep + * @param array $html the HTML body, leave null to create it from $text + * @param bool $wrap wrap the HTML in the default header/Footer + */ + public function setBody($text, $textrep=null, $htmlrep=null, $html=null, $wrap=true){ + global $INFO; + global $conf; + $htmlrep = (array) $htmlrep; + $textrep = (array) $textrep; + + // create HTML from text if not given + if(is_null($html)){ + $html = $text; + $html = hsc($html); + $html = preg_replace('/^-----*$/m','<hr >',$html); + $html = nl2br($html); + } + if($wrap){ + $wrap = rawLocale('mailwrap','html'); + $html = preg_replace('/\n-- <br \/>.*$/s','',$html); //strip signature + $html = str_replace('@HTMLBODY@',$html,$wrap); + } + + // copy over all replacements missing for HTML (autolink URLs) + foreach($textrep as $key => $value){ + if(isset($htmlrep[$key])) continue; + if(preg_match('/^https?:\/\//i',$value)){ + $htmlrep[$key] = '<a href="'.hsc($value).'">'.hsc($value).'</a>'; + }else{ + $htmlrep[$key] = hsc($value); + } + } + + // embed media from templates + $html = preg_replace_callback('/@MEDIA\(([^\)]+)\)@/', + array($this,'autoembed_cb'),$html); + + // prepare default replacements + $ip = clientIP(); + $cip = gethostsbyaddrs($ip); + $trep = array( + 'DATE' => dformat(), + 'BROWSER' => $_SERVER['HTTP_USER_AGENT'], + 'IPADDRESS' => $ip, + 'HOSTNAME' => $cip, + 'TITLE' => $conf['title'], + 'DOKUWIKIURL' => DOKU_URL, + 'USER' => $_SERVER['REMOTE_USER'], + 'NAME' => $INFO['userinfo']['name'], + 'MAIL' => $INFO['userinfo']['mail'], + ); + $trep = array_merge($trep,(array) $textrep); + $hrep = array( + 'DATE' => '<i>'.hsc(dformat()).'</i>', + 'BROWSER' => hsc($_SERVER['HTTP_USER_AGENT']), + 'IPADDRESS' => '<code>'.hsc($ip).'</code>', + 'HOSTNAME' => '<code>'.hsc($cip).'</code>', + 'TITLE' => hsc($conf['title']), + 'DOKUWIKIURL' => '<a href="'.DOKU_URL.'">'.DOKU_URL.'</a>', + 'USER' => hsc($_SERVER['REMOTE_USER']), + 'NAME' => hsc($INFO['userinfo']['name']), + 'MAIL' => '<a href="mailto:"'.hsc($INFO['userinfo']['mail']).'">'. + hsc($INFO['userinfo']['mail']).'</a>', + ); + $hrep = array_merge($hrep,(array) $htmlrep); + + // Apply replacements + foreach ($trep as $key => $substitution) { + $text = str_replace('@'.strtoupper($key).'@',$substitution, $text); + } + foreach ($hrep as $key => $substitution) { + $html = str_replace('@'.strtoupper($key).'@',$substitution, $html); + } + + $this->setHTML($html); + $this->setText($text); + } + + /** + * Set the HTML part of the mail + * + * Placeholders can be used to reference embedded attachments + * + * You probably want to use setBody() instead + */ + public function setHTML($html){ + $this->html = $html; + } + + /** + * Set the plain text part of the mail + * + * You probably want to use setBody() instead + */ + public function setText($text){ + $this->text = $text; + } + + /** + * Add the To: recipients + * + * @see setAddress + * @param string $address Multiple adresses separated by commas + */ + public function to($address){ + $this->setHeader('To', $address, false); + } + + /** + * Add the Cc: recipients + * + * @see setAddress + * @param string $address Multiple adresses separated by commas + */ + public function cc($address){ + $this->setHeader('Cc', $address, false); + } + + /** + * Add the Bcc: recipients + * + * @see setAddress + * @param string $address Multiple adresses separated by commas + */ + public function bcc($address){ + $this->setHeader('Bcc', $address, false); + } + + /** + * Add the From: address + * + * This is set to $conf['mailfrom'] when not specified so you shouldn't need + * to call this function + * + * @see setAddress + * @param string $address from address + */ + public function from($address){ + $this->setHeader('From', $address, false); + } + + /** + * Add the mail's Subject: header + * + * @param string $subject the mail subject + */ + public function subject($subject){ + $this->headers['Subject'] = $subject; + } + + /** + * Sets an email address header with correct encoding + * + * Unicode characters will be deaccented and encoded base64 + * for headers. Addresses may not contain Non-ASCII data! + * + * Example: + * setAddress("föö <foo@bar.com>, me@somewhere.com","TBcc"); + * + * @param string $address Multiple adresses separated by commas + * @param string returns the prepared header (can contain multiple lines) + */ + public function cleanAddress($address){ + // No named recipients for To: in Windows (see FS#652) + $names = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? false : true; + + $address = preg_replace('/[\r\n\0]+/',' ',$address); // remove attack vectors + + $headers = ''; + $parts = explode(',',$address); + foreach ($parts as $part){ + $part = trim($part); + + // parse address + if(preg_match('#(.*?)<(.*?)>#',$part,$matches)){ + $text = trim($matches[1]); + $addr = $matches[2]; + }else{ + $addr = $part; + } + // skip empty ones + if(empty($addr)){ + continue; + } + + // FIXME: is there a way to encode the localpart of a emailaddress? + if(!utf8_isASCII($addr)){ + msg(htmlspecialchars("E-Mail address <$addr> is not ASCII"),-1); + continue; + } + + if(is_null($this->validator)){ + $this->validator = new EmailAddressValidator(); + $this->validator->allowLocalAddresses = true; + } + if(!$this->validator->check_email_address($addr)){ + msg(htmlspecialchars("E-Mail address <$addr> is not valid"),-1); + continue; + } + + // text was given + if(!empty($text) && $names){ + // add address quotes + $addr = "<$addr>"; + + if(defined('MAILHEADER_ASCIIONLY')){ + $text = utf8_deaccent($text); + $text = utf8_strip($text); + } + + if(!utf8_isASCII($text)){ + $text = '=?UTF-8?B?'.base64_encode($text).'?='; + } + }else{ + $text = ''; + } + + // add to header comma seperated + if($headers != ''){ + $headers .= ', '; + } + $headers .= $text.' '.$addr; + } + + if(empty($headers)) return false; + + return $headers; + } + + + /** + * Prepare the mime multiparts for all attachments + * + * Replaces placeholders in the HTML with the correct CIDs + */ + protected function prepareAttachments(){ + $mime = ''; + $part = 1; + // embedded attachments + foreach($this->attach as $media){ + // create content id + $cid = 'part'.$part.'.'.$this->partid; + + // replace wildcards + if($media['embed']){ + $this->html = str_replace('%%'.$media['embed'].'%%','cid:'.$cid,$this->html); + } + + $mime .= '--'.$this->boundary.MAILHEADER_EOL; + $mime .= 'Content-Type: '.$media['mime'].';'.MAILHEADER_EOL; + $mime .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; + $mime .= "Content-ID: <$cid>".MAILHEADER_EOL; + if($media['embed']){ + $mime .= 'Content-Disposition: inline; filename="'.$media['name'].'"'.MAILHEADER_EOL; + }else{ + $mime .= 'Content-Disposition: attachment; filename="'.$media['name'].'"'.MAILHEADER_EOL; + } + $mime .= MAILHEADER_EOL; //end of headers + $mime .= chunk_split(base64_encode($media['data']),74,MAILHEADER_EOL); + + $part++; + } + return $mime; + } + + /** + * Build the body and handles multi part mails + * + * Needs to be called before prepareHeaders! + * + * @return string the prepared mail body, false on errors + */ + protected function prepareBody(){ + global $conf; + + // no HTML mails allowed? remove HTML body + if(!$this->allowhtml){ + $this->html = ''; + } + + // check for body + if(!$this->text && !$this->html){ + return false; + } + + // add general headers + $this->headers['MIME-Version'] = '1.0'; + + $body = ''; + + if(!$this->html && !count($this->attach)){ // we can send a simple single part message + $this->headers['Content-Type'] = 'text/plain; charset=UTF-8'; + $this->headers['Content-Transfer-Encoding'] = 'base64'; + $body .= chunk_split(base64_encode($this->text),74,MAILHEADER_EOL); + }else{ // multi part it is + $body .= "This is a multi-part message in MIME format.".MAILHEADER_EOL; + + // prepare the attachments + $attachments = $this->prepareAttachments(); + + // do we have alternative text content? + if($this->text && $this->html){ + $this->headers['Content-Type'] = 'multipart/alternative;'.MAILHEADER_EOL. + ' boundary="'.$this->boundary.'XX"'; + $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; + $body .= 'Content-Type: text/plain; charset=UTF-8'.MAILHEADER_EOL; + $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; + $body .= MAILHEADER_EOL; + $body .= chunk_split(base64_encode($this->text),74,MAILHEADER_EOL); + $body .= '--'.$this->boundary.'XX'.MAILHEADER_EOL; + $body .= 'Content-Type: multipart/related;'.MAILHEADER_EOL. + ' boundary="'.$this->boundary.'"'.MAILHEADER_EOL; + $body .= MAILHEADER_EOL; + } + + $body .= '--'.$this->boundary.MAILHEADER_EOL; + $body .= 'Content-Type: text/html; charset=UTF-8'.MAILHEADER_EOL; + $body .= 'Content-Transfer-Encoding: base64'.MAILHEADER_EOL; + $body .= MAILHEADER_EOL; + $body .= chunk_split(base64_encode($this->html),74,MAILHEADER_EOL); + $body .= MAILHEADER_EOL; + $body .= $attachments; + $body .= '--'.$this->boundary.'--'.MAILHEADER_EOL; + + // close open multipart/alternative boundary + if($this->text && $this->html){ + $body .= '--'.$this->boundary.'XX--'.MAILHEADER_EOL; + } + } + + return $body; + } + + /** + * Cleanup and encode the headers array + */ + protected function cleanHeaders(){ + global $conf; + + // clean up addresses + if(empty($this->headers['From'])) $this->from($conf['mailfrom']); + $addrs = array('To','From','Cc','Bcc'); + foreach($addrs as $addr){ + if(isset($this->headers[$addr])){ + $this->headers[$addr] = $this->cleanAddress($this->headers[$addr]); + } + } + + if(isset($this->headers['Subject'])){ + // add prefix to subject + if(empty($conf['mailprefix'])){ + if(utf8_strlen($conf['title']) < 20) { + $prefix = '['.$conf['title'].']'; + }else{ + $prefix = '['.utf8_substr($conf['title'], 0, 20).'...]'; + } + }else{ + $prefix = '['.$conf['mailprefix'].']'; + } + $len = strlen($prefix); + if(substr($this->headers['Subject'],0,$len) != $prefix){ + $this->headers['Subject'] = $prefix.' '.$this->headers['Subject']; + } + + // encode subject + if(defined('MAILHEADER_ASCIIONLY')){ + $this->headers['Subject'] = utf8_deaccent($this->headers['Subject']); + $this->headers['Subject'] = utf8_strip($this->headers['Subject']); + } + if(!utf8_isASCII($this->headers['Subject'])){ + $this->headers['Subject'] = '=?UTF-8?B?'.base64_encode($this->headers['Subject']).'?='; + } + } + + // wrap headers + foreach($this->headers as $key => $val){ + $this->headers[$key] = wordwrap($val,78,MAILHEADER_EOL.' '); + } + } + + /** + * Create a string from the headers array + * + * @returns string the headers + */ + protected function prepareHeaders(){ + $headers = ''; + foreach($this->headers as $key => $val){ + $headers .= "$key: $val".MAILHEADER_EOL; + } + return $headers; + } + + /** + * return a full email with all headers + * + * This is mainly intended for debugging and testing but could also be + * used for MHT exports + * + * @return string the mail, false on errors + */ + public function dump(){ + $this->cleanHeaders(); + $body = $this->prepareBody(); + if($body === 'false') return false; + $headers = $this->prepareHeaders(); + + return $headers.MAILHEADER_EOL.$body; + } + + /** + * Send the mail + * + * Call this after all data was set + * + * @triggers MAIL_MESSAGE_SEND + * @return bool true if the mail was successfully passed to the MTA + */ + public function send(){ + $success = false; + + // prepare hook data + $data = array( + // pass the whole mail class to plugin + 'mail' => $this, + // pass references for backward compatibility + 'to' => &$this->headers['To'], + 'cc' => &$this->headers['Cc'], + 'bcc' => &$this->headers['Bcc'], + 'from' => &$this->headers['From'], + 'subject' => &$this->headers['Subject'], + 'body' => &$this->text, + 'params' => &$this->sendparams, + 'headers' => '', // plugins shouldn't use this + // signal if we mailed successfully to AFTER event + 'success' => &$success, + ); + + // do our thing if BEFORE hook approves + $evt = new Doku_Event('MAIL_MESSAGE_SEND', $data); + if ($evt->advise_before(true)) { + // clean up before using the headers + $this->cleanHeaders(); + + // any recipients? + if(trim($this->headers['To']) === '' && + trim($this->headers['Cc']) === '' && + trim($this->headers['Bcc']) === '') return false; + + // The To: header is special + if(isset($this->headers['To'])){ + $to = $this->headers['To']; + unset($this->headers['To']); + }else{ + $to = ''; + } + + // so is the subject + if(isset($this->headers['Subject'])){ + $subject = $this->headers['Subject']; + unset($this->headers['Subject']); + }else{ + $subject = ''; + } + + // make the body + $body = $this->prepareBody(); + if($body === 'false') return false; + + // cook the headers + $headers = $this->prepareHeaders(); + // add any headers set by legacy plugins + if(trim($data['headers'])){ + $headers .= MAILHEADER_EOL.trim($data['headers']); + } + + // send the thing + if(is_null($this->sendparam)){ + $success = @mail($to,$subject,$body,$headers); + }else{ + $success = @mail($to,$subject,$body,$headers,$this->sendparam); + } + } + // any AFTER actions? + $evt->advise_after(); + return $success; + } +} diff --git a/inc/auth.php b/inc/auth.php index 59ef1cb54decae1e7437d7de13ee921f3318a057..ed0e2dcf7da351d397c7e8d239fb2064db22b685 100644 --- a/inc/auth.php +++ b/inc/auth.php @@ -669,22 +669,17 @@ function auth_sendPassword($user,$password){ if(!$userinfo['mail']) return false; $text = rawLocale('password'); - $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); - $text = str_replace('@FULLNAME@',$userinfo['name'],$text); - $text = str_replace('@LOGIN@',$user,$text); - $text = str_replace('@PASSWORD@',$password,$text); - $text = str_replace('@TITLE@',$conf['title'],$text); - - if(empty($conf['mailprefix'])) { - $subject = $lang['regpwmail']; - } else { - $subject = '['.$conf['mailprefix'].'] '.$lang['regpwmail']; - } + $trep = array( + 'FULLNAME' => $userinfo['name'], + 'LOGIN' => $user, + 'PASSWORD' => $password + ); - return mail_send($userinfo['name'].' <'.$userinfo['mail'].'>', - $subject, - $text, - $conf['mailfrom']); + $mail = new Mailer(); + $mail->to($userinfo['name'].' <'.$userinfo['mail'].'>'); + $mail->subject($lang['regpwmail']); + $mail->setBody($text,$trep); + return $mail->send(); } /** @@ -941,22 +936,17 @@ function act_resendpwd(){ io_saveFile($tfile,$user); $text = rawLocale('pwconfirm'); - $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); - $text = str_replace('@FULLNAME@',$userinfo['name'],$text); - $text = str_replace('@LOGIN@',$user,$text); - $text = str_replace('@TITLE@',$conf['title'],$text); - $text = str_replace('@CONFIRM@',$url,$text); - - if(empty($conf['mailprefix'])) { - $subject = $lang['regpwmail']; - } else { - $subject = '['.$conf['mailprefix'].'] '.$lang['regpwmail']; - } + $trep = array( + 'FULLNAME' => $userinfo['name'], + 'LOGIN' => $user, + 'CONFIRM' => $url + ); - if(mail_send($userinfo['name'].' <'.$userinfo['mail'].'>', - $subject, - $text, - $conf['mailfrom'])){ + $mail = new Mailer(); + $mail->to($userinfo['name'].' <'.$userinfo['mail'].'>'); + $mail->subject($lang['regpwmail']); + $mail->setBody($text,$trep); + if($mail->send()){ msg($lang['resendpwdconfirm'],1); }else{ msg($lang['regmailfail'],-1); diff --git a/inc/common.php b/inc/common.php index 0a75f2eab4e8f9317759fe9bf25817c9b15274e2..22a315901591d5e2153331b5d4f74f63b2805fc3 100644 --- a/inc/common.php +++ b/inc/common.php @@ -789,8 +789,8 @@ function formText($text){ * * @author Andreas Gohr <andi@splitbrain.org> */ -function rawLocale($id){ - return io_readFile(localeFN($id)); +function rawLocale($id,$ext='txt'){ + return io_readFile(localeFN($id,$ext)); } /** @@ -1086,8 +1086,9 @@ function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){ global $lang; global $conf; global $INFO; + global $DIFF_INLINESTYLES; - // decide if there is something to do + // decide if there is something to do, eg. whom to mail if($who == 'admin'){ if(empty($conf['notify'])) return; //notify enabled? $text = rawLocale('mailtext'); @@ -1112,49 +1113,54 @@ function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){ return; //just to be safe } - $ip = clientIP(); - $text = str_replace('@DATE@',dformat(),$text); - $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text); - $text = str_replace('@IPADDRESS@',$ip,$text); - $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text); - $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text); - $text = str_replace('@PAGE@',$id,$text); - $text = str_replace('@TITLE@',$conf['title'],$text); - $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); - $text = str_replace('@SUMMARY@',$summary,$text); - $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text); - $text = str_replace('@NAME@',$INFO['userinfo']['name'],$text); - $text = str_replace('@MAIL@',$INFO['userinfo']['mail'],$text); - - foreach ($replace as $key => $substitution) { - $text = str_replace('@'.strtoupper($key).'@',$substitution, $text); - } + // prepare replacements (keys not set in hrep will be taken from trep) + $trep = array( + 'NEWPAGE' => wl($id,'',true,'&'), + 'PAGE' => $id, + 'SUMMARY' => $summary + ); + $trep = array_merge($trep,$replace); + $hrep = array(); + // prepare content if($who == 'register'){ - $subject = $lang['mail_new_user'].' '.$summary; + $subject = $lang['mail_new_user'].' '.$summary; }elseif($rev){ - $subject = $lang['mail_changed'].' '.$id; - $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text); - $df = new Diff(explode("\n",rawWiki($id,$rev)), - explode("\n",rawWiki($id))); - $dformat = new UnifiedDiffFormatter(); - $diff = $dformat->format($df); + $subject = $lang['mail_changed'].' '.$id; + $trep['OLDPAGE'] = wl($id,"rev=$rev",true,'&'); + $df = new Diff(explode("\n",rawWiki($id,$rev)), + explode("\n",rawWiki($id))); + $dformat = new UnifiedDiffFormatter(); + $tdiff = $dformat->format($df); + + $DIFF_INLINESTYLES = true; + $dformat = new InlineDiffFormatter(); + $hdiff = $dformat->format($df); + $hdiff = '<table>'.$hdiff.'</table>'; + $DIFF_INLINESTYLES = false; }else{ - $subject=$lang['mail_newpage'].' '.$id; - $text = str_replace('@OLDPAGE@','none',$text); - $diff = rawWiki($id); - } - $text = str_replace('@DIFF@',$diff,$text); - if(empty($conf['mailprefix'])) { - if(utf8_strlen($conf['title']) < 20) { - $subject = '['.$conf['title'].'] '.$subject; - }else{ - $subject = '['.utf8_substr($conf['title'], 0, 20).'...] '.$subject; - } - }else{ - $subject = '['.$conf['mailprefix'].'] '.$subject; - } - mail_send($to,$subject,$text,$conf['mailfrom'],'',$bcc); + $subject = $lang['mail_newpage'].' '.$id; + $trep['OLDPAGE'] = '---'; + $tdiff = rawWiki($id); + $hdiff = nl2br(hsc($tdiff)); + } + $trep['DIFF'] = $tdiff; + $hrep['DIFF'] = $hdiff; + + // send mail + $mail = new Mailer(); + $mail->to($to); + $mail->bcc($bcc); + $mail->subject($subject); + $mail->setBody($text,$trep,$hrep); + if($who == 'subscribers'){ + $mail->setHeader( + 'List-Unsubscribe', + '<'.wl($id,array('do'=>'subscribe'),true,'&').'>', + false + ); + } + return $mail->send(); } /** diff --git a/inc/lang/en/mailwrap.html b/inc/lang/en/mailwrap.html new file mode 100644 index 0000000000000000000000000000000000000000..f9f80fd8083dff21e22b828a0d79463d58c0c68e --- /dev/null +++ b/inc/lang/en/mailwrap.html @@ -0,0 +1,13 @@ +<html> +<head> + <title>@TITLE@</title> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +</head> +<body> + +@HTMLBODY@ + +<br /><hr /> +<small>This mail was generated by DokuWiki at @DOKUWIKIURL@.</small> +</body> +</html> diff --git a/inc/lang/en/subscr_digest.txt b/inc/lang/en/subscr_digest.txt index fac8564bd84a110a8024f82b037693113f1ee3ec..35011b6e61d76b5341a8a73ea5484099dd26b7a6 100644 --- a/inc/lang/en/subscr_digest.txt +++ b/inc/lang/en/subscr_digest.txt @@ -15,6 +15,6 @@ To cancel the page notifications, log into the wiki at @SUBSCRIBE@ and unsubscribe page and/or namespace changes. --- +-- This mail was generated by DokuWiki at @DOKUWIKIURL@ diff --git a/inc/lang/en/subscr_list.txt b/inc/lang/en/subscr_list.txt index efe27d866ec0f758a63cf513d78471e5d323b551..4c38b9326e467f6e1b2bca46711f99034b07d70c 100644 --- a/inc/lang/en/subscr_list.txt +++ b/inc/lang/en/subscr_list.txt @@ -12,6 +12,6 @@ To cancel the page notifications, log into the wiki at @SUBSCRIBE@ and unsubscribe page and/or namespace changes. --- +-- This mail was generated by DokuWiki at @DOKUWIKIURL@ diff --git a/inc/lang/en/subscr_single.txt b/inc/lang/en/subscr_single.txt index f2abe6d77e608508cbdfb3ce53bae4846380e30e..673c4c32adbad55f475f056c2cd385fb3c998c6c 100644 --- a/inc/lang/en/subscr_single.txt +++ b/inc/lang/en/subscr_single.txt @@ -18,6 +18,6 @@ To cancel the page notifications, log into the wiki at @NEWPAGE@ and unsubscribe page and/or namespace changes. --- +-- This mail was generated by DokuWiki at @DOKUWIKIURL@ diff --git a/inc/load.php b/inc/load.php index f3ab5bcdd8ab03fe527588da9c15fd0689529e1e..0572b5760f05dcedf31f65b1797871175226f6b7 100644 --- a/inc/load.php +++ b/inc/load.php @@ -76,8 +76,9 @@ function load_autoload($name){ 'SafeFN' => DOKU_INC.'inc/SafeFN.class.php', 'Sitemapper' => DOKU_INC.'inc/Sitemapper.php', 'PassHash' => DOKU_INC.'inc/PassHash.class.php', + 'Mailer' => DOKU_INC.'inc/Mailer.class.php', 'RemoteAPI' => DOKU_INC.'inc/remote.php', - 'RemoteAPICore' => DOKU_INC.'inc/RemoteAPICore.php', + 'RemoteAPICore' => DOKU_INC.'inc/RemoteAPICore.php', 'DokuWiki_Action_Plugin' => DOKU_PLUGIN.'action.php', 'DokuWiki_Admin_Plugin' => DOKU_PLUGIN.'admin.php', diff --git a/inc/media.php b/inc/media.php index dd0193fa07a9ecd91cb9b2b355a3c5f9601c91bd..66984e957d4c14bbb2851ed1e4dfcfeae0998f38 100644 --- a/inc/media.php +++ b/inc/media.php @@ -516,6 +516,7 @@ function media_contentcheck($file,$mime){ * Send a notify mail on uploads * * @author Andreas Gohr <andi@splitbrain.org> + * @fixme this should embed thumbnails of images in HTML version */ function media_notify($id,$file,$mime,$old_rev=false){ global $lang; @@ -523,31 +524,24 @@ function media_notify($id,$file,$mime,$old_rev=false){ global $INFO; if(empty($conf['notify'])) return; //notify enabled? - $ip = clientIP(); - $text = rawLocale('uploadmail'); - $text = str_replace('@DATE@',dformat(),$text); - $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text); - $text = str_replace('@IPADDRESS@',$ip,$text); - $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text); - $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); - $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text); - $text = str_replace('@MIME@',$mime,$text); - $text = str_replace('@MEDIA@',ml($id,'',true,'&',true),$text); - $text = str_replace('@SIZE@',filesize_h(filesize($file)),$text); - if ($old_rev && $conf['mediarevisions']) { - $text = str_replace('@OLD@', ml($id, "rev=$old_rev", true, '&', true), $text); - } else { - $text = str_replace('@OLD@', '', $text); - } + $trep = array( + 'MIME' => $mime, + 'MEDIA' => ml($id,'',true,'&',true), + 'SIZE' => filesize_h(filesize($file)), + ); - if(empty($conf['mailprefix'])) { - $subject = '['.$conf['title'].'] '.$lang['mail_upload'].' '.$id; + if ($old_rev && $conf['mediarevisions']) { + $trep['OLD'] = ml($id, "rev=$old_rev", true, '&', true); } else { - $subject = '['.$conf['mailprefix'].'] '.$lang['mail_upload'].' '.$id; + $trep['OLD'] = '---'; } - mail_send($conf['notify'],$subject,$text,$conf['mailfrom']); + $mail = new Mailer(); + $mail->to($conf['notify']); + $mail->subject($lang['mail_upload'].' '.$id); + $mail->setBody($text,$trep); + return $mail->send(); } /** diff --git a/inc/pageutils.php b/inc/pageutils.php index db00258e2dc3f1122ee44f69d8960abc1508b87b..c94d14624857c7d0fe3166fbc43640b4ad01e22a 100644 --- a/inc/pageutils.php +++ b/inc/pageutils.php @@ -355,19 +355,21 @@ function mediaFN($id, $rev=''){ } /** - * Returns the full filepath to a localized textfile if local + * Returns the full filepath to a localized file if local * version isn't found the english one is returned * + * @param string $id The id of the local file + * @param string $ext The file extension (usually txt) * @author Andreas Gohr <andi@splitbrain.org> */ -function localeFN($id){ +function localeFN($id,$ext='txt'){ global $conf; - $file = DOKU_CONF.'/lang/'.$conf['lang'].'/'.$id.'.txt'; + $file = DOKU_CONF.'/lang/'.$conf['lang'].'/'.$id.'.'.$ext; if(!@file_exists($file)){ - $file = DOKU_INC.'inc/lang/'.$conf['lang'].'/'.$id.'.txt'; + $file = DOKU_INC.'inc/lang/'.$conf['lang'].'/'.$id.'.'.$ext; if(!@file_exists($file)){ //fall back to english - $file = DOKU_INC.'inc/lang/en/'.$id.'.txt'; + $file = DOKU_INC.'inc/lang/en/'.$id.'.'.$ext; } } return $file; diff --git a/inc/subscription.php b/inc/subscription.php index c94f17ad0e57954379240e2382b06d371402663a..d1ee0397adc6b67c9cfe499f65cb19f9b24f45ef 100644 --- a/inc/subscription.php +++ b/inc/subscription.php @@ -377,18 +377,20 @@ function subscription_send_list($subscriber_mail, $ids, $ns_id) { */ function subscription_send($subscriber_mail, $replaces, $subject, $id, $template) { global $conf; + global $lang; $text = rawLocale($template); - $replaces = array_merge($replaces, array('TITLE' => $conf['title'], - 'DOKUWIKIURL' => DOKU_URL, - 'PAGE' => $id)); - - foreach ($replaces as $key => $substitution) { - $text = str_replace('@'.strtoupper($key).'@', $substitution, $text); - } + $trep = array_merge($replaces, array('PAGE' => $id)); - global $lang; $subject = $lang['mail_' . $subject] . ' ' . $id; - mail_send('', '['.$conf['title'].'] '. $subject, $text, - $conf['mailfrom'], '', $subscriber_mail); + $mail = new Mailer(); + $mail->bcc($subscriber_mail); + $mail->subject($subject); + $mail->setBody($text,$trep); + $mail->setHeader( + 'List-Unsubscribe', + '<'.wl($id,array('do'=>'subscribe'),true,'&').'>', + false + ); + return $mail->send(); } diff --git a/lib/plugins/config/lang/en/lang.php b/lib/plugins/config/lang/en/lang.php index 42f34de74a9f104875ec2bf6509dd845de241c43..d588e02f86d9c85190afed99544f6a71d08b500d 100644 --- a/lib/plugins/config/lang/en/lang.php +++ b/lib/plugins/config/lang/en/lang.php @@ -147,6 +147,7 @@ $lang['notify'] = 'Always send change notifications to this email address'; $lang['registernotify'] = 'Always send info on newly registered users to this email address'; $lang['mailfrom'] = 'Sender email address to use for automatic mails'; $lang['mailprefix'] = 'Email subject prefix to use for automatic mails. Leave blank to use the wiki title'; +$lang['htmlmail'] = 'Send better looking, but larger in size HTML multipart emails. Disable for plain text only mails.'; /* Syndication Settings */ $lang['sitemap'] = 'Generate Google sitemap this often (in days). 0 to disable'; diff --git a/lib/plugins/config/settings/config.metadata.php b/lib/plugins/config/settings/config.metadata.php index e610ac4d47d877863ad9dc971c30ec1d73d508d1..997495b2e698a2744567d172b313a0705f0233e4 100644 --- a/lib/plugins/config/settings/config.metadata.php +++ b/lib/plugins/config/settings/config.metadata.php @@ -177,6 +177,7 @@ $meta['notify'] = array('email', '_multiple' => true); $meta['registernotify'] = array('email'); $meta['mailfrom'] = array('richemail'); $meta['mailprefix'] = array('string'); +$meta['htmlmail'] = array('onoff'); $meta['_syndication'] = array('fieldset'); $meta['sitemap'] = array('numeric');