1 <?php
2
3 /*
4 * This file is part of the ICanBoogie package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace ICanBoogie\Mailer;
13
14 /**
15 * An address list.
16 *
17 * <pre>
18 * <?php
19 *
20 * use ICanBoogie\Mailer\AddressList;
21 *
22 * $list = new AddressList;
23 * $list[] = "olivier.laviale@gmail.com";
24 * # or
25 * $list["olivier.laviale@gmail.com"] = true;
26 * # or, with a name
27 * $list["olivier.laviale@gmail.com"] = "Olivier Laviale";
28 *
29 * # Creating a collection from an array
30 *
31 * $list = new AddressList([
32 *
33 * 'person1@example.com',
34 * 'person2@example.org',
35 * 'person3@example.net' => 'Person 3 Name',
36 * 'person4@example.org',
37 * 'person5@example.net' => 'Person 5 Name'
38 *
39 * ]);
40 *
41 * isset($list['person1@example.com']); // true
42 * # remove mailbox
43 * unset($list['person1@example.com']);
44 * isset($list['person1@example.com']); // false
45 * </pre>
46 *
47 * @see http://tools.ietf.org/html/rfc5322#section-3.4
48 */
49 class AddressList implements \ArrayAccess, \IteratorAggregate
50 {
51 /**
52 * Creates a {@link AddressList} instance from the provided source.
53 *
54 * @param string|array|AddressList $address_list The address list can be specified as a string,
55 * an array or an {@link AddressList} instance.
56 *
57 * While specified as a string, the address list must be separated by a comma. The display-name of
58 * the address can be defined using the following notation : "display-name <email>", where
59 * `display-name` is the name of the recipient and `email` is its email address.
60 *
61 * @return \ICanBoogie\Mailer\AddressList
62 *
63 * @see http://tools.ietf.org/html/rfc5322#section-3.4
64 */
65 static public function from($address_list)
66 {
67 if (!$address_list)
68 {
69 return new static();
70 }
71
72 if ($address_list instanceof self)
73 {
74 return new static($address_list->address_list);
75 }
76
77 if (!is_array($address_list))
78 {
79 $address_list = static::parse($address_list);
80 }
81
82 return new static($address_list);
83 }
84
85 /**
86 * Parses an address-list string and returns an array of mailbox/display-name pairs.
87 *
88 * @param string $address_list
89 *
90 * @return array[string]string An array where each key/value pair represents a mailbox
91 * and a display-name, or an integer and a mailbox.
92 */
93 static public function parse($address_list)
94 {
95 $parts = explode(',', $address_list);
96 $parts = array_map('trim', $parts);
97
98 $list = array();
99
100 foreach ($parts as $part)
101 {
102 if ($part[strlen($part) - 1] === '>')
103 {
104 list($display_name, $angle_addr) = explode('<', $part, 2);
105
106 $mailbox = substr($angle_addr, 0, -1);
107
108 $list[$mailbox] = trim($display_name);
109
110 continue;
111 }
112
113 $list[] = $part;
114 }
115
116 return $list;
117 }
118
119 /**
120 * Escapes the specified display name according to the specification.
121 *
122 * <pre>
123 * <?php
124 *
125 * use ICanBoogie\Mailer\AddressList;
126 *
127 * echo AddressList('Joe Q. Public'); // "Joe Q. Public"
128 * echo AddressList('Mary Smith'); // Mary Smith
129 * echo AddressList('Who?'); // Who?
130 * echo AddressList('Giant; "Big" Box'); // "Giant; \"Big\" Box"
131 * echo AddressList('ACME, Inc.'); // "ACME, Inc."
132 * </pre>
133 *
134 * @param string $display_name The display name to escape.
135 *
136 * @return string
137 *
138 * @see http://tools.ietf.org/html/rfc5322#section-3.2.3
139 * @see http://tools.ietf.org/html/rfc5322#appendix-A.1.2
140 */
141 static public function escape_display_name($display_name)
142 {
143 if (preg_match('#\(|\)|\<|\>|\[|\]|\:|\;|\@|\\|\,|\.|\"#', $display_name))
144 {
145 $display_name = '"' . addslashes($display_name) . '"';
146 }
147
148 return $display_name;
149 }
150
151 /**
152 * A collection of recipient.
153 *
154 * Each key/value pair represents a mailbox and a display-name, which might be `true` if
155 * the display-name of mailbox was not provided.
156 *
157 * @var array
158 */
159 protected $address_list = array();
160
161 /**
162 * Initializes the {@link $address_list} property.
163 *
164 * Note: The method uses the `ArrayAccess` interface to set the mailboxes.
165 *
166 * @param array $address_list An address list, such as one provided by
167 * the {@link parse()} method.
168 */
169 public function __construct(array $address_list=array())
170 {
171 foreach ($address_list as $mailbox => $display_name)
172 {
173 if (is_numeric($mailbox))
174 {
175 $this[] = $display_name;
176
177 continue;
178 }
179
180 $this[$mailbox] = $display_name;
181 }
182 }
183
184 /**
185 * Checks if the recipient exists in the collection.
186 *
187 * @return `true` if the recipient exists, `false` otherwise.
188 */
189 public function offsetExists($mailbox)
190 {
191 return isset($this->address_list[$mailbox]);
192 }
193
194 /**
195 * Add or set a recipient.
196 */
197 public function offsetSet($mailbox, $display_name)
198 {
199 if ($mailbox === null)
200 {
201 $mailbox = $display_name;
202 $display_name = true;
203 }
204
205 if (!filter_var($mailbox, FILTER_VALIDATE_EMAIL))
206 {
207 throw new \InvalidArgumentException("Invalid email address: $mailbox.");
208 }
209
210 $this->address_list[$mailbox] = $display_name;
211 }
212
213 /**
214 * Returns the recipient name.
215 *
216 * @param string $mailbox The email of the recipient.
217 */
218 public function offsetGet($mailbox)
219 {
220 return $this->address_list[$mailbox];
221 }
222
223 /**
224 * Removes a recipient.
225 *
226 * @param string $mailbox The email of the recipient.
227 */
228 public function offsetUnset($mailbox)
229 {
230 unset($this->address_list[$mailbox]);
231 }
232
233 public function getIterator()
234 {
235 return new \ArrayIterator($this->address_list);
236 }
237
238 /**
239 * Returns a string representation of the instance.
240 *
241 * Note: The returned string is not suitable for a header field,
242 * use a {@link AddressListHeader} instance for that.
243 *
244 * @return string An address-list string that can be parsed by
245 * the {@link parse()} method.
246 */
247 public function __toString()
248 {
249 $rc = '';
250
251 foreach ($this->address_list as $mailbox => $display_name)
252 {
253 if ($rc)
254 {
255 $rc .= ', ';
256 }
257
258 if ($display_name === true)
259 {
260 $rc .= $mailbox;
261
262 continue;
263 }
264
265 $rc .= static::escape_display_name($display_name) . " <$mailbox>";
266 }
267
268 return $rc;
269 }
270 }