1: <?php
2: /**
3: * The Horde_Mime_Address:: class provides methods for dealing with email
4: * address standards (RFC 822/2822/3490/5322).
5: *
6: * Copyright 2008-2012 Horde LLC (http://www.horde.org/)
7: *
8: * See the enclosed file COPYING for license information (LGPL). If you
9: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
10: *
11: * @author Chuck Hagenbuch <chuck@horde.org>
12: * @author Michael Slusarz <slusarz@horde.org>
13: * @category Horde
14: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
15: * @package Mime
16: */
17: class Horde_Mime_Address
18: {
19: /**
20: * Builds an RFC compliant email address.
21: *
22: * @param string $mailbox Mailbox name.
23: * @param string $host Domain name of mailbox's host.
24: * @param string $personal Personal name phrase.
25: * @param array $opts Additional options:
26: * - idn: (boolean) If true, decode IDN domain names (Punycode/RFC 3490).
27: * If false, convert domain names into IDN if necessary (@since
28: * 1.5.0).
29: * If null, does no conversion.
30: * Requires the idn or intl PHP module.
31: * DEFAULT: true
32: *
33: * @return string The correctly escaped and quoted
34: * "$personal <$mailbox@$host>" string.
35: */
36: static public function writeAddress($mailbox, $host, $personal = '',
37: $opts = array())
38: {
39: $host = ltrim($host, '@');
40: if (isset($opts['idn'])) {
41: switch ($opts['idn']) {
42: case true:
43: if (function_exists('idn_to_utf8')) {
44: $host = idn_to_utf8($host);
45: }
46: break;
47:
48: case false:
49: if (function_exists('idn_to_ascii')) {
50: $host = idn_to_ascii($host);
51: }
52: break;
53: }
54: }
55:
56: $address = self::encode($mailbox, 'address') . '@' . $host;
57:
58: return (strlen($personal) && ($personal != $address))
59: ? self::encode($personal, 'personal') . ' <' . $address . '>'
60: : $address;
61: }
62:
63: /**
64: * Write an RFC compliant group address, given the group name and a list
65: * of email addresses.
66: *
67: * @param string $groupname The name of the group.
68: * @param array $addresses The component email addresses. These e-mail
69: * addresses must be in RFC format.
70: *
71: * @return string The correctly quoted group string.
72: */
73: static public function writeGroupAddress($groupname, $addresses = array())
74: {
75: return self::encode($groupname, 'address') . ':' . (empty($addresses) ? '' : (' ' . implode(', ', $addresses)) . ';');
76: }
77:
78: /**
79: * If an email address has no personal information, get rid of any angle
80: * brackets (<>) around it.
81: *
82: * @param string $address The address to trim.
83: *
84: * @return string The trimmed address.
85: */
86: static public function trimAddress($address)
87: {
88: $address = trim($address);
89:
90: if (($address[0] == '<') && (substr($address, -1) == '>')) {
91: $address = substr($address, 1, -1);
92: }
93:
94: return $address;
95: }
96:
97: /**
98: * Explodes an RFC string, ignoring a delimiter if preceded by a "\"
99: * character, or if the delimiter is inside single or double quotes.
100: *
101: * @param string $string The RFC compliant string.
102: * @param string $delimiters A string containing valid delimiters.
103: * Defaults to ','.
104: *
105: * @return array The exploded string in an array.
106: */
107: static public function explode($string, $delimiters = ',')
108: {
109: if (!strlen($string)) {
110: return array($string);
111: }
112:
113: $emails = array();
114: $pos = 0;
115: $in_group = $in_quote = false;
116:
117: for ($i = 0, $iMax = strlen($string); $i < $iMax; ++$i) {
118: $char = $string[$i];
119: if ($char == '"') {
120: if (!$i || ($prev !== '\\')) {
121: $in_quote = !$in_quote;
122: }
123: } elseif ($in_group) {
124: if ($char == ';') {
125: $emails[] = substr($string, $pos, $i - $pos + 1);
126: $pos = $i + 1;
127: $in_group = false;
128: }
129: } elseif (!$in_quote) {
130: if ($char == ':') {
131: $in_group = true;
132: } elseif ((strpos($delimiters, $char) !== false) &&
133: (!$i || ($prev !== '\\'))) {
134: $emails[] = $i ? substr($string, $pos, $i - $pos) : '';
135: $pos = $i + 1;
136: }
137: }
138: $prev = $char;
139: }
140:
141: if ($pos != $i) {
142: /* The string ended without a delimiter. */
143: $emails[] = substr($string, $pos, $i - $pos);
144: }
145:
146: return array_map('trim', $emails);
147: }
148:
149: /**
150: * Takes an address object array and formats it as a string.
151: *
152: * Object array format for the address "John Doe <john_doe@example.com>"
153: * is:
154: * - host: The host the mailbox is on ("example.com")
155: * - mailbox: The user's mailbox ("john_doe")
156: * - personal: Personal name ("John Doe")
157: *
158: * @param array $ob The address object to be turned into a string.
159: * @param array $opts Additional options:
160: * - charset: (string) The local charset.
161: * DEFAULT: NONE
162: * - filter: (mixed) A user@example.com style bare address to ignore.
163: * Either single string or an array of strings. If the address
164: * matches $filter, an empty string will be returned.
165: * DEFAULT: No filter
166: * - idn: (boolean) Convert IDN domain names (Punycode/RFC 3490) into
167: * the local charset.
168: * Requires the idn or intl PHP module.
169: * DEFAULT: true
170: *
171: * @return string The formatted address.
172: */
173: static public function addrObject2String($ob, $opts = array())
174: {
175: $opts = array_merge(array(
176: 'charset' => null
177: ), $opts);
178:
179: /* If the personal name is set, decode it. */
180: $ob['personal'] = isset($ob['personal'])
181: ? Horde_Mime::decode($ob['personal'], $opts['charset'])
182: : '';
183:
184: /* If both the mailbox and the host are empty, return an empty string.
185: * If we just let this case fall through, the call to writeAddress()
186: * will end up return just a '@', which is undesirable. */
187: if (empty($ob['mailbox']) && empty($ob['host'])) {
188: return '';
189: }
190:
191: /* Make sure these two variables have some sort of value. */
192: if (!isset($ob['mailbox'])) {
193: $ob['mailbox'] = '';
194: }
195: if (!isset($ob['host'])) {
196: $ob['host'] = '';
197: }
198:
199: /* Filter out unwanted addresses based on the $filter string. */
200: if (!empty($opts['filter'])) {
201: $filter = is_array($opts['filter'])
202: ? $opts['filter']
203: : array($opts['filter']);
204: foreach ($filter as $f) {
205: if (strcasecmp($f, $ob['mailbox'] . '@' . $ob['host']) == 0) {
206: return '';
207: }
208: }
209: }
210:
211: /* Return the formatted email address. */
212: return self::writeAddress($ob['mailbox'], $ob['host'], $ob['personal'], $opts);
213: }
214:
215: /**
216: * Takes an array of address object arrays and passes each of them through
217: * addrObject2String().
218: *
219: * @param array $addresses The array of address objects.
220: * @param array $opts Additional options:
221: * - charset: (string) The local charset.
222: * DEFAULT: NONE
223: * - filter: (mixed) A user@example.com style bare address to ignore.
224: * Either single string or an array of strings.
225: * DEFAULT: No filter
226: * - idn: (boolean) Convert IDN domain names (Punycode/RFC 3490) into
227: * the local charset.
228: * Requires the idn or intl PHP module.
229: * DEFAULT: true
230: *
231: * @return string All of the addresses in a comma-delimited string.
232: * Returns the empty string on error/no addresses found.
233: */
234: static public function addrArray2String($addresses, $opts = array())
235: {
236: if (!is_array($addresses)) {
237: return '';
238: }
239:
240: $addrList = array();
241:
242: foreach ($addresses as $addr) {
243: $val = self::addrObject2String($addr, $opts);
244: if (!empty($val)) {
245: $addrList[Horde_String::lower(self::bareAddress($val))] = $val;
246: }
247: }
248:
249: return implode(', ', $addrList);
250: }
251:
252: /**
253: * Return the list of addresses for a header object.
254: *
255: * @todo Replace with built-in Horde_Mail_Rfc822_Address function.
256: *
257: * @param array $obs An array of header objects.
258: * @param array $opts Additional options:
259: * - charset: (string) The local charset.
260: * DEFAULT: NONE
261: * - filter: (mixed) A user@example.com style bare address to ignore.
262: * Either single string or an array of strings.
263: * DEFAULT: No filter
264: * - idn: (boolean) Convert IDN domain names (Punycode/RFC 3490) into
265: * the local charset.
266: * Requires the idn or intl PHP module.
267: * DEFAULT: true
268: *
269: * @return array An array of address information. Array elements:
270: * - address: (string) Full address
271: * - display: (string) A displayable version of the address
272: * - groupname: (string) The group name.
273: * - host: (string) Hostname
274: * - inner: (string) Trimmed, bare address
275: * - personal: (string) Personal string
276: */
277: static public function getAddressesFromObject($obs, $opts = array())
278: {
279: $opts = array_merge(array(
280: 'charset' => null
281: ), $opts);
282:
283: $ret = array();
284:
285: if (!is_array($obs) || empty($obs)) {
286: return $ret;
287: }
288:
289: foreach ($obs as $ob) {
290: if (isset($ob['groupname'])) {
291: $ret[] = array(
292: 'addresses' => self::getAddressesFromObject($ob['addresses'], $opts),
293: 'groupname' => $ob['groupname']
294: );
295: continue;
296: }
297:
298: if (is_array($ob)) {
299: $ob = array_merge(array(
300: 'host' => '',
301: 'mailbox' => '',
302: 'personal' => ''
303: ), $ob);
304: }
305:
306: /* Ensure we're working with initialized values. */
307: if (!empty($ob['personal'])) {
308: $ob['personal'] = trim(stripslashes(Horde_Mime::decode($ob['personal'], $opts['charset'])), '"');
309: }
310:
311: $inner = self::writeAddress($ob['mailbox'], $ob['host']);
312:
313: $addr_string = self::addrObject2String($ob, $opts);
314:
315: if (!empty($addr_string)) {
316: /* Generate the new object. */
317: $ret[] = array(
318: 'address' => $addr_string,
319: 'display' => (empty($ob['personal']) ? '' : $ob['personal'] . ' <') . $inner . (empty($ob['personal']) ? '' : '>'),
320: 'host' => $ob['host'],
321: 'inner' => $inner,
322: 'personal' => $ob['personal']
323: );
324: }
325: }
326:
327: return $ret;
328: }
329:
330: /**
331: * Returns the bare address.
332: *
333: * @param string $address The address string.
334: * @param string $defserver The default domain to append to mailboxes.
335: * @param boolean $multiple Should we return multiple results?
336: *
337: * @return mixed If $multiple is false, returns the mailbox@host e-mail
338: * address. If $multiple is true, returns an array of
339: * these addresses.
340: */
341: static public function bareAddress($address, $defserver = null,
342: $multiple = false)
343: {
344: $addressList = array();
345:
346: try {
347: $from = self::parseAddressList($address, array(
348: 'defserver' => $defserver
349: ));
350: } catch (Horde_Mime_Exception $e) {
351: return $multiple ? array() : '';
352: }
353:
354: foreach ($from as $entry) {
355: if (!empty($entry['mailbox'])) {
356: $addressList[] = $entry['mailbox'] . (isset($entry['host']) ? '@' . $entry['host'] : '');
357: }
358: }
359:
360: return $multiple ? $addressList : array_pop($addressList);
361: }
362:
363: /**
364: * Parses a list of email addresses into its parts. Handles distribution
365: * lists.
366: *
367: * @param string $address The address string.
368: * @param array $opts Additional options:
369: * - defserver: (string) The default domain to append to mailboxes.
370: * DEFAULT: No domain appended.
371: * - nestgroups: (boolean) Nest the groups? (Will appear under the
372: * 'groupname' key)
373: * DEFAULT: No.
374: * - validate: (boolean) Validate the address(es)?
375: * DEFAULT: No.
376: *
377: * @return array A list of arrays with the possible keys: 'mailbox',
378: * 'host', 'personal', 'adl', 'groupname', and 'comment'.
379: * @throws Horde_Mime_Exception
380: */
381: static public function parseAddressList($address, array $opts = array())
382: {
383: $opts = array_merge(array(
384: 'defserver' => null,
385: 'nestgroups' => false,
386: 'validate' => false
387: ), $opts);
388:
389: $rfc822 = new Horde_Mail_Rfc822();
390:
391: try {
392: $ret = $rfc822->parseAddressList($address, array(
393: 'default_domain' => $opts['defserver'],
394: 'nest_groups' => $opts['nestgroups'],
395: 'validate' => $opts['validate']
396: ));
397: } catch (Horde_Mail_Exception $e) {
398: throw new Horde_Mime_Exception($e);
399: }
400:
401: /* Convert objects to arrays. */
402: foreach (array_keys($ret) as $key) {
403: $ret[$key] = (array)$ret[$key];
404: if (isset($ret[$key]['addresses'])) {
405: $ptr = &$ret[$key]['addresses'];
406: foreach (array_keys($ptr) as $key2) {
407: $ptr[$key2] = (array)$ptr[$key2];
408: }
409: }
410: }
411:
412: return $ret;
413: }
414:
415: /**
416: * Quotes and escapes the given string if necessary using rules contained
417: * in RFC 2822 [3.2.5].
418: *
419: * @param string $str The string to be quoted and escaped.
420: * @param string $type Either 'address', or 'personal';
421: *
422: * @return string The correctly quoted and escaped string.
423: */
424: static public function encode($str, $type = 'address')
425: {
426: // Excluded (in ASCII): 0-8, 10-31, 34, 40-41, 44, 58-60, 62, 64,
427: // 91-93, 127
428: $filter = "\0\1\2\3\4\5\6\7\10\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\"(),:;<>@[\\]\177";
429:
430: switch ($type) {
431: case 'personal':
432: // RFC 2822 [3.4]: Period not allowed in display name
433: $filter .= '.';
434: break;
435:
436: case 'address':
437: default:
438: // RFC 2822 [3.4.1]: (HTAB, SPACE) not allowed in address
439: $filter .= "\11\40";
440: break;
441: }
442:
443: // Strip double quotes if they are around the string already.
444: // If quoted, we know that the contents are already escaped, so
445: // unescape now.
446: $str = trim($str);
447: if ($str && ($str[0] == '"') && (substr($str, -1) == '"')) {
448: $str = stripslashes(substr($str, 1, -1));
449: }
450:
451: return (strcspn($str, $filter) != strlen($str))
452: ? '"' . addcslashes($str, '\\"') . '"'
453: : $str;
454: }
455:
456: }
457: