Skip to content

Instantly share code, notes, and snippets.

@thijzert
Created October 22, 2011 18:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thijzert/1306299 to your computer and use it in GitHub Desktop.
Save thijzert/1306299 to your computer and use it in GitHub Desktop.
Fancy Mailer - PHP SMTP wrapper capable of sending multipart plaintext/html messages
<?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( "&lt;i&gt;", "&lt;/i&gt;", "&lt;b&gt;", "&lt;/b&gt;" ),
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 ","&nbsp;&nbsp;&nbsp;&nbsp;","&nbsp;"), $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