1 <?php
2
3 4 5 6
7
8 if(isset($_SERVER['SERVER_NAME'])) {
9 10 11
12 define('X_MAILER', 'SilverStripe Mailer - version 2006.06.21 (Sent from "'.$_SERVER['SERVER_NAME'].'")');
13 } else {
14 15 16
17 define('X_MAILER', 'SilverStripe Mailer - version 2006.06.21');
18 }
19
20
21 22 23 24 25
26 class Email extends ViewableData {
27
28 29 30
31 protected $from;
32
33 34 35
36 protected $to;
37
38 39 40
41 protected $subject;
42
43 44 45 46
47 protected $body;
48
49 50 51 52
53 protected $plaintext_body;
54
55 56 57
58 protected $cc;
59
60 61 62
63 protected $bcc;
64
65 66 67
68 protected static $mailer;
69
70 71 72 73 74
75 static function set_mailer(Mailer $mailer) {
76 self::$mailer = $mailer;
77 }
78
79 80 81 82 83
84 static function mailer() {
85 if(!self::$mailer) self::$mailer = new Mailer();
86 return self::$mailer;
87 }
88
89 90 91
92 protected = array();
93
94 95 96
97 protected $attachments = array();
98
99 100 101
102 protected $parseVariables_done = false;
103
104 105 106
107 protected $ss_template = "GenericEmail";
108
109 110 111 112
113 protected $template_data = null;
114
115 116 117
118 protected $bounceHandlerURL = null;
119
120 121 122 123
124 static $admin_email_address = '';
125
126 127 128
129 protected static $send_all_emails_to = null;
130
131 132 133
134 protected static $bcc_all_emails_to = null;
135
136 137 138
139 protected static $cc_all_emails_to = null;
140
141 142 143
144 public function __construct($from = null, $to = null, $subject = null, $body = null, $bounceHandlerURL = null, $cc = null, $bcc = null) {
145 if($from != null) $this->from = $from;
146 if($to != null) $this->to = $to;
147 if($subject != null) $this->subject = $subject;
148 if($body != null) $this->body = $body;
149 if($cc != null) $this->cc = $cc;
150 if($bcc != null) $this->bcc = $bcc;
151 if($bounceHandlerURL != null) $this->setBounceHandlerURL($bounceHandlerURL);
152 parent::__construct();
153 }
154
155 public function attachFileFromString($data, $filename, $mimetype = null) {
156 $this->attachments[] = array(
157 'contents' => $data,
158 'filename' => $filename,
159 'mimetype' => $mimetype,
160 );
161 }
162
163 public function setBounceHandlerURL( $bounceHandlerURL ) {
164 if($bounceHandlerURL) {
165 $this->bounceHandlerURL = $bounceHandlerURL;
166 } else {
167 $this->bounceHandlerURL = $_SERVER['HTTP_HOST'] . Director::baseURL() . 'Email_BounceHandler';
168 }
169 }
170
171 public function attachFile($filename, $attachedFilename = null, $mimetype = null) {
172 $absoluteFileName = Director::getAbsFile($filename);
173 if(file_exists($absoluteFileName)) {
174 $this->attachFileFromString(file_get_contents($absoluteFileName), $attachedFilename, $mimetype);
175 } else {
176 user_error("Could not attach '$absoluteFileName' to email. File does not exist.", E_USER_NOTICE);
177 }
178 }
179
180 public function Subject() {
181 return $this->subject;
182 }
183
184 public function Body() {
185 return $this->body;
186 }
187
188 public function To() {
189 return $this->to;
190 }
191
192 public function From() {
193 return $this->from;
194 }
195
196 public function Cc() {
197 return $this->cc;
198 }
199
200 public function Bcc() {
201 return $this->bcc;
202 }
203
204 public function setSubject($val) {
205 $this->subject = $val;
206 }
207
208 public function setBody($val) {
209 $this->body = $val;
210 }
211
212 public function setTo($val) {
213 $this->to = $val;
214 }
215
216 public function setFrom($val) {
217 $this->from = $val;
218 }
219
220 public function setCc($val) {
221 $this->cc = $val;
222 }
223
224 public function setBcc($val) {
225 $this->bcc = $val;
226 }
227
228 229 230 231
232 public function replyTo($email) {
233 $this->addCustomHeader('Reply-To', $email);
234 }
235
236 237 238 239 240 241 242
243 public function ($headerName, $headerValue) {
244 if($headerName == 'Cc') $this->cc = $headerValue;
245 else if($headerName == 'Bcc') $this->bcc = $headerValue;
246 else {
247 if(isset($this->customHeaders[$headerName])) $this->customHeaders[$headerName] .= ", " . $headerValue;
248 else $this->customHeaders[$headerName] = $headerValue;
249 }
250 }
251
252 public function BaseURL() {
253 return Director::absoluteBaseURL();
254 }
255
256 257 258
259 public function debug() {
260 $this->parseVariables();
261
262 return "<h2>Email template $this->class</h2>\n" .
263 "<p><b>From:</b> $this->from\n" .
264 "<b>To:</b> $this->to\n" .
265 "<b>Cc:</b> $this->cc\n" .
266 "<b>Bcc:</b> $this->bcc\n" .
267 "<b>Subject:</b> $this->subject</p>" .
268 $this->body;
269 }
270
271 272 273 274 275
276 public function setTemplate($template) {
277 $this->ss_template = $template;
278 }
279
280 281 282
283 public function getTemplate() {
284 return $this->ss_template;
285 }
286
287 protected function templateData() {
288 if($this->template_data) {
289 return $this->template_data->customise(array(
290 "To" => $this->to,
291 "Cc" => $this->cc,
292 "Bcc" => $this->bcc,
293 "From" => $this->from,
294 "Subject" => $this->subject,
295 "Body" => $this->body,
296 "BaseURL" => $this->BaseURL(),
297 "IsEmail" => true,
298 ));
299 } else {
300 return $this;
301 }
302 }
303
304 305 306
307 public function IsEmail() {
308 return true;
309 }
310
311 312 313 314
315 function populateTemplate($data) {
316 if($this->template_data) {
317 $this->template_data = $this->template_data->customise($data);
318 } else {
319 if(is_array($data)) $data = new ArrayData($data);
320 $this->template_data = $this->customise($data);
321 }
322 $this->parseVariables_done = false;
323 }
324
325 326 327 328 329 330
331 protected function parseVariables($isPlain = false) {
332 SSViewer::set_source_file_comments(false);
333
334 if(!$this->parseVariables_done) {
335 $this->parseVariables_done = true;
336
337
338 $data = $this->templateData();
339
340
341 $fullBody = $this->body;
342 if($this->ss_template && !$isPlain) {
343
344 $data = $this->templateData();
345
346 $template = new SSViewer($this->ss_template);
347
348 if($template->exists()) {
349 $fullBody = $template->process($data);
350 }
351 }
352
353
354 $this->body = HTTP::absoluteURLs($fullBody);
355 }
356 }
357
358 359 360
361 static function validEmailAddress($address) {
362 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);
363 }
364
365 366 367 368 369 370 371 372 373 374
375 function sendPlain($messageID = null) {
376 Requirements::clear();
377
378 $this->parseVariables(true);
379
380 if(empty($this->from)) $this->from = Email::getAdminEmail();
381
382 $this->setBounceHandlerURL($this->bounceHandlerURL);
383
384 $headers = $this->customHeaders;
385
386 $headers['X-SilverStripeBounceURL'] = $this->bounceHandlerURL;
387
388 if($messageID) $headers['X-SilverStripeMessageID'] = project() . '.' . $messageID;
389
390 if(@$_SERVER['REMOTE_ADDR'] && $_SERVER['REMOTE_ADDR'] != '127.0.0.1')
391 $headers['X-SilverStripeClientIP'] = $_SERVER['REMOTE_ADDR'];
392
393 $to = $this->to;
394 $subject = $this->subject;
395 if(self::$send_all_emails_to) {
396 $subject .= " [addressed to $to";
397 $to = self::$send_all_emails_to;
398 if($this->cc) $subject .= ", cc to $this->cc";
399 if($this->bcc) $subject .= ", bcc to $this->bcc";
400 $subject .= ']';
401 } else {
402 if($this->cc) $headers['Cc'] = $this->cc;
403 if($this->bcc) $headers['Bcc'] = $this->bcc;
404 }
405
406 if(self::$cc_all_emails_to) {
407 if(!empty($headers['Cc']) && trim($headers['Cc'])) {
408 $headers['Cc'] .= ', ' . self::$cc_all_emails_to;
409 } else {
410 $headers['Cc'] = self::$cc_all_emails_to;
411 }
412 }
413
414 if(self::$bcc_all_emails_to) {
415 if(!empty($headers['Bcc']) && trim($headers['Bcc'])) {
416 $headers['Bcc'] .= ', ' . self::$bcc_all_emails_to;
417 } else {
418 $headers['Bcc'] = self::$bcc_all_emails_to;
419 }
420 }
421
422 Requirements::restore();
423
424 return self::mailer()->sendPlain($to, $this->from, $subject, $this->body, $this->attachments, $headers);
425 }
426
427 428 429 430 431 432 433 434 435 436
437 public function send($messageID = null) {
438 Requirements::clear();
439
440 $this->parseVariables();
441
442 if(empty($this->from)) $this->from = Email::getAdminEmail();
443
444 $this->setBounceHandlerURL( $this->bounceHandlerURL );
445
446 $headers = $this->customHeaders;
447
448 $headers['X-SilverStripeBounceURL'] = $this->bounceHandlerURL;
449
450 if($messageID) $headers['X-SilverStripeMessageID'] = project() . '.' . $messageID;
451
452 if(@$_SERVER['REMOTE_ADDR'] && $_SERVER['REMOTE_ADDR'] != '127.0.0.1')
453 $headers['X-SilverStripeClientIP'] = $_SERVER['REMOTE_ADDR'];
454
455 $to = $this->to;
456 $subject = $this->subject;
457 if(self::$send_all_emails_to) {
458 $subject .= " [addressed to $to";
459 $to = self::$send_all_emails_to;
460 if($this->cc) $subject .= ", cc to $this->cc";
461 if($this->bcc) $subject .= ", bcc to $this->bcc";
462 $subject .= ']';
463 unset($headers['Cc']);
464 unset($headers['Bcc']);
465 } else {
466 if($this->cc) $headers['Cc'] = $this->cc;
467 if($this->bcc) $headers['Bcc'] = $this->bcc;
468 }
469
470 if(self::$cc_all_emails_to) {
471 if(!empty($headers['Cc']) && trim($headers['Cc'])) {
472 $headers['Cc'] .= ', ' . self::$cc_all_emails_to;
473 } else {
474 $headers['Cc'] = self::$cc_all_emails_to;
475 }
476 }
477
478 if(self::$bcc_all_emails_to) {
479 if(!empty($headers['Bcc']) && trim($headers['Bcc'])) {
480 $headers['Bcc'] .= ', ' . self::$bcc_all_emails_to;
481 } else {
482 $headers['Bcc'] = self::$bcc_all_emails_to;
483 }
484 }
485
486 Requirements::restore();
487
488 return self::mailer()->sendHTML($to, $this->from, $subject, $this->body, $this->attachments, $headers, $this->plaintext_body);
489 }
490
491 492 493 494 495 496 497 498 499
500 public static function setAdminEmail($newEmail) {
501 self::$admin_email_address = $newEmail;
502 }
503
504 505 506
507 public static function getAdminEmail() {
508 return self::$admin_email_address;
509 }
510
511 512 513 514 515 516 517
518 public static function send_all_emails_to($emailAddress) {
519 self::$send_all_emails_to = $emailAddress;
520 }
521
522 523 524 525 526 527 528 529 530 531
532 public static function cc_all_emails_to($emailAddress) {
533 self::$cc_all_emails_to = $emailAddress;
534 }
535
536 537 538 539 540 541 542 543 544 545
546 public static function bcc_all_emails_to($emailAddress) {
547 self::$bcc_all_emails_to = $emailAddress;
548 }
549
550 551 552 553 554 555 556 557 558 559 560
561 function is_valid_address($email){
562 $qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]';
563 $dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]';
564 $atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c'.
565 '\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+';
566 $quoted_pair = '\\x5c[\\x00-\\x7f]';
567 $domain_literal = "\\x5b($dtext|$quoted_pair)*\\x5d";
568 $quoted_string = "\\x22($qtext|$quoted_pair)*\\x22";
569 $domain_ref = $atom;
570 $sub_domain = "($domain_ref|$domain_literal)";
571 $word = "($atom|$quoted_string)";
572 $domain = "$sub_domain(\\x2e$sub_domain)*";
573 $local_part = "$word(\\x2e$word)*";
574 $addr_spec = "$local_part\\x40$domain";
575
576 return preg_match("!^$addr_spec$!", $email) ? 1 : 0;
577 }
578
579 580 581 582 583 584 585 586 587 588 589 590 591 592
593 public static function obfuscate($email, $method = 'visible') {
594 switch($method) {
595 case 'direction' :
596 Requirements::customCSS(
597 'span.codedirection { unicode-bidi: bidi-override; direction: rtl; }',
598 'codedirectionCSS'
599 );
600 return '<span class="codedirection">' . strrev($email) . '</span>';
601 case 'visible' :
602 $obfuscated = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ');
603 return strtr($email, $obfuscated);
604 case 'hex' :
605 $encoded = '';
606 for ($x=0; $x < strlen($email); $x++) $encoded .= '&#x' . bin2hex($email{$x}).';';
607 return $encoded;
608 default:
609 user_error('Email::obfuscate(): Unknown obfuscation method', E_USER_NOTICE);
610 return $email;
611 }
612 }
613 }
614
615 616 617 618 619
620 class Email_BounceHandler extends Controller {
621
622 function init() {
623 BasicAuth::protect_entire_site(false);
624 parent::init();
625 }
626
627 function index() {
628 $subclasses = ClassInfo::subclassesFor( $this->class );
629 unset($subclasses[$this->class]);
630
631 if( $subclasses ) {
632 $subclass = array_pop( $subclasses );
633 $task = new $subclass();
634 $task->index();
635 return;
636 }
637
638
639 if( !isset($_REQUEST['Key']) ) {
640 echo 'Error: Access validation failed. No "Key" specified.';
641 return;
642 }
643
644
645 if( $_REQUEST['Key'] != EMAIL_BOUNCEHANDLER_KEY) {
646 echo 'Error: Access validation failed. Invalid "Key" specified.';
647 return;
648 }
649
650 if( !$_REQUEST['Email'] ) {
651 echo "No email address\n";
652 return;
653 }
654
655 $this->recordBounce( $_REQUEST['Email'], $_REQUEST['Date'], $_REQUEST['Time'], $_REQUEST['Message'] );
656 }
657
658 private function recordBounce( $email, $date = null, $time = null, $error = null ) {
659 if(ereg('<(.*)>', $email, $parts)) $email = $parts[1];
660
661 if( !$date )
662 $date = date( 'Y-m-d' );
663 else
664 $date = date( 'Y-m-d', strtotime( $date ) );
665
666 if( !$time )
667 $time = date( 'H:i:s' );
668 else
669 $time = date( 'H:i:s', strtotime( $time ) );
670
671 $SQL_email = Convert::raw2sql($email);
672 $SQL_bounceTime = Convert::raw2sql("$date $time");
673
674 $duplicateBounce = DataObject::get_one("Email_BounceRecord", "\"BounceEmail\" = '$SQL_email' AND (\"BounceTime\"+INTERVAL 1 MINUTE) > '$SQL_bounceTime'");
675
676 if(!$duplicateBounce) {
677 $record = new Email_BounceRecord();
678
679 $member = DataObject::get_one( 'Member', "\"Email\"='$SQL_email'" );
680
681 if( $member ) {
682 $record->MemberID = $member->ID;
683
684
685
686 if( isset($_REQUEST['SilverStripeMessageID'])) {
687
688 $message_id_parts = explode('.', $_REQUEST['SilverStripeMessageID']);
689
690 $newsletter_id_date_parts = explode ('_', base64_decode($message_id_parts[1]) );
691
692
693 $SQL_memberID = Convert::raw2sql($member->ID);
694 $SQL_newsletterID = Convert::raw2sql($newsletter_id_date_parts[0]);
695
696
697 $oldNewsletterSentRecipient = DataObject::get_one("Newsletter_SentRecipient", "\"MemberID\" = '$SQL_memberID' AND \"ParentID\" = '$SQL_newsletterID' AND \"Email\" = '$SQL_email'");
698
699
700 if($oldNewsletterSentRecipient) {
701 $oldNewsletterSentRecipient->Result = 'Bounced';
702 $oldNewsletterSentRecipient->write();
703 } else {
704
705 $newNewsletterSentRecipient = new Newsletter_SentRecipient();
706 $newNewsletterSentRecipient->Email = $SQL_email;
707 $newNewsletterSentRecipient->MemberID = $member->ID;
708 $newNewsletterSentRecipient->Result = 'Bounced';
709 $newNewsletterSentRecipient->ParentID = $newsletter_id_date_parts[0];
710 $newNewsletterSentRecipient->write();
711 }
712
713
714
715 $member->setBlacklistedEmail(TRUE);
716 echo '<p><b>Member: '.$member->FirstName.' '.$member->Surname.' <'.$member->Email.'> was added to the Email Blacklist!</b></p>'."\n";
717 }
718 }
719 $record->BounceEmail = $email;
720 $record->BounceTime = $date . ' ' . $time;
721 $record->BounceMessage = $error;
722 $record->write();
723
724 echo "Handled bounced email to address: $email\n";
725 } else {
726 echo "Sorry, this bounce report has already been logged, not logging this duplicate bounce.\n";
727 }
728 }
729
730 }
731
732 733 734 735 736
737 class Email_BounceRecord extends DataObject {
738 static $db = array(
739 'BounceEmail' => 'Varchar',
740 'BounceTime' => 'SS_Datetime',
741 'BounceMessage' => 'Varchar'
742 );
743
744 static $has_one = array(
745 'Member' => 'Member'
746 );
747
748 static $has_many = array();
749
750 static $many_many = array();
751
752 static $defaults = array();
753
754 static $singular_name = 'Email Bounce Record';
755
756
757 758 759 760
761 public function canCreate($member = null) {
762 return false;
763 }
764 }
765
766 ?>
767
[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the
Silverstripe Forums to ask development related questions.
-