Some input desired

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Some input desired

Post by Chris Corbyn »

Hiya all!

I'm writing a mailer class (yeah I know phpMailer exists). I'm wondering what things you'd like to see support for in something like this if you were to make use of it.

So far it can (easily):

Send email without needing mail()
Use custom mail headers
Send Single-part emails with any content-type
Send multipart/aternative emails (with custom boundary or calculated ones)
Send file attachments of any type, with any file name
Encode emails to a handful of transfer-encoding types
Supports nesting multipart content as many times as neeeded
Send batch email through a single connection to the server

Things I'm thinking I need to add are:

Support for SMTP servers that use authentication
Sendmail binary (or other MTA support)

I'd love some suggestions on things I may not have considered :)
timvw
DevNet Master
Posts: 4897
Joined: Mon Jan 19, 2004 11:11 pm
Location: Leuven, Belgium

Post by timvw »

Show us the interface how we would have to use it...
Apart from support for SSL it seems like a complete list.
User avatar
Oren
DevNet Resident
Posts: 1640
Joined: Fri Apr 07, 2006 5:13 am
Location: Israel

Post by Oren »

Does it have some kind of rich text editor?
Can we view attached files without having to download them first? (like in Gmail where I can see images without having to download it first)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

timvw wrote:Show us the interface how we would have to use it...
Apart from support for SSL it seems like a complete list.
I wasn't sure about SSL. I don't want to show any actual code from the class yet but here's an example of sending a multipart message from it:

Code: Select all

$m = new mailer('mail.myserver.co.uk'); //The SMTP server we're using (can set a port too)
$m->setDomain('mydomain.com'); //This is for the HELO
$m->connect();
//Adding parts to the message is this easy
$m->addPart("<strong>This is the first part</strong>", 'text/html', 'base64');
$m->addPart('This is the second part'); //Default is text/plain 7bit

//Send the message to
$m->send('user@somedomain.com', 'me@mydomain.com', 'Example mail');
$m->disconnect();
Oh I forgot to add... you get to see all the commands that were sent along with the responses received if you look at $mail->transactions (and array):

Code: Select all

Array
(
    [0] => Array
        (
            [command] => 
            [time] => 0.26490100 1146327485
            [response] => 220 server.co.uk ESMTP Exim 4.50 Sat, 29 Apr 2006 16:18:05 +0000

        )

    [1] => Array
        (
            [command] => HELO manc.cable.ntl.com

            [time] => 0.36500200 1146327485
            [response] => 250 w3style.co.uk Hello cpc3-salf2-0-0-cust61.manc.cable.ntl.com [82.7.240.62]

        )

    [2] => Array
        (
            [command] => mail from: user@me.ac

            [time] => 0.39271600 1146327485
            [response] => 250 OK

        )

    [3] => Array
        (
            [command] => rcpt to: user@domain.co.uk

            [time] => 0.42951600 1146327485
            [response] => 250 Accepted

        )

    [4] => Array
        (
            [command] => data

            [time] => 0.47451400 1146327485
            [response] => 354 Enter message, ending with "." on a line by itself

        )

    [5] => Array
        (
            [command] => From: user@me.ac
Reply-To: user@me.ac
To: user@domain.co.uk
Subject: Example mail
Date: Sat, 29 Apr 2006 17:18:05 +0100
X-Mailer: dMail by Chris Corbyn
MIME-Version: 1.0
Content-Type: multipart/alternative;
	boundary="dMail-A299C3E17345ED40F80F3890EE9F33A4"
Content-Transfer-Encoding: 7bit

This part of the E-mail should never be seen. If
you are reading this, consider upgrading your e-mail
client to a MIME-compatible client.

--dMail-A299C3E17345ED40F80F3890EE9F33A4
Content-Type: text/html
Content-Transfer-Encoding: base64

PHN0cm9uZz5UaGlzIGlzIHRoZSBmaXJzdCBwYXJ0PC9zdHJvbmc+

--dMail-A299C3E17345ED40F80F3890EE9F33A4
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

This is the second part

--dMail-A299C3E17345ED40F80F3890EE9F33A4--
.

            [time] => 0.50442100 1146327485
            [response] => 250 OK id=1FZs8z-0007zW-Hk

        )

)

If you want to send just a single-part email you don't do that addPart() bit... you just add a thrid parameter to send() which contains the message you want to send.... the mailer class detects what you're trying to do ;)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Oren wrote:Does it have some kind of rich text editor?
Can we view attached files without having to download them first? (like in Gmail where I can see images without having to download it first)
*cough* It's a mailer class for sending mail.... it not an application with a front-end.
User avatar
Oren
DevNet Resident
Posts: 1640
Joined: Fri Apr 07, 2006 5:13 am
Location: Israel

Post by Oren »

Well yeah... That's what I thought at the beginning, but then I saw this and got confused:
timvw wrote:Show us the interface how we would have to use it...
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

d11wtq wrote:I don't want to show any actual code from the class yet but here's an example of sending a multipart message from it:
Go ahead and show us some code. I like the direction you are going. I would go for clean and small, supporting most general features (80/20) but not go crazy because there are things like phpMailer for that. Maybe have a base class and allow less commonly used features to be added by composition or extension.
(#10850)
d3ad1ysp0rk
Forum Donator
Posts: 1661
Joined: Mon Oct 20, 2003 8:31 pm
Location: Maine, USA

Post by d3ad1ysp0rk »

Oren wrote:Well yeah... That's what I thought at the beginning, but then I saw this and got confused:
timvw wrote:Show us the interface how we would have to use it...
Refers to the way something interfaces with something else. In this case, the way the PHP code interfaces with the mail class.
User avatar
Oren
DevNet Resident
Posts: 1640
Joined: Fri Apr 07, 2006 5:13 am
Location: Israel

Post by Oren »

Well... Ok... If you say so :P
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

d11wtq wrote:Oh I forgot to add... you get to see all the commands that were sent along with the responses received if you look at $mail->transactions (and array):
While nice, I think this is an example of additional functionality that may be rarely used I would recommend separating it (if it does not add complexity to the code) and do something like:

Code: Select all

$email = new Email();
$logger = new EmailLogger();
$email->setLogger($logger);
$email->send();

$transactions = $logger->info();
Here is the classic log4j interface, you could probably do something simpler:

Code: Select all

public class Logger {

    // Creation & retrieval methods:
    public static Logger getRootLogger();
    public static Logger getLogger(String name);

    // printing methods:
    public void debug(Object message);
    public void info(Object message);
    public void warn(Object message);
    public void error(Object message);
    public void fatal(Object message);

    // generic printing method:
    public void log(Level l, Object message);
}
So let's see some code.
Last edited by Christopher on Sun Apr 30, 2006 1:43 pm, edited 2 times in total.
(#10850)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

arborint wrote:
d11wtq wrote:I don't want to show any actual code from the class yet but here's an example of sending a multipart message from it:
Go ahead and show us some code. I like the direction you are going. I would go for clean and small, supporting most general features (80/20) but not go crazy because there are things like phpMailer for that. Maybe have a base class and allow less commonly used features to be added by composition or extension.
OK then... current (unfinished) code below. I wanted to make something light yes, but even with a small class like this you can acheive pretty much all the standard things you'd get from your mail client in terms of what you can send.

I don't think.... the way it's laid out I'd say it lends itself to extensions pretty well (some keywords need changing to "protected").

Code: Select all

<?php

/*
 dMail: A Flexible PHP Mailer Class.
 Author: Chris Corbyn
 Date: 29th April 2006
 
 Current functionality:
  
  * Send uses one single connection to the SMTP server
  * Doesn't rely on mail()
  * Custom Headers
  * Sends Multipart messages, handles encoding
  * Sends Plain-text single-part emails
  * Batch emailing
  * Support for attachments
 
 Things to add:
 
  * Basic SMTP Authentication
  * SSL (Not sure?)
  * Sendmail binary support
 
 -----------------------------------------------------------------------

	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 2 of the License, or
    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, write to the Free Software
    Foundation, Inc.,
    51 Franklin Street,
    Fifth Floor,
    Boston,
    MA  02110-1301  USA
    
    "Chris Corbyn" <chris@w3style.co.uk>

 */

class mailer
{
	private
	
	$server,
	$socket,
	$port,
	$connectTimeout = 30,
	$headers = "X-Mailer: dMail by Chris Corbyn\r\n",
	$domain = 'dMailUser',
	$mimeBoundary,
	$mimeWarning,
	$parts = array(),
	$attachments = array();
	
	public
	
	//Just logging stuff
	$errors = array(),
	$transactions = array(),
	$lastResponse;
	
	public function __construct($server, $port=25)
	{
		$this->server = $server;
		$this->port = $port;

		$this->mimeWarning = "This part of the E-mail should never be seen. If\r\n".
		"you are reading this, consider upgrading your e-mail\r\n".
		"client to a MIME-compatible client.";
	}

	private function getMimeBoundary($string=false)
	{
		$force = true;
		
		if (!$string)
		{
			$force = false;
			$string = implode('', $this->parts);
			$string .= implode('', $this->attachments);
		}
		
		if ($this->mimeBoundary && !$force) return $this->mimeBoundary;
		else
		{ //Make sure we don't (as if it would ever happen!) -
		  // produce a hash that's actually in the email already
			do
			{
				$this->mimeBoundary = 'dMail-'.strtoupper(md5($string));
			} while(strpos($string, $this->mimeBoundary));
		}
		return $this->mimeBoundary;
	}

	public function setDomain($domain)
	{
		$this->domain = $domain;
	}

	public function addHeaders($string)
	{
		$this->headers .= $string;
	}

	public function setMimeBoundary($string)
	{
		$this->mimeBoundary = $string;
	}

	public function setMimeWarning($warning)
	{
		$this->mimeWarning = $warning;
	}

	public function setConnectTimeout($secs)
	{
		$this->connectTimeout = $secs;
	}

	//When a multipart message has been sent, and the user wants to
	// send a different message through the same connection
	public function flush($clear_headers=false)
	{
		$this->parts = array();
		$this->attachments = array();
		$this->mimeBoundary = null;
		if ($clear_headers) $this->headers = "X-Mailer: dMail by Chris Corbyn\r\n";
	}

	public function connect()
	{
		$this->socket = @fsockopen($this->server, $this->port, $errno, $errstr, $this->connectTimeout);
		if (!$this->socket)
		{
			$this->logError($errstr, $errno);
			return false;
		}
		//What did the server greet us with on connect?
		$this->logTransaction();
		//Just being polite
		$this->command("HELO {$this->domain}\r\n");
		return true;
	}

	//Dish out the appropriate commands to send an email through the server
	public function send($to, $from, $subject, $body=false, $type='text/plain')
	{
		if (!is_array($to)) $to = array($to);
		
		foreach ($to as $address)
		{
			$data = $this->buildMail($address, $from, $subject, $body, $type);
			
			foreach ($data as $command)
			{
				if (!$this->command($command)) return false;
			}
		}
		return true;
	}

	//Connect errors, timeouts, anything that went wrong but
	// wasn't brought up by SMTP itself
	private function logError($errstr, $errno=0)
	{
		$this->errors[] = array(
			'num' => $errno,
			'time' => microtime(),
			'message' => $errstr
		);
	}

	//Keep track of everything that gets said
	private function logTransaction($command='')
	{
		$this->transactions[] = array(
			'command' => $command,
			'time' => microtime(),
			'response' => $this->getResponse()
		);
	}

	//Read back the data from our socket
	private function getResponse()
	{
		if (!$this->socket) return false;
		
		$ret = fgets($this->socket);
		$this->lastResponse = $ret;
		return $ret;
	}

	public function command($comm)
	{
		if (!$this->socket) return false;
		
		//SMTP commands must end with a return
		if (substr($comm, -2) != "\r\n") $comm .= "\r\n";
		
		if (@fwrite($this->socket, $comm))
		{
			$this->logTransaction($comm);
			return $this->lastResponse;
		}
		else return false;
	}

	//Add a part to a multipart message
	public function addPart($string, $type='text/plain', $encoding='7bit')
	{
		$ret = "Content-Type: $type\r\n".
				"Content-Transfer-Encoding: $encoding\r\n\r\n".
				$this->encode($string, $encoding);
		$this->parts[] = $ret;
	}

	//Attachments are added as base64 encoded data
	public function addAttachment($data, $filename, $type)
	{
		$ret = "Content-Type: $type; ".
				"name="$filename";\r\n".
				"Content-Transfer-Encoding: base64\r\n".
				"Content-Disposition: attachment;\r\n\r\n".
				chunk_split($this->encode($data, 'base64'));
		$this->attachments[] = $ret;
	}

	public function disconnect()
	{
		if ($this->socket) fclose($this->socket);
	}

	private function encode($string, $type)
	{
		$type = strtolower($type);
		
		switch ($type)
		{
			case 'base64':
			$string = base64_encode($string);
			break;
			//
			case '7bit':
			default:
			break;
			//
		}
		
		return $string;
	}

	//Builds the list of commands to send the email
	private function buildMail($to, $from, $subject, $body, $type='text/plain')
	{
		$date = date('r'); //RFC 2822

		if ($body) //Ignore multipart messaging
		{
			$data = "Subject: $subject\r\n".
				"From: $from\r\n".
				"Reply-To: $from\r\n".
				"To: $to\r\n".
				"Date: $date\r\n".
				"{$this->headers}\r\n".
				"Content-Type: $type\r\n".
				"Content-Transfer-Encoding: 7bit\r\n\r\n".
				"$body";
		}
		else //Build a multipart message
		{
			$boundary = $this->getMimeBoundary(); //Overall MIME boundary
			
			$message_body = implode("\r\n\r\n--$boundary\r\n", $this->parts);

			if (!empty($this->attachments)) //Make a sub-message that contains attachment data
			{
				$attachment_boundary = $this->getMimeBoundary(implode('', $this->attachments));
				
				$attachments = implode("\r\n\r\n--$attachment_boundary\r\n", $this->attachments);
				
				$attachments = "\r\n\r\n--$boundary\r\n".
				"Content-Type: multipart/alternative;\r\n".
				"	boundary="$attachment_boundary"\r\n".
				"Content-Transfer-Encoding: 7bit\r\n\r\n".
				"\r\n\r\n--$attachment_boundary\r\n".
				$attachments.
				"\r\n--$attachment_boundary--\r\n";
			}
			
			$data = "From: $from\r\n".
				"Reply-To: $from\r\n".
				"To: $to\r\n".
				"Subject: $subject\r\n".
				"Date: $date\r\n".
				"{$this->headers}".
				"MIME-Version: 1.0\r\n".
				"Content-Type: multipart/mixed;\r\n".
				"	boundary="{$boundary}"\r\n".
				"Content-Transfer-Encoding: 7bit\r\n\r\n";

			$data .= $this->mimeWarning;
			
			if (isset($attachments)) $message_body .= $attachments;
			
			$data .= "\r\n\r\n--$boundary\r\n".
				"$message_body\r\n".
				"\r\n--$boundary--";
		}

		$data .= "\r\n.\r\n";
		
		$ret = array(
			//Can be spoofed but that's not my problem
			"mail from: $from\r\n",
			//Recipient
			"rcpt to: $to\r\n",
			//Inform that we're about the give our message
			"data\r\n",
			//Message body
			$data
		);

		return $ret;
	}
}

?>
Last edited by Chris Corbyn on Sun Apr 30, 2006 3:19 pm, edited 1 time in total.
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

arborint wrote:
d11wtq wrote:Oh I forgot to add... you get to see all the commands that were sent along with the responses received if you look at $mail->transactions (and array):
While nice, I think this is an example of additional functionality that may be rarely used I would recommend separating it (if it does not add complexity to the code)
Yeah I see your point... the overhead of doing it is really small though and it's incredibly simple to do so I'll probably keep it... besides... it helps in debugging if something went wrong and the server complains ;)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

d11wtq wrote:Yeah I see your point... the overhead of doing it is really small though and it's incredibly simple to do so I'll probably keep it... besides... it helps in debugging if something went wrong and the server complains ;)
It is not the data gathering that is "the overhead" it is all the nice output features that a logger gives you that should be separate.
(#10850)
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

arborint wrote:
d11wtq wrote:Yeah I see your point... the overhead of doing it is really small though and it's incredibly simple to do so I'll probably keep it... besides... it helps in debugging if something went wrong and the server complains ;)
It is not the data gathering that is "the overhead" it is all the nice output features that a logger gives you that should be separate.
But the data is only stored in an array, there's no output provided by the class... an extension could be written to handle the display, or even keep these log in text files... Maybe I'm misunderstanding what you mean? :)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

Looks good. I would need to look through the code closer to give specific feedback. A couple of thoughts though:

- Should send() auto-connect if connect() has not been called?

- And likewise, should send() auto-flush under certain circumstances?

I am also thinking that perhaps it should cache the email it builds and then just fill in the to, from and subject. It seems like either you are going to provide a new body or parts each send() so you would auto-flush, or you are going to send the same email to many recipients where you just keep calling send(to, from and subject).

Crazy idea: What if the buildMail() function simply built a template that it cached. Then send() could just do a str_replace() on "{to}", "{from}" and "{subject}", but you could also embed other tags to customize each email send out like "{name}" or "{userid}". So you could do:

Code: Select all

m = new mailer('mail.myserver.co.uk'); //The SMTP server we're using (can set a port too)

$m->set('name', 'Bob Smith');
$m->set('orderID', '54321');
$m->send('bsmith@somedomain.com', 'me@mydomain.com', 'Hello Bob');

$m->set('name', 'Jane Jones');
$m->set('orderID', '12345');
$m->send('jjones@otherdomain.com', 'me@mydomain.com', 'Hello Jane');
Internally it could just use set() for to, from and subject as well. They would send emails:
Dear Bob Smith,

Your order confirmation number is 54321
Dear Jane Jones,

Your order confirmation number is 12345
But the following would implicitly flush and recreate a new email each time:

Code: Select all

$m->send('bsmith@somedomain.com', 'me@mydomain.com', 'Hello Bob', 'Email to Bob');
$m->send('jjones@otherdomain.com', 'me@mydomain.com', 'Hello Jane', 'Different email to Jane');
(#10850)
Post Reply