Created
October 22, 2011 18:12
-
-
Save thijzert/1306299 to your computer and use it in GitHub Desktop.
Fancy Mailer - PHP SMTP wrapper capable of sending multipart plaintext/html messages
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* | |
* fancy_mail.inc - Sends e-mails in multipart/alternative à la Gmail | |
* Copyright (C) 2008 Thijs van Dijk | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License as published by | |
* the Free Software Foundation, either version 3 of the License, or | |
* (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |
* | |
*/ | |
# Mail types: TEXT sends mail in plaintext format, HTML sends mail in html, | |
# and BOTH sends mail - you guessed it - in both HTML and plaintext format. | |
define( "MAIL_TYPE_TEXT", 1 ); | |
define( "MAIL_TYPE_HTML", 2 ); | |
define( "MAIL_TYPE_BOTH", 3 ); | |
# Enable/disable debug messages | |
define( "DEBUG_FANCY_MAIL", false ); | |
class FancyMail | |
{ | |
protected $subject; | |
private $from; | |
protected $contents; | |
protected $recipients; | |
protected $cc; | |
protected $bcc; | |
protected $type; | |
protected $files; | |
private $separator; | |
protected $html_header; | |
protected $html_footer; | |
protected $text_header; | |
protected $text_footer; | |
protected $smtp_server; | |
protected $smtp_port; | |
protected $smtp_ssl; | |
protected $smtp_username; | |
protected $smtp_password; | |
########################################################################### | |
# Fancymail::FancyMail - Constructor. | |
public function __construct( $Type = MAIL_TYPE_BOTH ) | |
{ | |
$this->subject = ""; # String | |
$this->from = null; # Address | |
$this->contents = array(); # Segment[] | |
$this->recipients = array(); # Address[] | |
$this->cc = array(); # Address[] | |
$this->bcc = array(); # Address[] | |
$this->files = array(); # String[] | |
$this->type = $Type; # MAIL_TYPE, although strictly speaking an integer | |
$this->separator = null; | |
$this->html_header = ""; | |
$this->html_footer = ""; | |
$this->text_header = ""; | |
$this->text_footer = ""; | |
$this->smtp_server = null; | |
$this->smtp_port = null; | |
$this->smtp_ssl = false; | |
$this->smtp_username = null; | |
$this->smtp_password = null; | |
} | |
########################################################################### | |
# Fancymail::addHeading - Adds a heading segment. | |
public function addHeading( $Title ) | |
{ | |
$this->contents[] = new HeadingSegment( utf8_decode($Title) ); | |
} | |
########################################################################### | |
# Fancymail::addParagraph - Adds a paragraph segment. | |
public function addParagraph( $Text="" ) | |
{ | |
$this->contents[] = new ParagraphSegment( utf8_decode($Text) ); | |
} | |
########################################################################### | |
# Fancymail::addQuote - Adds a quote | |
public function addQuote( $Text="" ) | |
{ | |
$this->contents[] = new QuoteSegment( utf8_decode($Text) ); | |
} | |
########################################################################### | |
# Fancymail::addImage - Adds an image segment. | |
public function addImage( $location, $alt="" ) | |
{ | |
$this->contents[] = new ImageSegment( "$location\n$alt" ); | |
} | |
########################################################################### | |
# Fancymail::addText - appends the last made segment | |
public function addText( $Text="" ) | |
{ | |
$a = count($this->contents); | |
$this->contents[$a-1]->Append( utf8_decode($Text) ); | |
} | |
########################################################################### | |
# Fancymail::setFrom - sets the source of the email | |
public function setFrom( $From, $Name=null ) | |
{ | |
$this->from = new Address( $From, $Name ); | |
} | |
########################################################################### | |
# Fancymail::getFrom - returns the source of the email | |
public function getFrom() | |
{ | |
return $this->from; | |
} | |
########################################################################### | |
# Fancymail::setSubject - sets the subject | |
public function setSubject( $Subject ) | |
{ | |
$this->subject = $Subject; | |
} | |
########################################################################### | |
# Fancymail::getSubject - returns the subject | |
public function getSubject() | |
{ | |
return $this->subject; | |
} | |
########################################################################### | |
# Fancymail::addRecipient - adds a recipient to the mail ( To: ) | |
public function addRecipient( $Email, $Name=null ) | |
{ | |
$this->recipients[] = new Address( $Email, $Name ); | |
} | |
########################################################################### | |
# Fancymail::addCC - adds a "carbon copy" recipient to the mail ( Cc: ) | |
public function addCC( $Email, $Name=null ) | |
{ | |
$this->cc[] = new Address( $Email, $Name ); | |
} | |
########################################################################### | |
# Fancymail::addBCC - adds a "blind carbon copy" recipient ( Bcc: ) | |
public function addBCC( $Email, $Name=null ) | |
{ | |
$this->bcc[] = new Address( $Email, $Name ); | |
} | |
########################################################################### | |
# Fancymail::attachFile - attaches a file. | |
public function attachFile( $Filename ) | |
{ | |
$this->files[] = $Filename; | |
} | |
########################################################################### | |
# Fancymail::Send - Sends. | |
public function Send() | |
{ | |
$Headers = $this->_getHeaders(); | |
$FinalContent= $this->_getFinalContent(); | |
$To = $this->_getRecipients(); | |
if ( strlen($this->smtp_server) && is_numeric($this->smtp_port) && strlen($this->smtp_username) && strlen($this->smtp_password) ) | |
{ | |
# Mail via authenticated smtp | |
if( ! $this->_mailOverSMTP( $To, $this->subject, $FinalContent, $Headers ) ) | |
{ | |
print( "Mailing over SMTP failed. Please take a screenshot of this message, " . | |
"print it, put the printed page on a wooden table, take a picture " . | |
"with you phone, MMS it to your blog, and e-mail a link to the " . | |
"following recipients:<br /><br />\n" ); | |
print( "To:$To\n$Headers$FinalContent" ); | |
} | |
} | |
else | |
{ | |
# Mail via sendmail. | |
if( ! mail( $To, $this->subject, $FinalContent, $Headers ) ) | |
{ | |
print( "Mailing failed. Please take a screenshot of this message, " . | |
"print it, put the printed page on a wooden table, take a picture " . | |
"with you phone, MMS it to your blog, and e-mail a link to the " . | |
"following recipients:<br /><br />\n" ); | |
print( "To:$To\n$Headers$FinalContent" ); | |
} | |
} | |
} | |
private function _mailOverSMTP( $to, $subject, $body, $headers ) | |
{ | |
$timeout = "5"; | |
$localhost = "127.0.0.1"; | |
$newLine = "\r\n"; | |
$errno = null; | |
$errstr = null; | |
//Connect to the host on the specified port | |
$smtpConnect = fsockopen( | |
$this->smtp_server, | |
$this->smtp_port, | |
$errno, | |
$errstr, | |
$timeout | |
); | |
$smtpResponse = fgets($smtpConnect, 515); | |
if(empty($smtpConnect)) | |
{ | |
throw new Exception( "Failed to connect: $smtpResponse" ); | |
} | |
else | |
{ | |
$logArray['connection'] = "Connected: $smtpResponse"; | |
} | |
//Say Hello to SMTP | |
$logArray['heloresponse'] = $this->_sock_send( $smtpConnect, | |
"HELO $localhost" . $newLine | |
); | |
//Request Auth Login | |
$logArray['authrequest'] = $this->_sock_send( $smtpConnect, | |
"AUTH LOGIN" . $newLine | |
); | |
//Send username | |
$logArray['authusername'] = $this->_sock_send( $smtpConnect, | |
base64_encode($this->smtp_username) . $newLine | |
); | |
//Send password | |
$logArray['authpassword'] = $this->_sock_send( $smtpConnect, | |
base64_encode($this->smtp_password) . $newLine | |
); | |
//Email From | |
$logArray['mailfromresponse'] = $this->_sock_send( $smtpConnect, | |
"MAIL FROM: " . $this->from->email . $newLine | |
); | |
//Email To | |
$fag = @$this->recipients[0]->email; | |
$logArray['mailtoresponse'] = $this->_sock_send( $smtpConnect, | |
"RCPT TO: $fag" . $newLine | |
); | |
//The Email | |
$logArray['data1response'] = $this->_sock_send( $smtpConnect, | |
"DATA" . $newLine | |
); | |
//Construct Message | |
$Message = "To: $to\nSubject: $subject\n$headers\n\n" . str_replace("\r","",$body); | |
$logArray['data2response'] = $this->_sock_send( $smtpConnect, | |
"$Message\r\n.\r\n" | |
); | |
// Say Bye to SMTP | |
$logArray['quitresponse'] = $this->_sock_send( $smtpConnect, | |
"QUIT" . $newLine | |
); | |
//print_r( $logArray ); | |
return ( | |
( substr( $logArray['mailfromresponse'], 0, 3 ) == "250" ) && | |
( substr( $logArray['mailtoresponse'], 0, 3 ) == "250" ) && | |
( substr( $logArray['data2response'], 0, 3 ) == "250" ) && | |
( substr( $logArray['quitresponse'], 0, 3 ) == "221" ) | |
); | |
} | |
private function _sock_send( $socket, $cmd ) | |
{ | |
if ( DEBUG_FANCY_MAIL ) | |
{ | |
print( "[" . trim($cmd) . "] - " ); | |
fputs($socket, $cmd); | |
$smtpResponse = fgets($socket, 515); | |
print( "[" . trim($smtpResponse) . "]\n" ); | |
} | |
else | |
{ | |
fputs($socket, $cmd); | |
$smtpResponse = fgets($socket, 515); | |
} | |
return $smtpResponse; | |
} | |
private function _getRecipients() | |
{ | |
$To = ""; | |
# Create recipient list | |
foreach ( $this->recipients as $rec ) | |
{ | |
$To .= $rec->output() . ", "; | |
} | |
return $To; | |
} | |
private function _getSeparator() | |
{ | |
if ( strlen($this->separator) == 0 ) | |
{ | |
$this->separator = "==_Part_" . md5( date("d-M-y H:i:s").$this->subject ); | |
} | |
return $this->separator; | |
} | |
private function _getHeaders() | |
{ | |
# Create Header | |
$Headers = ""; | |
$Sep = $this->_getSeparator(); | |
# Set From: | |
if ( $this->from != null ) | |
{ | |
$Headers .= "From: " . $this->from->output() . "\n"; | |
} | |
# Set CC's | |
if ( count($this->cc) > 0 ) | |
{ | |
$Headers .= "Cc: "; | |
foreach ( $this->cc as $rec ) | |
{ | |
$Headers .= $rec->output() . ", "; | |
} | |
$Headers .= "\n"; | |
} | |
# Set BCC's | |
if ( count($this->bcc) > 0 ) | |
{ | |
$Headers .= "Bcc: "; | |
foreach ( $this->bcc as $rec ) | |
{ | |
$Headers .= $rec->output() . ", "; | |
} | |
$Headers .= "\n"; | |
} | |
if ( $this->type == MAIL_TYPE_TEXT ) | |
{ | |
if ( count( $this->files ) > 0 ) | |
{ | |
$Headers .= "Content-Type: multipart/alternative;\n boundary=\"$Sep\""; | |
} | |
else | |
{ | |
$Headers .= "Content-Type: text/plain\n"; | |
} | |
} | |
else if ( $this->type == MAIL_TYPE_HTML ) | |
{ | |
if ( count( $this->files ) > 0 ) | |
{ | |
$Headers .= "Content-Type: multipart/alternative;\n boundary=\"$Sep\""; | |
} | |
else | |
{ | |
$Headers .= "Content-Type: text/html\n"; | |
$FinalContent = $HTMLContent; | |
} | |
} | |
else if ( $this->type == MAIL_TYPE_BOTH ) | |
{ | |
$Headers .= "Content-Type: multipart/alternative;\n boundary=\"$Sep\""; | |
} | |
else | |
{ | |
throw new Exception( "Unbeknownst mail type." ); | |
} | |
return $Headers; | |
} | |
private function _getFinalContent() | |
{ | |
$FinalContent= ""; | |
$HTMLContent = $this->_getHTMLContent(); | |
$TEXTContent = $this->_getTextContent(); | |
$Sep = $this->_getSeparator(); | |
if ( $this->type == MAIL_TYPE_TEXT ) | |
{ | |
if ( count( $this->files ) > 0 ) | |
{ | |
$FinalContent = "\n\n--$Sep\n"; | |
$FinalContent .= "Content-Type: text/html; charset=ISO-8859-1\n"; | |
$FinalContent .= "Content-Transfer-Encoding: 7bit\n"; | |
$FinalContent .= "Content-Disposition: inline\n\n"; | |
$FinalContent .= $TEXTContent; | |
return $FinalContent . $this->_addFiles( $Sep ); | |
} | |
else | |
{ | |
return $TEXTContent; | |
} | |
} | |
else if ( $this->type == MAIL_TYPE_HTML ) | |
{ | |
if ( count( $this->files ) > 0 ) | |
{ | |
$FinalContent = "\n\n--$Sep\n"; | |
$FinalContent .= "Content-Type: text/plain; charset=ISO-8859-1\n"; | |
$FinalContent .= "Content-Transfer-Encoding: 7bit\n"; | |
$FinalContent .= "Content-Disposition: inline\n\n"; | |
$FinalContent .= $HTMLContent; | |
return $FinalContent . $this->_addFiles( $Sep ); | |
} | |
else | |
{ | |
return $HTMLContent; | |
} | |
} | |
else if ( $this->type == MAIL_TYPE_BOTH ) | |
{ | |
$FinalContent = "\n\n--$Sep\n"; | |
$FinalContent .= "Content-Type: text/plain; charset=ISO-8859-1\n"; | |
$FinalContent .= "Content-Transfer-Encoding: 7bit\n"; | |
$FinalContent .= "Content-Disposition: inline\n\n"; | |
$FinalContent .= $TEXTContent; | |
$FinalContent .= "\n\n--$Sep\n"; | |
$FinalContent .= "Content-Type: text/html; charset=ISO-8859-1\n"; | |
$FinalContent .= "Content-Transfer-Encoding: 7bit\n"; | |
$FinalContent .= "Content-Disposition: inline\n\n"; | |
$FinalContent .= $HTMLContent; | |
$FinalContent .= $this->_addFiles( $Sep ); | |
return $FinalContent . "\n\n--$Sep--"; | |
} | |
else | |
{ | |
throw new Exception( "Unbeknownst mail type. Yet again." ); | |
} | |
throw new Exception( "Flow of control has reached an unreachable part of the code." ); | |
} | |
private function _getHTMLContent() | |
{ | |
$HTMLContent = $this->html_header; | |
# Render content: | |
foreach ( $this->contents as $Node ) | |
{ | |
# Create HTML Content | |
$HTMLContent .= $Node->OutputHTML(); | |
} | |
$HTMLContent .= $this->html_footer; | |
return $HTMLContent; | |
} | |
private function _getTextContent() | |
{ | |
$TEXTContent = $this->text_header; | |
# Render content: | |
foreach ( $this->contents as $Node ) | |
{ | |
# Create Plaintext content | |
$TEXTContent .= $Node->OutputPlaintext(); | |
} | |
$TEXTContent .= $this->text_footer; | |
return $TEXTContent; | |
} | |
private function _addFiles( $Sep ) | |
{ | |
$rv = ""; | |
foreach ( $this->files as $Path ) | |
{ | |
if ( file_exists( $Path ) ) | |
{ | |
$filename = substr( $Path, strrpos( $Path, "/" ) ); | |
$rv .= "\n\n--$Sep\n"; | |
$rv .= "Content-Type: application/octet-stream; name=\"$filename\"\n"; | |
$rv .= "Content-Transfer-Encoding: base64\n"; | |
$rv .= "X-Attachment-Id: " . md5($filename) . "\n"; | |
$rv .= "Content-Disposition: attachment; filename=\"$filename\"\n\n"; | |
$rv .= base64_encode( file_get_contents( $Path ) ); | |
} | |
} | |
return $rv; | |
} | |
}; | |
# The following classes are used internally, and thus poorly documented. | |
abstract class Segment | |
{ | |
protected $contents; | |
public function __construct( $Text="" ) | |
{ | |
$this->contents = $Text; | |
} | |
public function Append( $Text ) | |
{ | |
$this->contents .= $Text; | |
} | |
public function getContents() | |
{ | |
return $this->contents; | |
} | |
public function setContents( $Text ) | |
{ | |
$this->contents = $Text; | |
} | |
public abstract function OutputHTML(); | |
public abstract function OutputPlaintext(); | |
} | |
class ParagraphSegment extends Segment | |
{ | |
public function OutputHTML() | |
{ | |
$rv = nl2br( htmlentities( $this->contents ) ); | |
$rv = str_ireplace( | |
array( "<i>", "</i>", "<b>", "</b>" ), | |
array( "<i>", "</i>", "<b>", "</b>" ), | |
$rv | |
); | |
return "<p>\n $rv\n</p>\n"; | |
} | |
public function OutputPlaintext() | |
{ | |
$rv = str_ireplace( | |
array( "<i>", "</i>", "<b>", "</b>" ), | |
array( "*", "*", "_", "_" ), | |
$this->contents | |
); | |
return ( $rv . "\n\n" ); | |
} | |
} | |
class HeadingSegment extends Segment | |
{ | |
public function OutputHTML() | |
{ | |
$rv = htmlentities( $this->contents ); | |
return ( "<h2>$rv</h2>\n" ); | |
} | |
public function OutputPlaintext() | |
{ | |
$rv = $this->contents; | |
return ( " --- $rv \n\n\n" ); | |
} | |
} | |
class QuoteSegment extends Segment | |
{ | |
public function OutputHTML() | |
{ | |
$rv = htmlentities( $this->contents ); | |
$rv = str_replace( array("\n","\t"," "), array("<br />\n "," "," "), $rv ); | |
return ( "<div style=\"font-family: Courier; border-left: 1px solid #CCC; margin-left: 10px; padding-left: 3px;\">\n $rv\n</div>\n" ); | |
} | |
public function OutputPlaintext() | |
{ | |
# TODO: >'s | |
return $this->contents; | |
} | |
} | |
class ImageSegment extends Segment | |
{ | |
public function OutputHTML() | |
{ | |
list( $src, $alt ) = explode( "\n", $this->contents ); | |
return ( "<img alt=\"$alt\" src=\"$src\" />\n" ); | |
} | |
public function OutputPlaintext() | |
{ | |
# TODO: Image -> ASCII art | |
// Nah, that's rather stupid | |
# Yes, I suppose you're right. | |
list( $src, $alt ) = explode( "\n", $this->contents ); | |
return ( "$alt\n" ); | |
} | |
public function Append( $Text="" ) | |
{ | |
throw new Exception( "Tried to append to an image. I am not amused." ); | |
} | |
} | |
class Address | |
{ | |
public $email; | |
public $name; | |
function __construct( $Email, $Name="" ) | |
{ | |
$this->email = $Email; | |
$this->name = $Name; | |
} | |
function output( $HTML = false ) | |
{ | |
if ( $HTML ) | |
{ | |
} | |
else | |
{ | |
if ( strlen( $this->name ) >0 ) | |
{ | |
return ( "\"" . $this->name . "\" <" . $this->email . ">" ); | |
} | |
else | |
{ | |
return $this->email; | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment