summaryrefslogtreecommitdiff
path: root/it_mail.class
diff options
context:
space:
mode:
authorDavid Flatz2022-07-29 14:18:56 +0200
committerDavid Flatz2022-07-29 14:51:07 +0200
commit744db7c9e5fc89d227c1beb981d01898ce478e41 (patch)
tree64f1fcb4fb2a355e6fa3194008118301383bb7f1 /it_mail.class
parent4640a1e8b286c3ff2785abaddb9e449b023c88a7 (diff)
downloaditools-744db7c9e5fc89d227c1beb981d01898ce478e41.tar.gz
itools-744db7c9e5fc89d227c1beb981d01898ce478e41.tar.bz2
itools-744db7c9e5fc89d227c1beb981d01898ce478e41.zip
improve validation and escaping of e-mail addresses: use same method for validation in check_email and address_error; encode and validate IDNs; make (more) robust functions to split address lists and emails
Diffstat (limited to 'it_mail.class')
-rw-r--r--it_mail.class150
1 files changed, 119 insertions, 31 deletions
diff --git a/it_mail.class b/it_mail.class
index 4119994..94801a5 100644
--- a/it_mail.class
+++ b/it_mail.class
@@ -301,6 +301,70 @@ function send($p = array())
/**
+ * INTERNAL: Split address lists in headers like From, To, Cc, Bcc into
+ * name/email pairs
+ * @param $string String containing address list
+ * @return Generator returning name/email tuples
+ */
+static function addrlist_split($string)
+{
+ $quoted = false;
+ foreach (str_split($string) as $char)
+ {
+ if ($char == '"')
+ $quoted = !$quoted;
+
+ if ($char == ',' && !$quoted)
+ $n++;
+ else
+ $mailboxes[$n] .= $char;
+ }
+
+ foreach ((array)$mailboxes as $mailbox)
+ {
+ $mode = 'name';
+ $name = $email = '';
+ $quoted = false;
+ foreach (str_split($mailbox) as $char)
+ {
+ if ($char == '"')
+ $quoted = !$quoted;
+ switch ($mode)
+ {
+ case 'name':
+ if ($char == '<' && !$quoted)
+ $mode = 'email';
+ else
+ $name .= $char;
+ break;
+ case 'email':
+ if ($char == '>' && !$quoted)
+ {
+ $mode = 'finish';
+ break 2;
+ }
+ else
+ $email .= $char;
+ break;
+ }
+ }
+ if ($mode == 'email')
+ {
+ $name = $name . '<' . $email;
+ $email = '';
+ $mode = 'name';
+ }
+ if ($mode == 'name' && !$email)
+ {
+ $email = it::replace(['^"(.*)"$' => '$1'], trim($name));
+ $name = '';
+ }
+ yield [$name, trim($email)];
+ }
+}
+
+
+/**
* 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
@@ -328,23 +392,13 @@ function addrlist_escape($string)
{
# Exclude e-mail addresses from being encoded as
# e.g. GMail or Exchange have problems with that
- foreach (str_split($string) as $char)
+ foreach (self::addrlist_split($string) as list($name, $email))
{
- if ($char == '"')
- $quoted = !$quoted;
-
- if ($char == ',' && !$quoted)
- $n++;
- else
- $mailboxes[$n] .= $char;
- }
-
- foreach ((array)$mailboxes as $mailbox)
- {
- if (preg_match('/^(.*)(\s+?<[^>]+@[^>]+>\s*)$/', $mailbox, $matches))
- $result[] = $this->header_escape(trim($matches[1])) . $matches[2];
- else
- $result[] = trim($mailbox);
+ $email = self::email_escape($email);
+ if ($name && $email)
+ $result[] = $this->header_escape(trim($name)) . " <$email>";
+ else if (!$name && $email)
+ $result[] = $email;
}
return implode(', ', $result);
@@ -364,6 +418,48 @@ static function fullname_escape($string)
/**
+ * INTERNAL: Split email-address into local part and doomain
+ * @return Array with to elements: local part and domain
+ */
+static function email_split($email)
+{
+ $mode = 'local';
+ $local = $domain = '';
+ $quoted = false;
+ foreach (str_split($email) as $char)
+ {
+ if ($char == '"')
+ $quoted = !$quoted;
+ switch ($mode)
+ {
+ case 'local':
+ if ($char == '@' && !$quoted)
+ $mode = 'domain';
+ else
+ $local .= $char;
+ break;
+ case 'domain':
+ $domain .= $char;
+ }
+ }
+
+ return [$local, $domain];
+}
+
+
+/**
+ * INTERNAL: Convert domain part of email address into ascii idn form
+ * @return String with converted email address
+ */
+static function email_escape($email)
+{
+ list($local, $domain) = self::email_split($email);
+
+ return $local . ($domain ? '@' . idn_to_ascii($domain) : '');
+}
+
+
+/**
* 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
@@ -408,20 +504,12 @@ static function send_smtp_cmd($fp, $cmd, &$answer, $timeoutok = false, $failcode
*/
static function address_error($addresslist)
{
- foreach (str_split($addresslist) as $char)
+ foreach (self::addrlist_split($addresslist) as list($dummy, $email))
{
- if ($char == '"')
- $quoted = !$quoted;
- else if ($char == ',' && !$quoted)
- $n++;
- else if (!$quoted)
- $addresses[$n] .= $char;
+ $email = self::email_escape($email);
+ if (filter_var($email, FILTER_VALIDATE_EMAIL) === false && !it::match('^\s*[a-z][-a-z0-9]*\s*$', $email))
+ return "invalid format on $email";
}
-
- foreach ($addresses as $address)
- if (($email = it::match('<([^>]*)>', $address) ?? trim($address)))
- if (filter_var($email, FILTER_VALIDATE_EMAIL) === false && !it::match('^\s*[a-z][-a-z0-9]*\s*$', $email))
- return "invalid format on $email";
}
@@ -438,11 +526,11 @@ static function check_email($email, $checkmailbox = false)
{
$result = IT_MAIL_CHECKEMAIL_INVALID;
+ $email = self::email_escape($email);
+ list($dummy, $domain) = self::email_split($email);
/* 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))
+ if (!preg_match('/^www\./', $email) && filter_var($email, FILTER_VALIDATE_EMAIL) !== false && $domain)
{
- $domain = $regs[1];
-
if (!getmxrr($domain, $mxhosts, $weights))
{
/* If we find no MX we check if domain exists */