FIX for #5784: Added ->setReplyTo(), deprecated ->replyTo() for API consistency. Revamping, fixing, and enhancing internal Email API documentation. Simplified code and brought up-to-date with latest standards.

This commit is contained in:
Patrick Nelson 2016-07-08 13:03:30 -07:00
parent 4cd29e80b7
commit 74c555e004
3 changed files with 195 additions and 100 deletions

View File

@ -121,7 +121,7 @@ You can set the default sender address of emails through the `Email.admin_email`
<div class="alert" markdown="1"> <div class="alert" markdown="1">
Remember, setting a `from` address that doesn't come from your domain (such as the users email) will likely see your Remember, setting a `from` address that doesn't come from your domain (such as the users email) will likely see your
email marked as spam. If you want to send from another address think about using the `replyTo` method. email marked as spam. If you want to send from another address think about using the `setReplyTo` method.
</div> </div>
## Redirecting Emails ## Redirecting Emails
@ -144,11 +144,13 @@ Configuration of those properties looks like the following:
Config::inst()->update('Email', 'send_all_emails_to', "developer@example.com"); Config::inst()->update('Email', 'send_all_emails_to', "developer@example.com");
} }
### Setting custom replyTo ### Setting custom "Reply To" email address.
For email messages that should have an email address which is replied to that actually differs from the original "from" email, do the following. This is encouraged especially when the domain responsible for sending the message isn't necessarily the same which should be used for return correspondence and should help prevent your message from being marked as spam.
:::php :::php
$email = new Email(..); $email = new Email(..);
$email->replyTo('me@address.com'); $email->setReplyTo('me@address.com');
### Setting Custom Headers ### Setting Custom Headers

View File

@ -25,89 +25,76 @@ if(isset($_SERVER['SERVER_NAME'])) {
class Email extends ViewableData { class Email extends ViewableData {
/** /**
* @param string $from Email-Address * @var string $from Email-Address
*/ */
protected $from; protected $from;
/** /**
* @param string $to Email-Address. Use comma-separation to pass multiple email-addresses. * @var string $to Email-Address. Use comma-separation to pass multiple email-addresses.
*/ */
protected $to; protected $to;
/** /**
* @param string $subject Subject of the email * @var string $subject Subject of the email
*/ */
protected $subject; protected $subject;
/** /**
* @param string $body HTML content of the email.
* Passed straight into {@link $ss_template} as $Body variable. * Passed straight into {@link $ss_template} as $Body variable.
*
* @var string $body HTML content of the email.
*/ */
protected $body; protected $body;
/** /**
* @param string $plaintext_body Optional string for plaintext emails.
* If not set, defaults to converting the HTML-body with {@link Convert::xml2raw()}. * If not set, defaults to converting the HTML-body with {@link Convert::xml2raw()}.
*
* @var string $plaintext_body Optional string for plaintext emails.
*/ */
protected $plaintext_body; protected $plaintext_body;
/** /**
* @param string $cc * @var string $cc
*/ */
protected $cc; protected $cc;
/** /**
* @param string $bcc * @var string $bcc
*/ */
protected $bcc; protected $bcc;
/** /**
* @deprecated since version 4.0 * @var array $customHeaders A map of header-name -> header-value
*/
public static function set_mailer(Mailer $mailer) {
Deprecation::notice('4.0', 'Use Injector to override the Mailer service');
Injector::inst()->registerService($mailer, 'Mailer');
}
/**
* Get the mailer.
*
* @return Mailer
*/
public static function mailer() {
return Injector::inst()->get('Mailer');
}
/**
* @param array $customHeaders A map of header-name -> header-value
*/ */
protected $customHeaders = array(); protected $customHeaders = array();
/** /**
* @param array $attachements Internal, use {@link attachFileFromString()} or {@link attachFile()} * @var array $attachments Internal, use {@link attachFileFromString()} or {@link attachFile()}
*/ */
protected $attachments = array(); protected $attachments = array();
/** /**
* @param boolean $ * @var boolean $parseVariables_done
*/ */
protected $parseVariables_done = false; protected $parseVariables_done = false;
/** /**
* @param string $ss_template The name of the used template (without *.ss extension) * @var string $ss_template The name of the used template (without *.ss extension)
*/ */
protected $ss_template = "GenericEmail"; protected $ss_template = 'GenericEmail';
/** /**
* @param array $template_data Additional data available in a template.
* Used in the same way than {@link ViewableData->customize()}. * Used in the same way than {@link ViewableData->customize()}.
*
* @var ViewableData_Customised $template_data Additional data available in a template.
*/ */
protected $template_data = null; protected $template_data;
/** /**
* This will be set in the config on a site-by-site basis
*
* @config * @config
* @var string The default administrator email address. * @var string The default administrator email address.
* This will be set in the config on a site-by-site basis
*/ */
private static $admin_email = ''; private static $admin_email = '';
@ -120,9 +107,9 @@ class Email extends ViewableData {
* It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_TO. * It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_TO.
* *
* @config * @config
* @param string $send_all_emails_to Email-Address * @var string $send_all_emails_to Email-Address
*/ */
private static $send_all_emails_to = null; private static $send_all_emails_to;
/** /**
* Send every email generated by the Email class *from* the given address. * Send every email generated by the Email class *from* the given address.
@ -132,47 +119,80 @@ class Email extends ViewableData {
* It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_FROM. * It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_FROM.
* *
* @config * @config
* @param string $send_all_emails_from Email-Address * @var string $send_all_emails_from Email-Address
*/ */
private static $send_all_emails_from = null; private static $send_all_emails_from;
/** /**
* @config * @config
* @param string BCC every email generated by the Email class to the given address. * @var string BCC every email generated by the Email class to the given address.
*/ */
private static $bcc_all_emails_to = null; private static $bcc_all_emails_to;
/** /**
* @config * @config
* @param string CC every email generated by the Email class to the given address. * @var string CC every email generated by the Email class to the given address.
*/ */
private static $cc_all_emails_to = null; private static $cc_all_emails_to;
/** /**
* Create a new email. * Create a new email.
*
* @param string|null $from
* @param string|null $to
* @param string|null $subject
* @param string|null $body
* @param string|null $bounceHandlerURL
* @param string|null $cc
* @param string|null $bcc
*/ */
public function __construct($from = null, $to = null, $subject = null, $body = null, $bounceHandlerURL = null, public function __construct($from = null, $to = null, $subject = null, $body = null, $bounceHandlerURL = null,
$cc = null, $bcc = null) { $cc = null, $bcc = null) {
if($from != null) $this->from = $from; if($from !== null) $this->from = $from;
if($to != null) $this->to = $to; if($to !== null) $this->to = $to;
if($subject != null) $this->subject = $subject; if($subject !== null) $this->subject = $subject;
if($body != null) $this->body = $body; if($body !== null) $this->body = $body;
if($cc != null) $this->cc = $cc; if($cc !== null) $this->cc = $cc;
if($bcc != null) $this->bcc = $bcc; if($bcc !== null) $this->bcc = $bcc;
if($bounceHandlerURL != null) { if($bounceHandlerURL !== null) {
Deprecation::notice('4.0', 'Use "emailbouncehandler" module'); Deprecation::notice('4.0', 'Use "emailbouncehandler" module');
} }
parent::__construct(); parent::__construct();
} }
public function attachFileFromString($data, $filename, $mimetype = null) { /**
* Get the mailer.
*
* @return Mailer
*/
public static function mailer() {
return Injector::inst()->get('Mailer');
}
/**
* @deprecated since version 4.0
*/
public static function set_mailer(Mailer $mailer) {
Deprecation::notice('4.0', 'Use Injector to override the Mailer service');
Injector::inst()->registerService($mailer, 'Mailer');
}
/**
* Attach a file based on provided raw data.
*
* @param string $data The raw file data (not encoded).
* @param string $attachedFilename Name of the file that should appear once it's sent as a separate attachment.
* @param string|null $mimeType MIME type to use when attaching file. If not provided, will attempt to infer via HTTP::get_mime_type().
* @return $this
*/
public function attachFileFromString($data, $attachedFilename, $mimeType = null) {
$this->attachments[] = array( $this->attachments[] = array(
'contents' => $data, 'contents' => $data,
'filename' => $filename, 'filename' => $attachedFilename,
'mimetype' => $mimetype, 'mimetype' => $mimeType,
); );
return $this; return $this;
} }
@ -184,66 +204,116 @@ class Email extends ViewableData {
Deprecation::notice('4.0', 'Use "emailbouncehandler" module'); Deprecation::notice('4.0', 'Use "emailbouncehandler" module');
} }
public function attachFile($filename, $attachedFilename = null, $mimetype = null) { /**
* Attach the specified file to this email message.
*
* @param string $filename Relative or full path to file you wish to attach to this email message.
* @param string|null $attachedFilename Name of the file that should appear once it's sent as a separate attachment.
* @param string|null $mimeType MIME type to use when attaching file. If not provided, will attempt to infer via HTTP::get_mime_type().
* @return $this
*/
public function attachFile($filename, $attachedFilename = null, $mimeType = null) {
if(!$attachedFilename) $attachedFilename = basename($filename); if(!$attachedFilename) $attachedFilename = basename($filename);
$absoluteFileName = Director::getAbsFile($filename); $absoluteFileName = Director::getAbsFile($filename);
if(file_exists($absoluteFileName)) { if(file_exists($absoluteFileName)) {
$this->attachFileFromString(file_get_contents($absoluteFileName), $attachedFilename, $mimetype); $this->attachFileFromString(file_get_contents($absoluteFileName), $attachedFilename, $mimeType);
} else { } else {
user_error("Could not attach '$absoluteFileName' to email. File does not exist.", E_USER_NOTICE); user_error("Could not attach '$absoluteFileName' to email. File does not exist.", E_USER_NOTICE);
} }
return $this; return $this;
} }
/**
* @return string|null
*/
public function Subject() { public function Subject() {
return $this->subject; return $this->subject;
} }
/**
* @return string|null
*/
public function Body() { public function Body() {
return $this->body; return $this->body;
} }
/**
* @return string|null
*/
public function To() { public function To() {
return $this->to; return $this->to;
} }
/**
* @return string|null
*/
public function From() { public function From() {
return $this->from; return $this->from;
} }
/**
* @return string|null
*/
public function Cc() { public function Cc() {
return $this->cc; return $this->cc;
} }
/**
* @return string|null
*/
public function Bcc() { public function Bcc() {
return $this->bcc; return $this->bcc;
} }
/**
* @param string $val
* @return $this
*/
public function setSubject($val) { public function setSubject($val) {
$this->subject = $val; $this->subject = $val;
return $this; return $this;
} }
/**
* @param string $val
* @return $this
*/
public function setBody($val) { public function setBody($val) {
$this->body = $val; $this->body = $val;
return $this; return $this;
} }
/**
* @param string $val
* @return $this
*/
public function setTo($val) { public function setTo($val) {
$this->to = $val; $this->to = $val;
return $this; return $this;
} }
/**
* @param string $val
* @return $this
*/
public function setFrom($val) { public function setFrom($val) {
$this->from = $val; $this->from = $val;
return $this; return $this;
} }
/**
* @param string $val
* @return $this
*/
public function setCc($val) { public function setCc($val) {
$this->cc = $val; $this->cc = $val;
return $this; return $this;
} }
/**
* @param string $val
* @return $this
*/
public function setBcc($val) { public function setBcc($val) {
$this->bcc = $val; $this->bcc = $val;
return $this; return $this;
@ -251,36 +321,64 @@ class Email extends ViewableData {
/** /**
* Set the "Reply-To" header with an email address. * Set the "Reply-To" header with an email address.
* @param string $email The email address of the "Reply-To" header *
* @param string $val
* @return $this
*/ */
public function replyTo($email) { public function setReplyTo($val) {
$this->addCustomHeader('Reply-To', $email); $this->addCustomHeader('Reply-To', $val);
return $this; return $this;
} }
/** /**
* Add a custom header to this value. * @param string $email
* Useful for implementing all those cool features that we didn't think of. * @return $this
* @deprecated 4.0 Use the "setReplyTo" method instead
*/
public function replyTo($email) {
Deprecation::notice('4.0', 'Use the "setReplyTo" method instead');
$this->setReplyTo($email);
return $this;
}
/**
* Add a custom header to this email message. Useful for implementing all those cool features that we didn't think of.
*
* IMPORTANT: If the specified header already exists, the provided value will be appended!
*
* @todo Should there be an option to replace instead of append? Or maybe a new method ->setCustomHeader()?
* *
* @param string $headerName * @param string $headerName
* @param string $headerValue * @param string $headerValue
* @return $this
*/ */
public function addCustomHeader($headerName, $headerValue) { public function addCustomHeader($headerName, $headerValue) {
if($headerName == 'Cc') $this->cc = $headerValue; if ($headerName == 'Cc') {
else if($headerName == 'Bcc') $this->bcc = $headerValue; $this->cc = $headerValue;
else { } elseif($headerName == 'Bcc') {
if(isset($this->customHeaders[$headerName])) $this->customHeaders[$headerName] .= ", " . $headerValue; $this->bcc = $headerValue;
else $this->customHeaders[$headerName] = $headerValue; } else {
// Append value instead of replacing.
if(isset($this->customHeaders[$headerName])) {
$this->customHeaders[$headerName] .= ", " . $headerValue;
} else {
$this->customHeaders[$headerName] = $headerValue;
}
} }
return $this; return $this;
} }
/**
* @return string
*/
public function BaseURL() { public function BaseURL() {
return Director::absoluteBaseURL(); return Director::absoluteBaseURL();
} }
/** /**
* Debugging help * Get an HTML string for debugging purposes.
*
* @return string
*/ */
public function debug() { public function debug() {
$this->parseVariables(); $this->parseVariables();
@ -298,6 +396,7 @@ class Email extends ViewableData {
* Set template name (without *.ss extension). * Set template name (without *.ss extension).
* *
* @param string $template * @param string $template
* @return $this
*/ */
public function setTemplate($template) { public function setTemplate($template) {
$this->ss_template = $template; $this->ss_template = $template;
@ -311,6 +410,9 @@ class Email extends ViewableData {
return $this->ss_template; return $this->ss_template;
} }
/**
* @return $this
*/
protected function templateData() { protected function templateData() {
if($this->template_data) { if($this->template_data) {
return $this->template_data->customise(array( return $this->template_data->customise(array(
@ -336,8 +438,10 @@ class Email extends ViewableData {
} }
/** /**
* Populate this email template with values. * Populate this email template with values. This may be called many times.
* This may be called many times. *
* @param array|ViewableData $data
* @return $this
*/ */
public function populateTemplate($data) { public function populateTemplate($data) {
if($this->template_data) { if($this->template_data) {
@ -356,6 +460,9 @@ class Email extends ViewableData {
* the template into body. Called before send() or debugSend() * the template into body. Called before send() or debugSend()
* $isPlain=true will cause the template to be ignored, otherwise the GenericEmail template will be used * $isPlain=true will cause the template to be ignored, otherwise the GenericEmail template will be used
* and it won't be plain email :) * and it won't be plain email :)
*
* @param bool $isPlain
* @return $this
*/ */
protected function parseVariables($isPlain = false) { protected function parseVariables($isPlain = false) {
$origState = Config::inst()->get('SSViewer', 'source_file_comments'); $origState = Config::inst()->get('SSViewer', 'source_file_comments');
@ -389,15 +496,13 @@ class Email extends ViewableData {
} }
/** /**
* Validates the email address. Returns true of false * @param string $address
* @return bool
* @deprecated 4.0 Use the "is_valid_address" method instead
*/ */
public static function validEmailAddress($address) { public static function validEmailAddress($address) {
if (function_exists('filter_var')) { Deprecation::notice('4.0', 'Use the "is_valid_address" method instead');
return filter_var($address, FILTER_VALIDATE_EMAIL); return static::is_valid_address($address);
} else {
return preg_match('#^([a-zA-Z0-9_+\.\-]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)'
. '|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$#', $address);
}
} }
/** /**
@ -407,8 +512,8 @@ class Email extends ViewableData {
* @uses Mailer->sendPlain() * @uses Mailer->sendPlain()
* *
* @param string $messageID Optional message ID so the message can be identified in bounces etc. * @param string $messageID Optional message ID so the message can be identified in bounces etc.
* @return bool Success of the sending operation from an MTA perspective. * @return mixed Success of the sending operation from an MTA perspective. Doesn't actually give any indication if
* Doesn't actually give any indication if the mail has been delivered to the recipient properly) * the mail has been delivered to the recipient properly). See Mailer->sendPlain() for return type details.
*/ */
public function sendPlain($messageID = null) { public function sendPlain($messageID = null) {
Requirements::clear(); Requirements::clear();
@ -472,8 +577,8 @@ class Email extends ViewableData {
* @uses Mailer->sendHTML() * @uses Mailer->sendHTML()
* *
* @param string $messageID Optional message ID so the message can be identified in bounces etc. * @param string $messageID Optional message ID so the message can be identified in bounces etc.
* @return bool Success of the sending operation from an MTA perspective. * @return mixed Success of the sending operation from an MTA perspective. Doesn't actually give any indication if
* Doesn't actually give any indication if the mail has been delivered to the recipient properly) * the mail has been delivered to the recipient properly). See Mailer->sendPlain() for return type details.
*/ */
public function send($messageID = null) { public function send($messageID = null) {
Requirements::clear(); Requirements::clear();
@ -609,10 +714,10 @@ class Email extends ViewableData {
} }
/** /**
* Checks for RFC822-valid email format. * Validates the email address to get as close to RFC 822 compliant as possible.
* *
* @param string $str * @param string $email
* @return boolean * @return bool
* *
* @copyright Cal Henderson <cal@iamcal.com> * @copyright Cal Henderson <cal@iamcal.com>
* This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License * This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
@ -633,13 +738,12 @@ class Email extends ViewableData {
$local_part = "$word(\\x2e$word)*"; $local_part = "$word(\\x2e$word)*";
$addr_spec = "$local_part\\x40$domain"; $addr_spec = "$local_part\\x40$domain";
return preg_match("!^$addr_spec$!", $email) ? 1 : 0; return preg_match("!^$addr_spec$!", $email) === 1;
} }
/** /**
* Encode an email-address to protect it from spambots. * Encode an email-address to help protect it from spam bots. At the moment only simple string substitutions, which
* At the moment only simple string substitutions, * are not 100% safe from email harvesting.
* which are not 100% safe from email harvesting.
* *
* @todo Integrate javascript-based solution * @todo Integrate javascript-based solution
* *

View File

@ -55,27 +55,16 @@ class EmailTest extends SapphireTest {
$invalidEmails = array('foo.bar@', '@example.com', 'foo@'); $invalidEmails = array('foo.bar@', '@example.com', 'foo@');
foreach ($validEmails as $email) { foreach ($validEmails as $email) {
$this->assertEquals( $this->assertTrue(
$email,
Email::validEmailAddress($email),
'validEmailAddress() returns a valid email address'
);
$this->assertEquals(
1,
Email::is_valid_address($email), Email::is_valid_address($email),
'is_valid_address() returns 1 for a valid email address' 'is_valid_address() returns true for a valid email address'
); );
} }
foreach ($invalidEmails as $email) { foreach ($invalidEmails as $email) {
$this->assertFalse( $this->assertFalse(
Email::validEmailAddress($email),
'validEmailAddress() returns false for an invalid email address'
);
$this->assertEquals(
0,
Email::is_valid_address($email), Email::is_valid_address($email),
'is_valid_address() returns 0 for an invalid email address' 'is_valid_address() returns false for an invalid email address'
); );
} }
} }