Skip to content
Snippets Groups Projects
Unverified Commit c9fa8626 authored by Jørn Åne's avatar Jørn Åne Committed by GitHub
Browse files

Replace e-mail class with one that uses PHPMailer

The old mail class uses the built-in `mail` function in PHP, and builds a payload by itself using hard-coded HTML and MIME-snippets. Let's use a library for that; PHPMailer has new dependencies and does all the heavy lifting.
parent 4fdb5a3c
No related branches found
No related tags found
No related merge requests found
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "225bbfa4f3bdc5cac6a7943c5a705be5", "content-hash": "b8847137c81850b370dc6f05691d31db",
"packages": [ "packages": [
{ {
"name": "gettext/gettext", "name": "gettext/gettext",
...@@ -222,6 +222,72 @@ ...@@ -222,6 +222,72 @@
], ],
"time": "2019-01-03T20:59:08+00:00" "time": "2019-01-03T20:59:08+00:00"
}, },
{
"name": "phpmailer/phpmailer",
"version": "v6.0.7",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "0c41a36d4508d470e376498c1c0c527aa36a2d59"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/0c41a36d4508d470e376498c1c0c527aa36a2d59",
"reference": "0c41a36d4508d470e376498c1c0c527aa36a2d59",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"php": ">=5.5.0"
},
"require-dev": {
"doctrine/annotations": "1.2.*",
"friendsofphp/php-cs-fixer": "^2.2",
"phpdocumentor/phpdocumentor": "2.*",
"phpunit/phpunit": "^4.8 || ^5.7",
"zendframework/zend-eventmanager": "3.0.*",
"zendframework/zend-i18n": "2.7.3",
"zendframework/zend-serializer": "2.7.*"
},
"suggest": {
"ext-mbstring": "Needed to send email in multibyte encoding charset",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"time": "2019-02-01T15:04:28+00:00"
},
{ {
"name": "psr/container", "name": "psr/container",
"version": "1.0.0", "version": "1.0.0",
......
<?php
namespace SimpleSAML\Utils;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use SimpleSAML\Configuration;
use SimpleSAML\Logger;
use SimpleSAML\XHTML\Template;
/**
* E-mailer class that can generate a formatted e-mail from array
* input data.
*
* @author Jørn Åne de Jong, Uninett AS <jorn.dejong@uninett.no>
* @package SimpleSAMLphp
*/
class EMail
{
/**
* Get the default e-mail address from the configuration
* This is used both as source and destination address
* unless something else is provided at the constructor.
*
* It will refuse to return the SimpleSAMLphp default address,
* which is na@example.org.
*
* @return string Default mail address
*/
public static function getDefaultMailAddress()
{
$config = Configuration::getInstance();
$address = $config->getString('technicalcontact_email', 'na@example.org');
if ('na@example.org' === $address) {
throw new \Exception('technicalcontact_email must be changed from the default value');
}
return $address;
}
/** @var array Dictionary with multivalues */
private $data;
/** @var string Introduction text */
private $text;
/** @var PHPMailer The mailer instance */
private $mail;
/**
* Constructor
*
* If $from or $to is not provided, the <code>technicalcontact_email</code>
* from the configuration is used.
*
* @param string $subject The subject of the e-mail
* @param string $from The from-address (both envelope and header)
* @param string $to The recipient
*
* @throws PHPMailer\PHPMailer\Exception
*/
public function __construct($subject, $from = null, $to = null)
{
$this->mail = new PHPMailer(true);
$this->mail->Subject = $subject;
$this->mail->setFrom($from ?: static::getDefaultMailAddress());
$this->mail->addAddress($to ?: static::getDefaultMailAddress());
}
/**
* Set the data that should be embedded in the e-mail body
*
* @param array $data The data that should be embedded in the e-mail body
*/
public function setData(array $data)
{
/*
* Convert every non-array value to an array with the original
* as its only element. This guarantees that every value of $data
* can be iterated over.
*/
$this->data = array_map(function($v){return is_array($v) ? $v : [$v];}, $data);
}
/**
* Set an introduction text for the e-mail
*
* @param string $text Introduction text
*/
public function setText($text)
{
$this->text = $text;
}
/**
* Add a Reply-To address to the mail
*
* @param string $address Reply-To e-mail address
*/
public function addReplyTo($address)
{
$this->mail->addReplyTo($address);
}
/**
* Send the mail
*
* @param bool $plainTextOnly Do not send HTML payload
*
* @throws \PHPMailer\PHPMailer\Exception
*/
public function send($plainTextOnly = false)
{
if ($plainTextOnly) {
$this->mail->isHTML(false);
$this->mail->Body = $this->generateBody('mailtxt.twig');
} else {
$this->mail->isHTML(true);
$this->mail->Body = $this->generateBody('mailhtml.twig');
$this->mail->AltBody = $this->generateBody('mailtxt.twig');
}
$this->mail->send();
}
/**
* Generate the body of the e-mail
*
* @param string $template The name of the template to use
*
* @return string The body of the e-mail
*/
public function generateBody($template)
{
$config = Configuration::loadFromArray([
'usenewui' => true,
]);
$t = new Template($config, $template);
$twig = $t->getTwig();
if (is_bool($twig)) {
throw new \Exception('Even though we explicitly configure that we want Twig, the Template class does not give us Twig. This is a bug.');
}
$result = $twig->render($template, [
'subject' => $this->mail->Subject,
'text' => $this->text,
'data' => $this->data
]);
return $result;
}
}
<?php
namespace SimpleSAML\XHTML;
/**
* A minimalistic Emailer class. Creates and sends HTML emails.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class EMail
{
/** @var string|null */
private $to = null;
/** @var string|null */
private $cc = null;
/** @var string|null */
private $body = null;
/** @var string|null */
private $from = null;
/** @var string|null */
private $replyto = null;
/** @var string|null */
private $subject = null;
/** @var array */
private $headers = [];
/**
* Constructor
*
* @param string $to
* @param string $subject
* @param string|null $from
* @param string|null $cc
* @param string|null $replyto
*/
public function __construct($to, $subject, $from = null, $cc = null, $replyto = null)
{
$this->to = $to;
$this->cc = $cc;
$this->from = $from;
$this->replyto = $replyto;
$this->subject = $subject;
}
/**
* @param string $body
* @return void
*/
public function setBody($body)
{
$this->body = $body;
}
/**
* @param string $body
* @return string
*/
private function getHTML($body)
{
$config = \SimpleSAML\Configuration::getInstance();
$t = new \SimpleSAML\XHTML\Template($config, 'errorreport_mail.twig');
$twig = $t->getTwig();
return $twig->render('errorreport_mail.twig', ['body' => $body]);
}
/**
* @return void
*/
public function send()
{
if ($this->to === null) {
throw new \Exception('EMail field [to] is required and not set.');
} elseif ($this->subject === null) {
throw new \Exception('EMail field [subject] is required and not set.');
} elseif ($this->body === null) {
throw new \Exception('EMail field [body] is required and not set.');
}
$random_hash = bin2hex(openssl_random_pseudo_bytes(16));
if (isset($this->from)) {
$this->headers[] = 'From: '.$this->from;
}
if (isset($this->replyto)) {
$this->headers[] = 'Reply-To: '.$this->replyto;
}
$this->headers[] = 'Content-Type: multipart/alternative; boundary="simplesamlphp-'.$random_hash.'"';
$message = '
--simplesamlphp-'.$random_hash.'
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit
'.strip_tags(html_entity_decode($this->body)).'
--simplesamlphp-'.$random_hash.'
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 8bit
'.$this->getHTML($this->body).'
--simplesamlphp-'.$random_hash.'--
';
$headers = implode("\n", $this->headers);
$mail_sent = @mail($this->to, $this->subject, $message, $headers);
\SimpleSAML\Logger::debug('Email: Sending e-mail to ['.$this->to.'] : '.($mail_sent ? 'OK' : 'Failed'));
if (!$mail_sent) {
throw new \Exception('Error when sending e-mail');
}
}
}
...@@ -23,20 +23,9 @@ $croninfo = $cron->runTag($_REQUEST['tag']); ...@@ -23,20 +23,9 @@ $croninfo = $cron->runTag($_REQUEST['tag']);
$summary = $croninfo['summary']; $summary = $croninfo['summary'];
if ($cronconfig->getValue('sendemail', true) && count($summary) > 0) { if ($cronconfig->getValue('sendemail', true) && count($summary) > 0) {
$message = '<h1>Cron report</h1><p>Cron ran at '.$time.'</p>'. $mail = new \SimpleSAML\Utils\EMail('SimpleSAMLphp cron report');
'<p>URL: <code>'.$url.'</code></p>'. $mail->setData(['url' => $url, 'tag' => $croninfo['tag'], 'summary' => $croninfo['summary']]);
'<p>Tag: '.$croninfo['tag']."</p>\n\n". $mail->send();
'<ul><li>'.join('</li><li>', $summary).'</li></ul>';
$toaddress = $config->getString('technicalcontact_email', 'na@example.org');
if ($toaddress == 'na@example.org') {
\SimpleSAML\Logger::error('Cron - Could not send email. [technicalcontact_email] not set in config.');
} else {
// Use $toaddress for both TO and FROM
$email = new \SimpleSAML\XHTML\EMail($toaddress, 'SimpleSAMLphp cron report', $toaddress);
$email->setBody($message);
$email->send();
}
} }
if (isset($_REQUEST['output']) && $_REQUEST['output'] == "xhtml") { if (isset($_REQUEST['output']) && $_REQUEST['output'] == "xhtml") {
......
<style type="text/css">
body {
font-family: sans-serif;
}
ul {
list-style: none;
padding-left: 1em;
}
p {
white-space: pre-line;
}
</style>
<h1>{{ subject }}</h1>
<p>{{ text }}</p>
{% for name, values in data %}
<h2>{{ name }}</h2>
<ul>
{% for value in values %}
<li><pre>{{ value }}</pre></li>
{% endfor %}
</ul>
{% endfor %}
{% autoescape false %}
# {{ subject }}
{{ text }}
{% for name, values in data %}
## {{ name }}
{% for value in values %}
- {{ value }}
{% endfor %}
{% endfor %}
{% endautoescape %}
<?php
namespace SimpleSAML\Test\Utils;
use SimpleSAML\Test\Utils\TestCase;
use SimpleSAML\Configuration;
use SimpleSAML\Utils\EMail;
/**
* A base SSP test case that tests some simple e-mail related calls
*/
class EMailTestCase extends ClearStateTestCase
{
public function setUp()
{
parent::setUp();
// Override configuration
Configuration::loadFromArray([
'technicalcontact_email' => 'na@example.org',
], '[ARRAY]', 'simplesaml');
}
/**
* Test that an exception is thrown if using default configuration,
* and no custom from address is specified.
* @expectedException Exception
*/
public function testMailFromDefaultConfigurationException()
{
new EMail('test', null, 'phpunit@simplesamlphp.org');
}
/**
* Test that an exception is thrown if using an invalid "From"-address
* @expectedException Exception
*/
public function testInvalidFromAddressException()
{
new EMail('test', "phpunit@simplesamlphp.org\nLorem Ipsum", 'phpunit@simplesamlphp.org');
}
/**
* Test that an exception is thrown if using an invalid "To"-address
* @expectedException Exception
*/
public function testInvalidToAddressException()
{
new EMail('test', 'phpunit@simplesamlphp.org', "phpunit@simplesamlphp.org\nLorem Ipsum");
}
/**
* Test that the data given is visible in the resulting mail
* @dataProvider mailTemplates
*/
public function testMailContents($template)
{
$mail = new EMail('subject-subject-subject-subject-subject-subject-subject', 'phpunit@simplesamlphp.org', 'phpunit@simplesamlphp.org');
$mail->setText('text-text-text-text-text-text-text');
$mail->setData(['key-key-key-key-key-key-key' => 'value-value-value-value-value-value-value']);
$result = $mail->generateBody($template);
$this->assertRegexp('/(subject-){6}/', $result);
$this->assertRegexp('/(text-){6}/', $result);
$this->assertRegexp('/(key-){6}/', $result);
$this->assertRegexp('/(value-){6}/', $result);
}
/** All templates that should be tested in #testMailContents($template) */
public static function mailTemplates()
{
return [['mailtxt.twig'], ['mailhtml.twig']];
}
}
...@@ -13,9 +13,9 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { ...@@ -13,9 +13,9 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit; exit;
} }
$reportId = (string) $_REQUEST['reportId']; $reportId = $_REQUEST['reportId'];
$email = (string) $_REQUEST['email']; $email = $_REQUEST['email'];
$text = htmlspecialchars((string) $_REQUEST['text']); $text = $_REQUEST['text'];
$data = null; $data = null;
try { try {
...@@ -29,10 +29,8 @@ if ($data === null) { ...@@ -29,10 +29,8 @@ if ($data === null) {
$data = [ $data = [
'exceptionMsg' => 'not set', 'exceptionMsg' => 'not set',
'exceptionTrace' => 'not set', 'exceptionTrace' => 'not set',
'reportId' => $reportId,
'trackId' => 'not set', 'trackId' => 'not set',
'url' => 'not set', 'url' => 'not set',
'version' => $config->getVersion(),
'referer' => 'not set', 'referer' => 'not set',
]; ];
...@@ -41,90 +39,18 @@ if ($data === null) { ...@@ -41,90 +39,18 @@ if ($data === null) {
} }
} }
foreach ($data as $k => $v) { $data['reportId'] = $reportId;
$data[$k] = htmlspecialchars($v); $data['version'] = $config->getVersion();
} $data['hostname'] = php_uname('n');
$data['directory'] = dirname(dirname(__FILE__));
// build the email message
$message = <<<MESSAGE
<h1>SimpleSAMLphp Error Report</h1>
<p>Message from user:</p>
<div class="box" style="background: yellow; color: #888; border: 1px solid #999900; padding: .4em; margin: .5em">
%s
</div>
<p>Exception: <strong>%s</strong></p>
<pre>%s</pre>
<p>URL:</p>
<pre><a href="%s">%s</a></pre>
<p>Host:</p>
<pre>%s</pre>
<p>Directory:</p>
<pre>%s</pre>
<p>Track ID:</p>
<pre>%s</pre>
<p>Version: <code>%s</code></p>
<p>Report ID: <code>%s</code></p>
<p>Referer: <code>%s</code></p>
<hr />
<div class="footer">
This message was sent using SimpleSAMLphp. Visit the <a href="http://simplesamlphp.org/">SimpleSAMLphp homepage</a>.
</div>
MESSAGE;
$message = sprintf(
$message,
$text,
$data['exceptionMsg'],
$data['exceptionTrace'],
$data['url'],
$data['url'],
htmlspecialchars(php_uname('n')),
dirname(dirname(__FILE__)),
$data['trackId'],
$data['version'],
$data['reportId'],
$data['referer']
);
// add the email address of the submitter as the Reply-To address
$email = trim($email);
// check that it looks like a valid email address
if (!preg_match('/\s/', $email) && strpos($email, '@') !== false) {
$replyto = $email;
} else {
$replyto = null;
}
$from = $config->getString('sendmail_from', null);
if ($from === null || $from === '') {
$from = ini_get('sendmail_from');
if ($from === '' || $from === false) {
$from = 'no-reply@example.org';
}
}
// If no sender email was configured at least set some relevant from address
if ($from === 'no-reply@example.org' && $replyto !== null) {
$from = $replyto;
}
// send the email if ($config->getBoolean('errorreporting', true)) {
$toAddress = $config->getString('technicalcontact_email', 'na@example.org'); $mail = new SimpleSAML\Utils\EMail('SimpleSAMLphp error report from '.$email);
if ($config->getBoolean('errorreporting', true) && $toAddress !== 'na@example.org') { $mail->setData($data);
$email = new \SimpleSAML\XHTML\EMail($toAddress, 'SimpleSAMLphp error report', $from); $mail->addReplyTo($email);
$email->setBody($message); $mail->setText($text);
$email->send(); $mail->send();
SimpleSAML\Logger::error('Report with id '.$reportId.' sent to <'.$toAddress.'>.'); SimpleSAML\Logger::error('Report with id '.$reportId.' sent');
} }
// redirect the user back to this page to clear the POST request // redirect the user back to this page to clear the POST request
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment