silverstripe-framework/email/Email.php

610 lines
18 KiB
PHP
Raw Normal View History

<?php
/**
* @package sapphire
* @subpackage email
*/
if(isset($_SERVER['SERVER_NAME'])) {
/**
* X-Mailer header value on emails sent
*/
define('X_MAILER', 'SilverStripe Mailer - version 2006.06.21 (Sent from "'.$_SERVER['SERVER_NAME'].'")');
} else {
/**
* @ignore
*/
define('X_MAILER', 'SilverStripe Mailer - version 2006.06.21');
}
// Note: The constant 'BOUNCE_EMAIL' should be defined as a valid email address for where bounces should be returned to.
/**
* Class to support sending emails.
* @package sapphire
* @subpackage email
*/
class Email extends ViewableData {
protected $from, $to, $subject, $body, $plaintext_body, $cc, $bcc;
protected static $mailer;
/**
* Set the mailer.
* This can be used to provide a mailer other than the default, for testing, for example.
*/
static function set_mailer(Mailer $mailer) {
self::$mailer = $mailer;
}
/**
* Get the mailer.
*/
static function mailer() {
if(!self::$mailer) self::$mailer = new Mailer();
return self::$mailer;
}
/**
* A map of header-name -> header-value
*/
protected $customHeaders;
protected $attachments = array();
protected $parseVariables_done = false;
protected $ss_template = "GenericEmail";
protected $template_data = null;
protected $bounceHandlerURL = null;
/**
* The default administrator email address. This will be set in the config on a site-by-site basis
*/
static $admin_email_address = '';
protected static $send_all_emails_to = null;
protected static $bcc_all_emails_to = null;
protected static $cc_all_emails_to = null;
/**
* Create a new email.
*/
public function __construct($from = null, $to = null, $subject = null, $body = null, $bounceHandlerURL = null, $cc = null, $bcc = null) {
$this->from = $from;
$this->to = $to;
$this->subject = $subject;
$this->body = $body;
$this->cc = $cc;
$this->bcc = $bcc;
$this->setBounceHandlerURL($bounceHandlerURL);
}
public function attachFileFromString($data, $filename, $mimetype = null) {
$this->attachments[] = array(
'contents' => $data,
'filename' => $filename,
'mimetype' => $mimetype,
);
}
public function setBounceHandlerURL( $bounceHandlerURL ) {
if( $bounceHandlerURL )
$this->bounceHandlerURL = $bounceHandlerURL;
else
$this->bounceHandlerURL = $_SERVER['HTTP_HOST'] . Director::baseURL() . 'Email_BounceHandler';
}
public function attachFile($filename, $attachedFilename = null, $mimetype = null) {
$this->attachFileFromString(file_get_contents(Director::getAbsFile($filename)), $attachedFilename, $mimetype);
}
public function setFormat($format) {
$this->format = $format;
}
public function Subject() {
return $this->subject;
}
public function Body() {
return $this->body;
}
public function To() {
return $this->to;
}
public function From() {
return $this->from;
}
public function Cc() {
return $this->cc;
}
public function Bcc() {
return $this->bcc;
}
public function setSubject($val) { $this->subject = $val; }
public function setBody($val) { $this->body = $val; }
public function setTo($val) { $this->to = $val; }
public function setFrom($val) { $this->from = $val; }
public function setCc($val) {$this->cc = $val;}
public function setBcc($val) {$this->bcc = $val;}
/**
* Add a custom header to this value.
* Useful for implementing all those cool features that we didn't think of.
*/
public function addCustomHeader($headerName, $headerValue) {
if($headerName == 'Cc') $this->cc = $headerValue;
else if($headerName == 'Bcc') $this->bcc = $headerValue;
else {
if($this->customHeaders[$headerName]) $this->customHeaders[$headerName] .= ", ";
$this->customHeaders[$headerName] .= $headerValue;
}
}
public function BaseURL() {
return Director::absoluteBaseURL();
}
/**
* Debugging help
*/
public function debug() {
$this->parseVariables();
return "<h2>Email template $this->class</h2>\n" .
"<p><b>From:</b> $this->from\n" .
"<b>To:</b> $this->to\n" .
"<b>Cc:</b> $this->cc\n" .
"<b>Bcc:</b> $this->bcc\n" .
"<b>Subject:</b> $this->subject</p>" .
$this->body;
}
protected function templateData() {
if($this->template_data) {
return $this->template_data->customise(array(
"To" => $this->to,
"Cc" => $this->cc,
"Bcc" => $this->bcc,
"From" => $this->from,
"Subject" => $this->subject,
"Body" => $this->body,
"BaseURL" => $this->BaseURL(),
"IsEmail" => true,
));
} else {
return $this;
}
}
/**
* Used by SSViewer templates to detect if we're rendering an email template rather than a page template
*/
public function IsEmail() {
return true;
}
/**
* Populate this email template with values.
* This may be called many times.
*/
function populateTemplate($data) {
if($this->template_data) {
$this->template_data = $this->template_data->customise($data);
} else {
if(is_array($data)) $data = new ArrayData($data);
$this->template_data = $this->customise($data);
}
$this->parseVariables_done = false;
}
/**
* Load all the template variables into the internal variables, including
* 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
* and it won't be plain email :)
*/
protected function parseVariables($isPlain = false) {
if(!$this->parseVariables_done) {
$this->parseVariables_done = true;
// Parse $ variables in the base parameters
$data = $this->templateData();
foreach(array('from','to','subject','body', 'plaintext_body', 'cc', 'bcc') as $param) {
$template = SSViewer::fromString($this->$param);
$this->$param = $template->process($data);
}
// Process a .SS template file
$fullBody = $this->body;
if($this->ss_template && !$isPlain) {
// Requery data so that updated versions of To, From, Subject, etc are included
$data = $this->templateData();
$template = new SSViewer($this->ss_template);
if($template->exists()) {
$fullBody = $template->process($data);
}
}
// Rewrite relative URLs
$this->body = HTTP::absoluteURLs($fullBody);
}
}
/**
* @desc Validates the email address. Returns true of false
*/
static function validEmailAddress($address) {
return ereg('^([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);
}
/**
* @desc Send the email in plaintext
*/
function sendPlain($messageID = null) {
global $project;
Requirements::clear();
$this->parseVariables(true);
if(empty($this->from)) $this->from = Email::getAdminEmail();
$this->setBounceHandlerURL($this->bounceHandlerURL);
$headers['X-SilverStripeBounceURL'] = $this->bounceHandlerURL;
if($messageID) $headers['X-SilverStripeMessageID'] = $project . '.' . $messageID;
if($project) $headers['X-SilverStripeSite'] = $project;
$to = $this->to;
$subject = $this->subject;
if(self::$send_all_emails_to) {
$subject .= " [addressed to $to";
$to = self::$send_all_emails_to;
if($this->cc) $subject .= ", cc to $this->cc";
if($this->bcc) $subject .= ", bcc to $this->bcc";
$subject .= ']';
} else {
if($this->cc) $headers["Cc"] = $this->cc;
if($this->bcc) $headers["Bcc"] = $this->bcc;
}
if(self::$cc_all_emails_to) {
if(trim($headers['Cc'])) $headers['Cc'] .= ', ';
$headers['Cc'] .= self::$cc_all_emails_to;
}
if(self::$bcc_all_emails_to) {
if(trim($headers['Bcc'])) $headers['Bcc'] .= ', ';
$headers['Bcc'] .= self::$bcc_all_emails_to;
}
return self::mailer()->sendPlain($to, $this->from, $subject, $this->body, $this->attachments, $headers);
}
/**
* Send the email.
*/
public function send( $messageID = null ) {
Requirements::clear();
$this->parseVariables();
if( empty( $this->from ) ){
$this->from = Email::getAdminEmail();
}
$this->setBounceHandlerURL( $this->bounceHandlerURL );
$headers = $this->customHeaders;
$headers['X-SilverStripeBounceURL'] = $this->bounceHandlerURL;
if( $messageID ) $headers['X-SilverStripeMessageID'] = project() . '.' . $messageID;
if( project() ) $headers['X-SilverStripeSite'] = project();
$to = $this->to;
$subject = $this->subject;
if(self::$send_all_emails_to) {
$subject .= " [addressed to $to";
$to = self::$send_all_emails_to;
if($this->cc) $subject .= ", cc to $this->cc";
if($this->bcc) $subject .= ", bcc to $this->bcc";
$subject .= ']';
unset($headers['Cc']);
unset($headers['Bcc']);
} else {
if($this->cc) $headers["Cc"] = $this->cc;
if($this->bcc) $headers["Bcc"] = $this->bcc;
}
if(self::$cc_all_emails_to) {
if(!empty($headers['Cc']) && trim($headers['Cc'])) {
$headers['Cc'] .= ', ' . self::$cc_all_emails_to;
} else {
$headers['Cc'] = self::$cc_all_emails_to;
}
}
if(self::$bcc_all_emails_to) {
if(!empty($headers['Bcc']) && trim($headers['Bcc'])) {
$headers['Bcc'] .= ', ' . self::$bcc_all_emails_to;
} else {
$headers['Bcc'] = self::$bcc_all_emails_to;
}
}
return self::mailer()->sendHTML($to, $this->from, $subject, $this->body, $this->attachments, $headers, $this->plaintext_body);
Requirements::restore();
return $result;
}
public static function setAdminEmail( $newEmail ) {
self::$admin_email_address = $newEmail;
}
public static function getAdminEmail() {
return self::$admin_email_address;
}
/**
* Send every email generated by the Email class to the given address.
* It will also add " [addressed to (email), cc to (email), bcc to (email)]" to the end of the subject line
* This can be used when testing, by putting a command like this in your _config.php file
*
* if(!Director::isLive()) Email::send_all_emails_to("someone@example.com")
*/
public static function send_all_emails_to($emailAddress) {
self::$send_all_emails_to = $emailAddress;
}
/**
* CC every email generated by the Email class to the given address.
* It won't affect the original delivery in the same way that send_all_emails_to does. It just adds a CC header
* with the given email address. Note that you can only call this once - subsequent calls will overwrite the configuration
* variable.
*
* This can be used when you have a system that relies heavily on email and you want someone to be checking all correspondence.
*
* if(Director::isLive()) Email::cc_all_emails_to("supportperson@example.com")
*/
public static function cc_all_emails_to($emailAddress) {
self::$cc_all_emails_to = $emailAddress;
}
/**
* BCC every email generated by the Email class to the given address.
* It won't affect the original delivery in the same way that send_all_emails_to does. It just adds a BCC header
* with the given email address. Note that you can only call this once - subsequent calls will overwrite the configuration
* variable.
*
* This can be used when you have a system that relies heavily on email and you want someone to be checking all correspondence.
*
* if(Director::isLive()) Email::cc_all_emails_to("supportperson@example.com")
*/
public static function bcc_all_emails_to($emailAddress) {
self::$bcc_all_emails_to = $emailAddress;
}
/**
* Checks for RFC822-valid email format.
*
* @param string $str
* @return boolean
*
* @see http://code.iamcal.com/php/rfc822/rfc822.phps
* @copyright Cal Henderson <cal@iamcal.com>
* This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
* http://creativecommons.org/licenses/by-sa/2.5/
*/
function is_valid_address($email){
$qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]';
$dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]';
$atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c'.
'\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+';
$quoted_pair = '\\x5c[\\x00-\\x7f]';
$domain_literal = "\\x5b($dtext|$quoted_pair)*\\x5d";
$quoted_string = "\\x22($qtext|$quoted_pair)*\\x22";
$domain_ref = $atom;
$sub_domain = "($domain_ref|$domain_literal)";
$word = "($atom|$quoted_string)";
$domain = "$sub_domain(\\x2e$sub_domain)*";
$local_part = "$word(\\x2e$word)*";
$addr_spec = "$local_part\\x40$domain";
return preg_match("!^$addr_spec$!", $email) ? 1 : 0;
}
}
/**
* Implements an email template that can be populated.
*
* @deprecated - Please use Email_Template instead!
*
* @package sapphire
* @subpackage email
*/
class Email_Template extends Email {
public function __construct($from = null, $to = null, $subject = null, $body = null, $bounceHandlerURL = null, $cc = null, $bcc = null) {
parent::__construct($from, $to, $subject, $body, $bounceHandlerURL, $cc, $bcc);
user_error('Email_Template is deprecated. Please use Email instead.', E_USER_NOTICE);
}
}
/**
* Base class that email bounce handlers extend
* @package sapphire
* @subpackage email
*/
class Email_BounceHandler extends Controller {
function init() {
BasicAuth::disable();
parent::init();
}
function index() {
$subclasses = ClassInfo::subclassesFor( $this->class );
unset($subclasses[$this->class]);
if( $subclasses ) {
$subclass = array_pop( $subclasses );
$task = new $subclass();
$task->index();
return;
}
// Check if access key exists
if( !isset($_REQUEST['Key']) ) {
echo 'Error: Access validation failed. No "Key" specified.';
return;
}
// Check against access key defined in sapphire/_config.php
if( $_REQUEST['Key'] != EMAIL_BOUNCEHANDLER_KEY) {
echo 'Error: Access validation failed. Invalid "Key" specified.';
return;
}
if( !$_REQUEST['Email'] ) {
echo "No email address";
return;
}
$this->recordBounce( $_REQUEST['Email'], $_REQUEST['Date'], $_REQUEST['Time'], $_REQUEST['Message'] );
}
private function recordBounce( $email, $date = null, $time = null, $error = null ) {
if(ereg('<(.*)>', $email, $parts)) $email = $parts[1];
$SQL_email = Convert::raw2sql($email);
$SQL_bounceTime = Convert::raw2sql("$date $time");
$duplicateBounce = DataObject::get_one("Email_BounceRecord", "BounceEmail = '$SQL_email' AND (BounceTime+INTERVAL 1 MINUTE) > '$SQL_bounceTime'");
if(!$duplicateBounce) {
$record = new Email_BounceRecord();
$member = DataObject::get_one( 'Member', "`Email`='$SQL_email'" );
if( $member ) {
$record->MemberID = $member->ID;
// If the SilverStripeMessageID (taken from the X-SilverStripeMessageID header embedded in the email) is sent,
// then log this bounce in a Newsletter_SentRecipient record so it will show up on the 'Sent Status Report' tab of the Newsletter
if( isset($_REQUEST['SilverStripeMessageID'])) {
// Note: was sent out with: $project . '.' . $messageID;
$message_id_parts = explode('.', $_REQUEST['SilverStripeMessageID']);
// Note: was encoded with: base64_encode( $newsletter->ID . '_' . date( 'd-m-Y H:i:s' ) );
$newsletter_id_date_parts = explode ('_', base64_decode($message_id_parts[1]) );
// Escape just in case
$SQL_memberID = Convert::raw2sql($member->ID);
$SQL_newsletterID = Convert::raw2sql($newsletter_id_date_parts[0]);
// Log the bounce
$oldNewsletterSentRecipient = DataObject::get_one("Newsletter_SentRecipient", "MemberID = '$SQL_memberID' AND ParentID = '$SQL_newsletterID' AND Email = '$SQL_email'");
// Update the Newsletter_SentRecipient record if it exists
if($oldNewsletterSentRecipient) {
$oldNewsletterSentRecipient->Result = 'Bounced';
$oldNewsletterSentRecipient->write();
} else {
// For some reason it didn't exist, create a new record
$newNewsletterSentRecipient = new Newsletter_SentRecipient();
$newNewsletterSentRecipient->Email = $SQL_email;
$newNewsletterSentRecipient->MemberID = $member->ID;
$newNewsletterSentRecipient->Result = 'Bounced';
$newNewsletterSentRecipient->ParentID = $newsletter_id_date_parts[0];
$newNewsletterSentRecipient->write();
}
// Now we are going to Blacklist this member so that email will not be sent to them in the future.
// Note: Sending can be re-enabled by going to 'Mailing List' 'Bounced' tab and unchecking the box under 'Blacklisted'
$member->setBlacklistedEmail(TRUE);
echo '<p><b>Member: '.$member->FirstName.' '.$member->Surname.' <'.$member->Email.'> was added to the Email Blacklist!</b></p>';
}
}
if( !$date )
$date = date( 'd-m-Y' );
/*else
$date = date( 'd-m-Y', strtotime( $date ) );*/
if( !$time )
$time = date( 'H:i:s' );
/*else
$time = date( 'H:i:s', strtotime( $time ) );*/
$record->BounceEmail = $email;
$record->BounceTime = $date . ' ' . $time;
$record->BounceMessage = $error;
$record->write();
echo "Handled bounced email to address: $email";
} else {
echo 'Sorry, this bounce report has already been logged, not logging this duplicate bounce.';
}
}
}
/**
* Database record for recording a bounced email
* @package sapphire
* @subpackage email
*/
class Email_BounceRecord extends DataObject {
static $db = array(
'BounceEmail' => 'Varchar',
'BounceTime' => 'Datetime',
'BounceMessage' => 'Varchar'
);
static $has_one = array(
'Member' => 'Member'
);
}
/**
* This class is responsible for ensuring that members who are on it receive NO email
* communication at all. any correspondance is caught before the email is sent.
* @package sapphire
* @subpackage email
*/
class Email_BlackList extends DataObject{
static $db = array(
'BlockedEmail' => 'Varchar',
);
static $has_one = array(
'Member' => 'Member'
);
/**
* Helper function to see if the email being
* sent has specifically been blocked.
*/
static function isBlocked($email){
$blockedEmails = DataObject::get("Email_BlackList")->toDropDownMap("ID","BlockedEmail");
if($blockedEmails){
if(in_array($email,$blockedEmails)){
return true;
}else{
return false;
}
}else{
return false;
}
}
}
?>