* 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;
}
}
?>