summaryrefslogtreecommitdiff
path: root/devel-utf8/it_mail.class
diff options
context:
space:
mode:
authorNathan Gass2012-03-22 18:18:42 +0000
committerNathan Gass2012-03-22 18:18:42 +0000
commitd59a4921188753dbe4c0161081755a28112c3ef6 (patch)
tree81496414d988f37f1db9d92c9750d888ffa13746 /devel-utf8/it_mail.class
parentca11771e8fad5fef96615df4c44e04b8fb60ac31 (diff)
downloaditools-d59a4921188753dbe4c0161081755a28112c3ef6.tar.gz
itools-d59a4921188753dbe4c0161081755a28112c3ef6.tar.bz2
itools-d59a4921188753dbe4c0161081755a28112c3ef6.zip
Branch itools/devel-utf8 created
Diffstat (limited to 'devel-utf8/it_mail.class')
-rw-r--r--devel-utf8/it_mail.class496
1 files changed, 496 insertions, 0 deletions
diff --git a/devel-utf8/it_mail.class b/devel-utf8/it_mail.class
new file mode 100644
index 0000000..5045a44
--- /dev/null
+++ b/devel-utf8/it_mail.class
@@ -0,0 +1,496 @@
+<?php
+/*
+** $Id$
+**
+** Copyright (C) 1995-2007 by the ITools Authors.
+** This file is part of ITools - the Internet Tools Library
+**
+** ITools 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.
+**
+** ITools 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.class - Create a mail object, add header and content and send it
+*/
+
+define('IT_MAIL_PLAIN', 0);
+define('IT_MAIL_HTML', 1);
+
+/* Return codes of it_mail::check_email() */
+define('IT_MAIL_CHECKEMAIL_INVALID', 0);
+define('IT_MAIL_CHECKEMAIL_MAILBOXFULL', 5);
+define('IT_MAIL_CHECKEMAIL_OK', 10);
+
+/**
+ * Construct an email message with headers, body (plaintext and/or HTML) and
+ * send it. Also provides utility function to check email address validity.<br>
+ * On devel/twin machines, will output mail instead of sending it.<br>
+ * <em>Example:</em><br>
+ * <code><nobr>
+ * if (!it_mail::check_email($to))<br>
+ * &nbsp;&nbsp;&nbsp;&nbsp;die("Invalid email address '$to'\n");<br>
+ * <br>
+ * $mail = new it_mail(array('To' => $to&#44; 'From' => 'itools@gna.ch', 'Subject' => 'Example'));<br>
+ * $mail->add_body('Plain text message body');<br>
+ * $mail->add_body('HTML message with &lt;a href="http://gna.ch/"&gt;link&lt;/a&gt;', IT_MAIL_HTML);<br>
+ * $mail->send();<br>
+ * </nobr></code>
+ */
+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;
+
+/**
+ * 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 = array())
+{
+ $this->charset = ini_get('default_charset');
+
+ foreach ((array)$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)
+{
+ switch ($header)
+ {
+ case 'To':
+ foreach ((array)$value as $val)
+ $this->to[] = $this->header_escape($val, true);
+ break;
+
+ case 'Subject':
+ $this->subject .= $value; # Escape on sending
+ break;
+
+ case 'Cc':
+ foreach ((array)$value as $val)
+ $this->cc[] = $this->header_escape($val, true);
+ break;
+
+ case 'Bcc':
+ foreach ((array)$value as $val)
+ $this->bcc[] = $this->header_escape($val, true);
+ 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[] = $this->header_escape($value, $header == 'From');
+ 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:
+ it::fatal("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 $p['dispositon'] optional Content-Disposition value: "inline" or "attachment"
+ * @param $p['mimetype'] optional MIME-Type of attached file
+ * @param $p['name'] optional name of attached file
+ * @return 'cid:'.Content-ID of this attachment
+ */
+function add_attachment($data, $p = array(), $legacy_name = null)
+{
+ if (!is_array($p)) # legacy mode: two scalars mime type / name
+ $p = array('mimetype' => $p) + ($legacy_name ? array('name' => $legacy_name) : array());
+
+ $this->attachments[] = $p + array(
+ 'cid' => $cid = md5(uniqid(rand())),
+ 'data' => $data,
+ 'disposition' => "inline",
+ 'mimetype' => "application/octet-stream",
+ 'name' => "Attachment" . (count($this->attachments) + 1),
+ );
+
+ return "cid:$cid";
+}
+
+
+/**
+ * Add file attachment to this email message. Can be called repeatedly.
+ * @param $filename File to be attached
+ * @param $p['dispositon'] optional Content-Disposition value: "inline" or "attachment"
+ * @param $p['mimetype'] MIME-Type of attached file
+ * @param $p['name'] Name of attached file as stored in mail
+ */
+function add_file($filename, $p = array(), $legacy_name = null)
+{
+ if (!is_array($p)) # legacy mode: two scalars mime type / name
+ $p = array('mimetype' => $p) + ($legacy_name ? array('name' => $legacy_name) : array());
+
+ return $this->add_attachment(file_get_contents($filename), $p + array('name' => basename($filename)));
+}
+
+
+/**
+ * Send this email message
+ * @param $p['forcemail'] Send mail even if on twin or devel machine
+ * @return True if mail was accepted for delivery
+ */
+function send($p = array())
+{
+ $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] && !preg_match('/^<!doctype/i', $this->body[IT_MAIL_HTML]))
+ {
+ $this->body[IT_MAIL_HTML] = '<!doctype html public "-//w3c//dtd html 4.01 transitional//en">' . "\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: {$attachment['disposition']}; filename=\"{$attachment['name']}\"\n\n";
+ $text .= chunk_split(base64_encode($attachment['data']));
+ }
+
+ if ($boundary3)
+ $text .= "\n--$boundary3--\n\n";
+
+ $text .= "--$boundary1--\n";
+ }
+
+ if (it::is_live() || EDC('forcemail') || $p['forcemail'])
+ {
+ if (($result = mail($to, $this->header_escape($this->subject), $text, join("\n", $headers), $this->flags)) === false)
+ it::error(array('title' => "failed sending mail to $to subject $this->subject", 'body' => D($text, $headers, $this->flags)));
+ return $result;
+ }
+ else
+ return ED($to, $this->header_escape($this->subject), $text, $headers, $this->flags);
+}
+
+
+/**
+ * INTERNAL: Escape header line with ?=?<charset>?Q 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, $emailmode = false)
+{
+ # If in emailmode (From, To, Cc, Bcc) then exclude <email@domain.com>
+ # from being encoded as e.g. GMail has problems with that
+ # Otherwise encode whole string at once, i.e. do not split
+ $emailpattern = $emailmode ? '/(\s*<[^>]+@[^>]+>[,\s]*)/' : '/DO_NOT_SLPIT{64738}/';
+
+ foreach (preg_split($emailpattern, $string, -1, PREG_SPLIT_DELIM_CAPTURE) as $part)
+ {
+ # Encode if not email address and contains special chars
+ $result .= !preg_match($emailpattern, $part) && preg_match('/[\x00-\x1f\x7f-\xff]/', $part)
+ ? ("=?{$this->charset}?Q?" . str_replace(" ", "_", preg_replace_callback('/[\x00-\x1f=\x7f-\xff]/', function($m) { return sprintf('=%02X', ord($m[0])); }, $part)) . "?=")
+ : $part;
+ }
+
+ return $result;
+}
+
+
+/**
+ * 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
+ * @param timeoutok Whether a timeout is considered ok
+ * @param $failcode Lowest SMTP result code which is considered a failure (default 300)
+ * @return True if SMTP result code is lower than $failcode
+ *
+ */
+function send_smtp_cmd($fp, $cmd, &$answer, $timeoutok = false, $failcode = 300)
+{
+ $result = false;
+ $answer = '';
+
+ if (!$cmd || (!feof($fp) && fwrite($fp, "$cmd\r\n")))
+ {
+ while (!feof($fp) && preg_match('/^(...)(.?)(.*)[\r\n]*$/', fgets($fp, 1024), $regs))
+ {
+ $answer .= $regs[0];
+ $resultcode = intval($regs[1]);
+
+ if ($regs[2] != '-') /* Multi line response? */
+ {
+ if (($resultcode >= 100) && ($resultcode < $failcode))
+ $result = true;
+
+ break;
+ }
+ }
+ }
+
+ /* A timeout can be considered ok for SLOW mail servers */
+ if ($answer == '')
+ $result = $timeoutok;
+
+ EDC('mailcheck', 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 (!preg_match('/^www\./', $email) && preg_match('/^[a-z0-9&_+.-]+@([a-z0-9.-]+\.[a-z]+)$/i', $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@" . (($domain = it::match('[^.]*\.(.+)', getenv('ULTRAHOSTNAME') ? getenv('ULTRAHOSTNAME') : it::exec('hostname -f 2>/dev/null || hostname 2>/dev/null'))) ? $domain : "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 (preg_match('/@([a-z0-9.-]+\.[a-z]+)$/i', $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 => $dummy_weight)
+ {
+ //echo ">>it_mail::check_email: " . time() . " checking $mxhost\n";
+ if ($fp = @fsockopen($mxhost, $port, $dummy_errno, $dummy_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, 500)) # 450 is often used for Greylisting
+ $result = IT_MAIL_CHECKEMAIL_OK;
+ else if (preg_match('/quota|full|exceeded storage/i', $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;
+}
+
+}
+
+?>