* Example:
 * 
 * if (!it_mail::check_email($to))
 *     die("Invalid email address '$to'\n");
 * 
 * $mail = new it_mail(array('To' => $to, 'From' => 'itools@gna.ch', 'Subject' => 'Example'));
 * $mail->add_body('Plain text message body');
 * $mail->add_body('HTML message with <a href="http://gna.ch/">link</a>', IT_MAIL_HTML);
 * $mail->send();
 * 
 */
class it_mail
{
	var $header_names = array();
	var $header_values = array();
	var $body = array();
	var $attachments = array();
	var $to = array();
	var $subject = "";
	var $cc = array();
	var $bcc = array();
	var $flags = "";
	var $charset = 'iso-8859-1';
/**
 * Construct a new email message. Headers and body can be added later.
 * Note: Headers To, Cc, Bcc can be arrays
 * @param $headers Array of headers for this email, e.g. From, To and Subject
 */
function it_mail($headers = "")
{
	if (is_array($headers))
	{
		foreach ($headers as $header => $value)
			$this->add_header($header, $value);
	}
}
/**
 * Add header line to the this email message. Can be called repeatedly.
 * Note: Headers To, Cc, Bcc can be arrays
 * @param $header Header to be added, e.g. Cc, Bcc or X-My-Header
 * @param $value Value of header, e.g. itools@gna.ch for Bcc header
 */
function add_header($header, $value)
{
	$value = $this->header_escape($value);
	switch ($header)
	{
		case 'To':
			if (is_array($value))
			{
				foreach ($value as $val)
					$this->to[] = $val;
			}
			else
				$this->to[] = $value;
			break;
		case 'Subject':
			$this->subject .= $value;
			break;
		case 'Cc':
			if (is_array($value))
			{
				foreach ($value as $val)
					$this->cc[] = $val;
			}
			else
				$this->cc[] = $value;
			break;
		case 'Bcc':
			if (is_array($value))
			{
				foreach ($value as $val)
					$this->bcc[] = $val;
			}
			else
				$this->bcc[] = $value;
			break;
		case 'charset':
			$this->charset = $value;
			break;
		case 'Errors-To':
			$this->flags = "-f " . escapeshellarg(preg_replace('/.*<([^>]+)>.*/', '$1', $value));
			/* FALLTHROUGH */
		default:
			$this->header_names[] = $header;
			$this->header_values[] = $value;
			break;
	}
}
/**
 * Add body part to this email message. Can be called repeatedly.
 * @param $text Text to be added to email message
 * @param $type Type of text, one of IT_MAIL_PLAIN (default) or IT_MAIL_HTML
 */
function add_body($text, $type = IT_MAIL_PLAIN)
{
	switch ($type)
	{
		case IT_MAIL_PLAIN:
		case IT_MAIL_HTML:
			$this->body[$type] .= $text;
			break;
		default:
			internal_error("it_mail::add_body invalid type $type");
			break;
	}
}
/**
 * Add attachment to this email message. Can be called repeatedly.
 * @param $data Data to be attached
 * @param $name Name of attached file
 * @param $mimetype MIME-Type of attached file
 * @return 'cid:'.Content-ID of this attachment
 */
function add_attachment($data, $mimetype = "application/octet-stream", $name = '')
{
	if ($name == '')
		$name = 'Attachment' . (count($this->attachments) + 1);
	$cid = md5(uniqid(rand()));
	$this->attachments[] = array ('mimetype' => $mimetype, 'data' => $data, 'encode' => $encode, 'name' => $name, 'cid' => $cid);
	return 'cid:'.$cid;
}
/**
 * Add file attachment to this email message. Can be called repeatedly.
 * @param $filename File to be attached
 * @param $name Name of attached file as stored in mail
 * @param $mimetype MIME-Type of attached file
 */
function add_file($filename, $mimetype = "application/octet-stream", $name = '')
{
	if ($name == '')
		$name = basename($filename);
	if ($file = @fopen($filename, "r"))
	{
		if ($data = fread($file, @filesize($filename)))
			$result = $this->add_attachment($data, $mimetype, $name);
		fclose($file);
	}
	return $result;
}
/**
 * Send this email message
 * @return True if mail was sent
 */
function send()
{
	$to = join(",", $this->to);
	$headers = array();
	if (count($this->cc) > 0)
		$headers[] = "Cc: " . join(",", $this->cc);
	if (count($this->bcc) > 0)
		$headers[] = "Bcc: " . join(",", $this->bcc);
	for ($i = 0; $i < count($this->header_names); $i++)
		$headers[] = $this->header_names[$i] . ': ' . $this->header_values[$i];
	/* Automatically add doctype if none given */
	if ($this->body[IT_MAIL_HTML] && !eregi('^body[IT_MAIL_HTML]))
	{
		$this->body[IT_MAIL_HTML] = '' . "\n" . $this->body[IT_MAIL_HTML];
	}
	$headers[] = "MIME-Version: 1.0";
	if ($this->attachments)
	{
		/* Attachments need multipart MIME mail */
		$boundary1 = md5(uniqid(rand()));
		$mixedtype = "Content-Type: multipart/mixed; boundary=\"$boundary1\"";
		$headers[] = $mixedtype;
		$text .= "This is a multi-part message in MIME format.";
		$text .= "\n--$boundary1\n";
	}
	/* Headers for plain and HTML content */
	$plaintype = "Content-Type: text/plain; charset=".$this->charset."\nContent-Transfer-Encoding: 8bit";
	$htmltype = "Content-Type: text/html; charset=".$this->charset."\nContent-Transfer-Encoding: 8bit";
	if ($this->body[IT_MAIL_PLAIN])
	{
		if ($this->body[IT_MAIL_HTML])
		{
			$boundary2 = md5(uniqid(rand()));
			$alternativetype = "Content-Type: multipart/alternative; boundary=\"$boundary2\"";
			/* Plain and HTML */
			if ($this->attachments)
				$text .= "$alternativetype\n\n";
			else
				$headers[] = $alternativetype;
			$text .= "--$boundary2\n$plaintype\n\n";
			$text .= $this->body[IT_MAIL_PLAIN];
			$text .= "\n--$boundary2\n$htmltype\n\n";
			$text .= $this->body[IT_MAIL_HTML];
			$text .= "\n--$boundary2--\n";
		}
		else
		{
			/* Just plain */
			if ($this->attachments)
				$text .= "$plaintype\n\n";
			else
				$headers[] = $plaintype;
			$text .= $this->body[IT_MAIL_PLAIN];
		}
	}
	else if ($this->body[IT_MAIL_HTML])
	{
		/* Just HTML */
		if ($this->attachments)
		{
			if (strstr($this->body[IT_MAIL_HTML], "cid:"))
			{
				$boundary3 = md5(uniqid(rand()));
				$text .= "Content-Type: multipart/related; boundary=\"$boundary3\"\n\n--$boundary3\n";
			}
			$text .= "$htmltype\n\n";
		}
		else
			$headers[] = $htmltype;
		$text .= $this->body[IT_MAIL_HTML];
	}
	if ($this->attachments)
	{
		$text .= "\n";
		$boundary = $boundary3 ? $boundary3 : $boundary1;
		foreach ($this->attachments as $attachment)
		{
			$text .= "\n--$boundary\nContent-Type: {$attachment['mimetype']}; name=\"{$attachment['name']}\"\nContent-Transfer-Encoding: base64\nContent-ID: <{$attachment['cid']}>\nContent-Disposition: inline; filename=\"{$attachment['name']}\"\n\n";
			$text .= chunk_split(base64_encode($attachment['data']));
		}
		if ($boundary3)
			$text .= "\n--$boundary3--\n\n";
		$text .= "--$boundary1--\n";
	}
	return mail($to, $this->subject, $text, join("\n", $headers), $this->flags);
}
/**
 * INTERNAL: Escape header line with ?=iso-8859-1? if necessary, e.g. in Subject line
 * @param $string String to be escaped
 * @return String escape suitable for sending in header line
 */
function header_escape($string)
{
	return preg_replace('/[\x80-\xff]/e', "sprintf('=?iso-8859-1?Q?=%02X?=', ord('\\0'))", $string);
}
/**
 * Make string safe to be used in "$fullname <$email>": Remove illegal
 * characters and enclose in double quotes.
 * @param $string Fulle name to be escaped
 * @return String to be safely used in "$fullname <$email>" for To: etc.
 */
function fullname_escape($string)
{
	return '"' . preg_replace('/["\x00-\x1f]/', '', $string) . '"';
}
/**
 * INTERNAL: Send SMTP command and return true if result is 2XX
 * @param $fp Connection to SMTP server opened using fsockopen
 * @param $command Command to send, e.g. HELO gna.ch
 * @param $anwer String containing full answer from SMTP server
 * @return True if SMTP result code is 2XX
 *
 */
function send_smtp_cmd($fp, $cmd, &$answer, $timeoutok = false)
{
	$result = false;
	$answer = '';
	if (!$cmd || (!feof($fp) && fwrite($fp, "$cmd\r\n")))
	{
		while (!feof($fp) && ereg('^(.)..(.?)(.*)$', fgets($fp, 1024), $regs))
		{
			$answer .= $regs[0];
			if ($regs[2] != '-')	/* Multi line response? */
			{
				if ($regs[1] == 2)
					$result = true;
				break;
			}
		}
	}
	/* A timeout can be considered ok for SLOW mail servers */
	if ($answer == '')
		$result = $timeoutok;
	//echo ">>it_mail::send_smtp_cmd " . time() . " '$cmd' => $result: $answer";
	return $result;
}
/**
 * Check if given email address is valid (syntax, MX record). If you set
 * checkmailbox to true it also connects to MX and asks if mailbox is valid.
 * Note: Connecting to the MX can be slow.
 * @param $email email to be check
 * @param $checkmailbox True if you want to connect to MX and check account
 * @return One of IT_MAIL_CHECKEMAIL_INVALID (0), IT_MAIL_CHECKEMAIL_MAILBOXFULL
 * or IT_MAIL_CHECKEMAIL_OK (both != 0).
 */
function check_email($email, $checkmailbox = true)
{
	$result = IT_MAIL_CHECKEMAIL_INVALID;
	/* Check if username starts with www. or not well-formed => reject */
	if (!ereg('^www\.', $email) && eregi('^[a-z0-9&_+.-]+@([a-z0-9.-]+\.[a-z]+)$', $email, $regs))
	{
		$domain = $regs[1];
		if (!getmxrr($domain, $mxhosts, $weights))
		{
			/* If we find no MX we check if domain exists */
			if (gethostbynamel($domain))
			{
				$mxhosts = array($domain);
				$weights = array(1);
			}
		}
		if ($mxhosts)	/* We need at least one valid MX */
		{
			if ($checkmailbox)
			{
				$mx = array();
				for ($i = 0; $i < count($mxhosts); $i++)
					$mx[$mxhosts[$i]] = $weights[$i];
				asort($mx, SORT_NUMERIC);
				$port = getservbyname('smtp', 'tcp');
				$from = 'itools@gna.ch';
				/* Determine domain and email for test, skip if called as it_mail::check_email() */
				if (is_object($this))
				{
					for ($i = 0; $i < count($this->header_names); $i++)
					{
						if ($this->header_names[$i] == 'From')
						{
							$from = $this->header_values[$i];
							break;
						}
					}
				}
				if (eregi('@([a-z0-9.-]+\.[a-z]+)$', $from, $regs))
					$fromdomain = $regs[1];
				else
					$fromdomain = 'gna.ch';
				$finished = false;
				$connected = 0;
				if (function_exists('stream_set_timeout'))
					$timeout = 'stream_set_timeout';
				else
					$timeout = 'socket_set_timeout';
				foreach ($mx as $mxhost => $weight)
				{
					//echo ">>it_mail::check_email: " . time() . " checking $mxhost\n";
					if ($fp = @fsockopen($mxhost, $port, $errno, $errstr, 5))
					{
						$connected++;
						$timeout($fp, 45);
						$answer = '';
						if (it_mail::send_smtp_cmd($fp, '', $answer) && it_mail::send_smtp_cmd($fp, "HELO $fromdomain", $answer) && it_mail::send_smtp_cmd($fp, "MAIL FROM: <$from>", $answer))
						{
							$timeout($fp, 2);
							$timeoutok = ($domain != 'bluewin.ch');
							if (it_mail::send_smtp_cmd($fp, "RCPT TO: <$email>", $answer, $timeoutok))
								$result = IT_MAIL_CHECKEMAIL_OK;
							else if (eregi('quota|full|exceeded storage', $answer))
								$result = IT_MAIL_CHECKEMAIL_MAILBOXFULL;
							/* Do not try other MX if we got this far */
							$finished = true;
						}
						$timeout($fp, 0, 1);
						it_mail::send_smtp_cmd($fp, 'RSET', $answer);
						it_mail::send_smtp_cmd($fp, 'QUIT', $answer);
						fclose($fp);
					}
					if ($finished)
						break;
				}
				/* If we cannot connect to _any_ MX it could be because of temporary overload / virus attack */
				if (!$connected)
					$result = IT_MAIL_CHECKEMAIL_OK;
			}
			else
				$result = IT_MAIL_CHECKEMAIL_OK;
		}
	}
	return $result;
}
}
?>