1. <?php
2. /**
3. * File containing the ezcMailImapTransport class.
4. *
5. * @package Mail
6. * @version 1.4
7. * @copyright Copyright (C) 2005-2007 eZ systems as. All rights reserved.
8. * @license http://ez.no/licenses/new_bsd New BSD License
9. */
10.
11. /**
12. * The class ezcMailImapTransport implements functionality for handling IMAP
13. * mail servers.
14. *
15. * The implementation supports most of the commands specified in:
16. * - {@link http://www.faqs.org/rfcs/rfc1730.html} (IMAP4)
17. * - {@link http://www.faqs.org/rfcs/rfc2060.html} (IMAP4rev1)
18. *
19. * Each user account on the IMAP server has it's own folders (mailboxes).
20. * Mailboxes can be created, renamed or deleted. All accounts have a special
21. * mailbox called Inbox which cannot be deleted or renamed.
22. *
23. * Messages are organized in mailboxes, and are identified by a message number
24. * (which can change over time) and a unique ID (which does not change under
25. * normal circumstances). The commands operating on messages can handle both
26. * modes (message numbers or unique IDs).
27. *
28. * Messages are marked by certain flags (SEEN, DRAFT, etc). Deleting a message
29. * actually sets it's DELETED flag, and a later call to {@link expunge()} will
30. * delete all the messages marked with the DELETED flag.
31. *
32. * The IMAP server can be in different states. Most IMAP commands require
33. * that a connection is established and a user is authenticated. Certain
34. * commands require in addition that a mailbox is selected.
35. *
36. * The IMAP transport class allows developers to interface with an IMAP server.
37. * The commands which support unique IDs to refer to messages are marked with
38. * [*] (see {@link ezcMailImapTransportOptions} to find out how to enable
39. * unique IDs referencing):
40. *
41. * Basic commands:
42. * - connect to an IMAP server ({@link __construct()})
43. * - authenticate a user with a username and password ({@link authenticate()})
44. * - select a mailbox ({@link selectMailbox()})
45. * - disconnect from the IMAP server ({@link disconnect()})
46. *
47. * Work with mailboxes:
48. * - get the list of mailboxes of the user ({@link listMailboxes()})
49. * - create a mailbox ({@link createMailbox()}
50. * - rename a mailbox ({@link renameMailbox()}
51. * - delete a mailbox ({@link deleteMailbox()}
52. * - append a message to a mailbox ({@link append()}
53. * - select a mailbox ({@link selectMailbox()})
54. * - get the status of messages in the current mailbox ({@link status()})
55. * - get the number of messages with a certain flag ({@link countByFlag()}
56. *
57. * Work with message numbers (on the currently selected mailbox):
58. * - get the message numbers and sizes of all the messages ({@link listMessages()})
59. * - get the message numbers and IDs of all the messages ({@link listUniqueIdentifiers()})
60. * - [*] get the headers of a certain message ({@link top()})
61. * - [*] delete a message ({@link delete()} and {@link expunge()})
62. * - [*] copy messages to another mailbox ({@link copyMessages()})
63. * - [*] get the sizes of the specified messages ({@link fetchSizes()})
64. *
65. * Work with flags (on the currently selected mailbox):
66. * - [*] get the flags of the specified messages ({@link fetchFlags()})
67. * - [*] set a flag on the specified messages ({@link setFlag()})
68. * - [*] clear a flag from the specified messages ({@link clearFlag()})
69. *
70. * Work with {@link ezcMailImapSet} sets (parseable with {@link ezcMailParser})
71. * (on the currently selected mailbox):
72. * - [*] create a set from all messages ({@link fetchAll()})
73. * - [*] create a set from a certain message ({@link fetchByMessageNr()})
74. * - [*] create a set from a range of messages ({@link fetchFromOffset()})
75. * - [*] create a set from messages with a certain flag ({@link fetchByFlag()})
76. * - [*] create a set from a sorted range of messages ({@link sortFromOffset()})
77. * - [*] create a set from a sorted list of messages ({@link sortMessages()})
78. * - [*] create a set from a free-form search ({@link searchMailbox()})
79. *
80. * Miscellaneous commands:
81. * - get the capabilities of the IMAP server ({@link capability()})
82. * - get the hierarchy delimiter (useful for nested mailboxes) ({@link getHierarchyDelimiter()})
83. * - issue a NOOP command to keep the connection alive ({@link noop()})
84. *
85. * The usual operation with an IMAP server is illustrated by this example:
86. * <code>
87. * // create a new IMAP transport object by specifying the server name, optional port
88. * // and optional SSL mode
89. * $options = new ezcMailImapTransportOptions();
90. * $options->ssl = true;
91. * $imap = new ezcMailImapTransport( 'imap.example.com', null, $options );
92. *
93. * // Authenticate to the IMAP server
94. * $imap->authenticate( 'username', 'password' );
95. *
96. * // Select a mailbox (here 'Inbox')
97. * $imap->selectMailbox( 'Inbox' );
98. *
99. * // issue commands to the IMAP server
100. * // for example get the number of RECENT messages
101. * $recent = $imap->countByFlag( 'RECENT' );
102. *
103. * // see the above list of commands or consult the online documentation for
104. * // the full list of commands you can issue to an IMAP server and examples
105. *
106. * // disconnect from the IMAP server
107. * $imap->disconnect();
108. * </code>
109. *
110. * See {@link ezcMailImapTransportOptions} for other options you can specify
111. * for IMAP.
112. *
113. * @todo ignore messages of a certain size?
114. * @todo // support for signing?
115. * @todo listUniqueIdentifiers(): add UIVALIDITY value to UID (like in POP3).
116. * (if necessary).
117. *
118. * @property ezcMailImapTransportOptions $options
119. * Holds the options you can set to the IMAP transport.
120. *
121. * @package Mail
122. * @version 1.4
123. * @mainclass
124. */
125. class ezcMailImapTransport
126. {
127. /**
128. * Internal state when the IMAP transport is not connected to a server.
129. *
130. * @access private
131. */
132. const STATE_NOT_CONNECTED = 1;
133.
134. /**
135. * Internal state when the IMAP transport is connected to a server,
136. * but no successful authentication has been performed.
137. *
138. * @access private
139. */
140. const STATE_NOT_AUTHENTICATED = 2;
141.
142. /**
143. * Internal state when the IMAP transport is connected to a server
144. * and authenticated, but no mailbox is selected yet.
145. *
146. * @access private
147. */
148. const STATE_AUTHENTICATED = 3;
149.
150. /**
151. * Internal state when the IMAP transport is connected to a server,
152. * authenticated, and a mailbox is selected.
153. *
154. * @access private
155. */
156. const STATE_SELECTED = 4;
157.
158. /**
159. * Internal state when the IMAP transport is connected to a server,
160. * authenticated, and a mailbox is selected read only.
161. *
162. * @access private
163. */
164. const STATE_SELECTED_READONLY = 5;
165.
166. /**
167. * Internal state when the LOGOUT command has been issued to the IMAP
168. * server, but before the disconnect has taken place.
169. *
170. * @access private
171. */
172. const STATE_LOGOUT = 6;
173.
174. /**
175. * The response sent from the IMAP server is "OK".
176. *
177. * @access private
178. */
179. const RESPONSE_OK = 1;
180.
181. /**
182. * The response sent from the IMAP server is "NO".
183. *
184. * @access private
185. */
186. const RESPONSE_NO = 2;
187.
188. /**
189. * The response sent from the IMAP server is "BAD".
190. *
191. * @access private
192. */
193. const RESPONSE_BAD = 3;
194.
195. /**
196. * The response sent from the IMAP server is untagged (starts with "*").
197. *
198. * @access private
199. */
200. const RESPONSE_UNTAGGED = 4;
201.
202. /**
203. * The response sent from the IMAP server requires the client to send
204. * information (starts with "+").
205. *
206. * @access private
207. */
208. const RESPONSE_FEEDBACK = 5;
209.
210. /**
211. * Use UID commands (access messages by their unique ID).
212. *
213. * @access private
214. */
215. const UID = 'UID ';
216.
217. /**
218. * Use message number commands (access messages by their message numbers).
219. *
220. * @access private
221. */
222. const NO_UID = '';
223.
224. /**
225. * Basic flags are used by {@link setFlag()} and {@link clearFlag()}
226. *
227. * Basic flags:
228. * - ANSWERED - message has been answered
229. * - DELETED - message is marked to be deleted by later EXPUNGE
230. * - DRAFT - message is marked as a draft
231. * - FLAGGED - message is "flagged" for urgent/special attention
232. * - SEEN - message has been read
233. *
234. * @var array(string)
235. */
236. protected static $basicFlags = array( 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'SEEN' );
237.
238. /**
239. * Extended flags are used by {@link searchByFlag()}
240. *
241. * Basic flags:
242. * - ANSWERED - message has been answered
243. * - DELETED - message is marked to be deleted by later EXPUNGE
244. * - DRAFT - message is marked as a draft
245. * - FLAGGED - message is "flagged" for urgent/special attention
246. * - RECENT - message is recent
247. * - SEEN - message has been read
248. *
249. * Opposites of the above flags:
250. * - UNANSWERED
251. * - UNDELETED
252. * - UNDRAFT
253. * - UNFLAGGED
254. * - OLD
255. * - UNSEEN
256. *
257. * Composite flags:
258. * - NEW - equivalent to RECENT + UNSEEN
259. * - ALL - all the messages
260. *
261. * @var array(string)
262. */
263. protected static $extendedFlags = array( 'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT', 'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNRECENT', 'UNSEEN' );
264.
265. /**
266. * Used to generate a tag for sending commands to the IMAP server.
267. *
268. * @var string
269. */
270. protected $currentTag = 'A0000';
271.
272. /**
273. * Holds the connection state.
274. *
275. * @var int {@link STATE_NOT_CONNECTED},
276. * {@link STATE_NOT_AUTHENTICATED},
277. * {@link STATE_AUTHENTICATED},
278. * {@link STATE_SELECTED},
279. * {@link STATE_SELECTED_READONLY} or
280. * {@link STATE_LOGOUT}.
281. */
282. protected $state = self::STATE_NOT_CONNECTED;
283.
284. /**
285. * Holds the currently selected mailbox.
286. *
287. * @var string
288. */
289. protected $selectedMailbox = null;
290.
291. /**
292. * Holds the connection to the IMAP server.
293. *
294. * @var ezcMailTransportConnection
295. */
296. protected $connection = null;
297.
298. /**
299. * Holds the options for an IMAP transport connection.
300. *
301. * @var ezcMailImapTransportOptions
302. */
303. private $options;
304.
305. /**
306. * Creates a new IMAP transport and connects to the $server at $port.
307. *
308. * You can specify the $port if the IMAP server is not on the default port
309. * 993 (for SSL connections) or 143 (for plain connections). Use the $options
310. * parameter to specify an SSL connection.
311. *
312. * See {@link ezcMailImapTransportOptions} for options you can specify for
313. * IMAP.
314. *
315. * Example of creating an IMAP transport:
316. * <code>
317. * // replace with your IMAP server address
318. * $imap = new ezcMailImapTransport( 'imap.example.com' );
319. *
320. * // if you want to use SSL:
321. * $options = new ezcMailImapTransportOptions();
322. * $options->ssl = true;
323. *
324. * $imap = new ezcMailImapTransport( 'imap.example.com', null, $options );
325. * </code>
326. *
327. * @throws ezcMailTransportException
328. * if it was not possible to connect to the server
329. * @throws ezcBaseExtensionNotFoundException
330. * if trying to use SSL and the extension openssl is not installed
331. * @throws ezcBasePropertyNotFoundException
332. * if $options contains a property not defined
333. * @throws ezcBaseValueException
334. * if $options contains a property with a value not allowed
335. * @param string $server
336. * @param int $port
337. * @param ezcMailImapTransportOptions|array(string=>mixed)$options
338. */
339. public function __construct( $server, $port = null, $options = array() )
340. {
341. if ( $options instanceof ezcMailImapTransportOptions )
342. {
343. $this->options = $options;
344. }
345. else if ( is_array( $options ) )
346. {
347. $this->options = new ezcMailImapTransportOptions( $options );
348. }
349. else
350. {
351. throw new ezcBaseValueException( "options", $options, "ezcMailImapTransportOptions|array" );
352. }
353.
354. if ( $port === null )
355. {
356. $port = ( $this->options->ssl === true ) ? 993 : 143;
357. }
358. $this->connection = new ezcMailTransportConnection( $server, $port, $this->options );
359. // get the server greeting
360. $response = $this->connection->getLine();
361. if ( strpos( $response, "* OK" ) === false )
362. {
363. throw new ezcMailTransportException( "The connection to the IMAP server is ok, but a negative response from server was received. Try again later." );
364. }
365. $this->state = self::STATE_NOT_AUTHENTICATED;
366. }
367.
368. /**
369. * Destructs the IMAP transport.
370. *
371. * If there is an open connection to the IMAP server it is closed.
372. */
373. public function __destruct()
374. {
375. $this->disconnect();
376. }
377.
378. /**
379. * Sets the value of the property $name to $value.
380. *
381. * @throws ezcBasePropertyNotFoundException
382. * if the property $name does not exist
383. * @throws ezcBaseValueException
384. * if $value is not accepted for the property $name
385. * @param string $name
386. * @param mixed $value
387. * @ignore
388. */
389. public function __set( $name, $value )
390. {
391. switch ( $name )
392. {
393. case 'options':
394. if ( !( $value instanceof ezcMailImapTransportOptions ) )
395. {
396. throw new ezcBaseValueException( 'options', $value, 'instanceof ezcMailImapTransportOptions' );
397. }
398. $this->options = $value;
399. break;
400.
401. default:
402. throw new ezcBasePropertyNotFoundException( $name );
403. }
404. }
405.
406. /**
407. * Returns the value of the property $name.
408. *
409. * @throws ezcBasePropertyNotFoundException
410. * if the property $name does not exist
411. * @param string $name
412. * @ignore
413. */
414. public function __get( $name )
415. {
416. switch ( $name )
417. {
418. case 'options':
419. return $this->options;
420.
421. default:
422. throw new ezcBasePropertyNotFoundException( $name );
423. }
424. }
425.
426. /**
427. * Returns true if the property $name is set, otherwise false.
428. *
429. * @param string $name
430. * @return bool
431. * @ignore
432. */
433. public function __isset( $name )
434. {
435. switch ( $name )
436. {
437. case 'options':
438. return true;
439.
440. default:
441. return false;
442. }
443. }
444.
445. /**
446. * Disconnects the transport from the IMAP server.
447. */
448. public function disconnect()
449. {
450. if ( $this->state !== self::STATE_NOT_CONNECTED
451. && $this->connection->isConnected() === true )
452. {
453. $tag = $this->getNextTag();
454. $this->connection->sendData( "{$tag} LOGOUT" );
455. // discard the "bye bye" message ("{$tag} OK Logout completed.")
456. $this->getResponse( $tag );
457. $this->state = self::STATE_LOGOUT;
458. $this->selectedMailbox = null;
459.
460. $this->connection->close();
461. $this->connection = null;
462. $this->state = self::STATE_NOT_CONNECTED;
463. }
464. }
465.
466. /**
467. * Authenticates the user to the IMAP server with $user and $password.
468. *
469. * This method should be called directly after the construction of this
470. * object.
471. *
472. * If the server is waiting for the authentication process to respond, the
473. * connection with the IMAP server will be closed, and false is returned,
474. * and it is the application's task to reconnect and reauthenticate.
475. *
476. * Example of creating an IMAP transport and authenticating:
477. * <code>
478. * // replace with your IMAP server address
479. * $imap = new ezcMailImapTransport( 'imap.example.com' );
480. *
481. * // replace the values with your username and password for the IMAP server
482. * $imap->authenticate( 'username', 'password' );
483. * </code>
484. *
485. * @throws ezcMailTransportException
486. * if already authenticated
487. * or if the provided username/password combination did not work
488. * @param string $user
489. * @param string $password
490. * @return bool
491. */
492. public function authenticate( $user, $password )
493. {
494. if ( $this->state != self::STATE_NOT_AUTHENTICATED )
495. {
496. throw new ezcMailTransportException( "Tried to authenticate when there was no connection or when already authenticated." );
497. }
498.
499. $tag = $this->getNextTag();
500. $this->connection->sendData( "{$tag} LOGIN {$user} {$password}" );
501. $response = trim( $this->connection->getLine() );
502. if ( strpos( $response, '* OK' ) !== false )
503. {
504. // the server is busy waiting for authentication process to
505. // respond, so it is a good idea to just close the connection,
506. // otherwise the application will be halted until the server
507. // recovers
508. $this->connection->close();
509. $this->connection = null;
510. $this->state = self::STATE_NOT_CONNECTED;
511. return false;
512. }
513. if ( $this->responseType( $response ) != self::RESPONSE_OK )
514. {
515. throw new ezcMailTransportException( "The IMAP server did not accept the username and/or password: {$response}." );
516. }
517. else
518. {
519. $this->state = self::STATE_AUTHENTICATED;
520. $this->selectedMailbox = null;
521. }
522. return true;
523. }
524.
525. /**
526. * Returns an array with the names of the available mailboxes for the user
527. * currently authenticated on the IMAP server.
528. *
529. * Before calling this method, a connection to the IMAP server must be
530. * established and a user must be authenticated successfully.
531. *
532. * For more information about $reference and $mailbox, consult
533. * the IMAP RFCs documents ({@link http://www.faqs.org/rfcs/rfc1730.html}
534. * or {@link http://www.faqs.org/rfcs/rfc2060.html}, section 7.2.2.).
535. *
536. * By default, $reference is "" and $mailbox is "*".
537. *
538. * The array returned contains the mailboxes available for the connected
539. * user on this IMAP server. Inbox is a special mailbox, and it can be
540. * specified upper-case or lower-case or mixed-case. The other mailboxes
541. * should be specified as they are (to the {@link selectMailbox()} method).
542. *
543. * Example of listing mailboxes:
544. * <code>
545. * $imap = new ezcMailImapTransport( 'imap.example.com' );
546. * $imap->authenticate( 'username', 'password' );
547. *
548. * $mailboxes = $imap->listMailboxes();
549. * </code>
550. *
551. * @throws ezcMailMailTransportException
552. * if the current server state is not accepted
553. * or if the server sent a negative response
554. * @param string $reference
555. * @param string $mailbox
556. * @return array(string)
557. */
558. public function listMailboxes( $reference = '', $mailbox = '*' )
559. {
560. if ( $this->state != self::STATE_AUTHENTICATED &&
561. $this->state != self::STATE_SELECTED &&
562. $this->state != self::STATE_SELECTED_READONLY )
563. {
564. throw new ezcMailTransportException( "Can't call listMailboxes() when not successfully logged in." );
565. }
566.
567. $result = array();
568. $tag = $this->getNextTag();
569. $this->connection->sendData( "{$tag} LIST \"{$reference}\" \"{$mailbox}\"" );
570. $response = trim( $this->connection->getLine() );
571. while ( strpos( $response, '* LIST (' ) !== false )
572. {
573. // only consider the selectable mailboxes
574. if ( strpos( $response, "\\Noselect" ) === false )
575. {
576. $response = substr( $response, strpos( $response, "\" " ) + 2 );
577. $response = trim( $response );
578. $response = trim( $response, "\"" );
579. $result[] = $response;
580.
581. }
582. $response = $this->connection->getLine();
583. }
584.
585. $response = $this->getResponse( $tag, $response );
586. if ( $this->responseType( $response ) != self::RESPONSE_OK )
587. {
588. throw new ezcMailTransportException( "Could not list mailboxes with the parameters '\"{$reference}\"' and '\"{$mailbox}\"': {$response}." );
589. }
590. return $result;
591. }
592.
593. /**
594. * Returns the hierarchy delimiter of the IMAP server, useful for handling
595. * nested IMAP folders.
596. *
597. * For more information about the hierarchy delimiter, consult the IMAP RFCs
598. * {@link http://www.faqs.org/rfcs/rfc1730.html} or
599. * {@link http://www.faqs.org/rfcs/rfc2060.html}, section 6.3.8.
600. *
601. * Before calling this method, a connection to the IMAP server must be
602. * established and a user must be authenticated successfully.
603. *
604. * Example of returning the hierarchy delimiter:
605. * <code>
606. * $imap = new ezcMailImapTransport( 'imap.example.com' );
607. * $imap->authenticate( 'username', 'password' );
608. *
609. * $delimiter = $imap->getDelimiter();
610. * </code>
611. *
612. * After running the above code, $delimiter should be something like "/".
613. *
614. * @throws ezcMailMailTransportException
615. * if the current server state is not accepted
616. * or if the server sent a negative response
617. * @return string
618. */
619. public function getHierarchyDelimiter()
620. {
621. if ( $this->state != self::STATE_AUTHENTICATED &&
622. $this->state != self::STATE_SELECTED &&
623. $this->state != self::STATE_SELECTED_READONLY )
624. {
625. throw new ezcMailTransportException( "Can't call getDelimiter() when not successfully logged in." );
626. }
627.
628. $tag = $this->getNextTag();
629. $this->connection->sendData( "{$tag} LIST \"\" \"\"" );
630.
631. // there should be only one * LIST response line from IMAP
632. $response = trim( $this->getResponse( '* LIST' ) );
633. $parts = explode( '"', $response );
634.
635. if ( count( $parts ) >= 2 )
636. {
637. $result = $parts[1];
638. }
639. else
640. {
641. throw new ezcMailTransportException( "Could not retrieve the hierarchy delimiter: {$response}." );
642. }
643.
644. $response = $this->getResponse( $tag, $response );
645. if ( $this->responseType( $response ) != self::RESPONSE_OK )
646. {
647. throw new ezcMailTransportException( "Could not retrieve the hierarchy delimiter: {$response}." );
648. }
649. return $result;
650. }
651.
652. /**
653. * Selects the mailbox $mailbox, which will be the active mailbox for the
654. * subsequent commands until it is changed.
655. *
656. * Before calling this method, a connection to the IMAP server must be
657. * established and a user must be authenticated successfully.
658. *
659. * Inbox is a special mailbox and can be specified with any case.
660. *
661. * This method should be called after authentication, and before fetching
662. * any messages.
663. *
664. * Example of selecting a mailbox:
665. * <code>
666. * $imap = new ezcMailImapTransport( 'imap.example.com' );
667. * $imap->authenticate( 'username', 'password' );
668. *
669. * $imap->selectMailbox( 'Reports 2006' );
670. * </code>
671. *
672. * @throws ezcMailMailTransportException
673. * if the current server state is not accepted
674. * or if the server sent a negative response
675. * @param string $mailbox
676. * @param bool $readOnly
677. */
678. public function selectMailbox( $mailbox, $readOnly = false )
679. {
680. if ( $this->state != self::STATE_AUTHENTICATED &&
681. $this->state != self::STATE_SELECTED &&
682. $this->state != self::STATE_SELECTED_READONLY )
683. {
684. throw new ezcMailTransportException( "Can't call selectMailbox() when not successfully logged in." );
685. }
686.
687. $tag = $this->getNextTag();
688.
689. // if the mailbox selection will be successful, $state will be STATE_SELECTED
690. // or STATE_SELECTED_READONLY, depending on the $readOnly parameter
691. if ( $readOnly !== true )
692. {
693. $this->connection->sendData( "{$tag} SELECT \"{$mailbox}\"" );
694. $state = self::STATE_SELECTED;
695. }
696. else
697. {
698. $this->connection->sendData( "{$tag} EXAMINE \"{$mailbox}\"" );
699. $state = self::STATE_SELECTED_READONLY;
700. }
701.
702. // if the selecting of the mailbox fails (with "NO" or "BAD" response
703. // from the server), $state reverts to STATE_AUTHENTICATED
704. $response = trim( $this->getResponse( $tag ) );
705. if ( $this->responseType( $response ) == self::RESPONSE_OK )
706. {
707. $this->state = $state;
708. $this->selectedMailbox = $mailbox;
709. }
710. else
711. {
712. $this->state = self::STATE_AUTHENTICATED;
713. $this->selectedMailbox = null;
714. throw new ezcMailTransportException( "Could not select mailbox '{$mailbox}': {$response}." );
715. }
716. }
717.
718. /**
719. * Creates the mailbox $mailbox.
720. *
721. * Inbox cannot be created.
722. *
723. * Before calling this method, a connection to the IMAP server must be
724. * established and a user must be authenticated successfully.
725. *
726. * @throws ezcMailTransportException
727. * if the current server state is not accepted
728. * or if the server sent a negative response
729. * @param string $mailbox
730. * @return bool
731. */
732. public function createMailbox( $mailbox )
733. {
734. if ( $this->state != self::STATE_AUTHENTICATED &&
735. $this->state != self::STATE_SELECTED &&
736. $this->state != self::STATE_SELECTED_READONLY )
737. {
738. throw new ezcMailTransportException( "Can't call createMailbox() when not successfully logged in." );
739. }
740.
741. $tag = $this->getNextTag();
742. $this->connection->sendData( "{$tag} CREATE \"{$mailbox}\"" );
743. $response = trim( $this->getResponse( $tag ) );
744. if ( $this->responseType( $response ) != self::RESPONSE_OK )
745. {
746. throw new ezcMailTransportException( "The IMAP server could not create mailbox '{$mailbox}': {$response}." );
747. }
748. return true;
749. }
750.
751. /**
752. * Renames the mailbox $mailbox to $newName.
753. *
754. * Inbox cannot be renamed.
755. *
756. * Before calling this method, a connection to the IMAP server must be
757. * established and a user must be authenticated successfully.
758. *
759. * @throws ezcMailTransportException
760. * if the current server state is not accepted
761. * or if trying to rename the currently selected mailbox
762. * or if the server sent a negative response
763. * @param string $mailbox
764. * @param string $newName
765. * @return bool
766. */
767. public function renameMailbox( $mailbox, $newName )
768. {
769. if ( $this->state != self::STATE_AUTHENTICATED &&
770. $this->state != self::STATE_SELECTED &&
771. $this->state != self::STATE_SELECTED_READONLY )
772. {
773. throw new ezcMailTransportException( "Can't call renameMailbox() when not successfully logged in." );
774. }
775.
776. if ( strtolower( $this->selectedMailbox ) == strtolower( $mailbox ) )
777. {
778. throw new ezcMailTransportException( "Can't rename the currently selected mailbox." );
779. }
780.
781. $tag = $this->getNextTag();
782. $this->connection->sendData( "{$tag} RENAME \"{$mailbox}\" \"{$newName}\"" );
783. $response = trim( $this->getResponse( $tag ) );
784. if ( $this->responseType( $response ) != self::RESPONSE_OK )
785. {
786. throw new ezcMailTransportException( "The IMAP server could not rename the mailbox '{$mailbox}' to '{$newName}': {$response}." );
787. }
788. return true;
789. }
790.
791. /**
792. * Deletes the mailbox $mailbox.
793. *
794. * Inbox and the the currently selected mailbox cannot be deleted.
795. *
796. * Before calling this method, a connection to the IMAP server must be
797. * established and a user must be authenticated successfully.
798. *
799. * @throws ezcMailTransportException
800. * if the current server state is not accepted
801. * or if trying to delete the currently selected mailbox
802. * or if the server sent a negative response
803. * @param string $mailbox
804. * @return bool
805. */
806. public function deleteMailbox( $mailbox )
807. {
808. if ( $this->state != self::STATE_AUTHENTICATED &&
809. $this->state != self::STATE_SELECTED &&
810. $this->state != self::STATE_SELECTED_READONLY )
811. {
812. throw new ezcMailTransportException( "Can't call deleteMailbox() when not successfully logged in." );
813. }
814.
815. if ( strtolower( $this->selectedMailbox ) == strtolower( $mailbox ) )
816. {
817. throw new ezcMailTransportException( "Can't delete the currently selected mailbox." );
818. }
819.
820. $tag = $this->getNextTag();
821. $this->connection->sendData( "{$tag} DELETE \"{$mailbox}\"" );
822. $response = trim( $this->getResponse( $tag ) );
823. if ( $this->responseType( $response ) != self::RESPONSE_OK )
824. {
825. throw new ezcMailTransportException( "The IMAP server could not delete the mailbox '{$mailbox}': {$response}." );
826. }
827. return true;
828. }
829.
830. /**
831. * Copies message(s) from the currently selected mailbox to mailbox
832. * $destination.
833. *
834. * This method supports unique IDs instead of message numbers. See
835. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
836. * referencing.
837. *
838. * Warning! When using unique IDs referencing and trying to copy a message
839. * with an ID that does not exist, this method will not throw an exception.
840. *
841. * @todo Find out if it is possible to catch this IMAP bug.
842. *
843. * $messages can be:
844. * - a single message number (eg: '1')
845. * - a message range (eg. '1:4')
846. * - a message list (eg. '1,2,4')
847. *
848. * Before calling this method, a connection to the IMAP server must be
849. * established and a user must be authenticated successfully, and a mailbox
850. * must be selected (the mailbox from which messages will be copied).
851. *
852. * Example of copying 3 messages to a mailbox:
853. * <code>
854. * $imap = new ezcMailImapTransport( 'imap.example.com' );
855. * $imap->authenticate( 'username', 'password' );
856. * $imap->selectMailbox( 'Inbox' );
857. *
858. * $imap->copyMessages( '1,2,4', 'Reports 2006' );
859. * </code>
860. *
861. * The above code will copy the messages with numbers 1, 2 and 4 from Inbox
862. * to Reports 2006.
863. *
864. * @throws ezcMailTransportException
865. * if the current server state is not accepted
866. * or if the server sent a negative response
867. * @param string $messages
868. * @param string $destination
869. * @return bool
870. */
871. public function copyMessages( $messages, $destination )
872. {
873. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
874.
875. if ( $this->state != self::STATE_SELECTED &&
876. $this->state != self::STATE_SELECTED_READONLY )
877. {
878. throw new ezcMailTransportException( "Can't call copyMessages() on the IMAP transport when a mailbox is not selected." );
879. }
880.
881. $tag = $this->getNextTag();
882. $this->connection->sendData( "{$tag} {$uid}COPY {$messages} \"{$destination}\"" );
883.
884. $response = trim( $this->getResponse( $tag ) );
885. if ( $this->responseType( $response ) != self::RESPONSE_OK )
886. {
887. throw new ezcMailTransportException( "The IMAP server could not copy '{$messages}' to '{$destination}': {$response}." );
888. }
889. return true;
890. }
891.
892. /**
893. * Returns a list of the not deleted messages in the current mailbox.
894. *
895. * It returns only the messages with the flag DELETED not set.
896. *
897. * Before calling this method, a connection to the IMAP server must be
898. * established and a user must be authenticated successfully, and a mailbox
899. * must be selected.
900. *
901. * The format of the returned array is
902. * <code>
903. * array( message_id => size );
904. * </code>
905. *
906. * Example:
907. * <code>
908. * array( 2 => 1700, 5 => 1450, 6 => 21043 );
909. * </code>
910. *
911. * If $contentType is set, it returns only the messages with
912. * $contentType in the Content-Type header.
913. *
914. * For example $contentType can be "multipart/mixed" to return only the
915. * messages with attachments.
916. *
917. * @throws ezcMailTransportException
918. * if a mailbox is not selected
919. * or if the server sent a negative response
920. * @param string $contentType
921. * @return array(int)
922. */
923. public function listMessages( $contentType = null )
924. {
925. if ( $this->state != self::STATE_SELECTED &&
926. $this->state != self::STATE_SELECTED_READONLY )
927. {
928. throw new ezcMailTransportException( "Can't call listMessages() on the IMAP transport when a mailbox is not selected." );
929. }
930.
931. $messageList = array();
932. $messages = array();
933.
934. // get the numbers of the existing messages
935. $tag = $this->getNextTag();
936. $command = "{$tag} SEARCH UNDELETED";
937. if ( !is_null( $contentType ) )
938. {
939. $command .= " HEADER \"Content-Type\" \"{$contentType}\"";
940. }
941. $this->connection->sendData( $command );
942. $response = trim( $this->getResponse( '* SEARCH' ) );
943. if ( strpos( $response, '* SEARCH' ) !== false )
944. {
945. $ids = trim( substr( $response, 9 ) );
946. if ( $ids !== "" )
947. {
948. $messageList = explode( ' ', $ids );
949. }
950. }
951. // skip the OK response ("{$tag} OK Search completed.")
952. $response = trim( $this->getResponse( $tag, $response ) );
953. if ( $this->responseType( $response ) != self::RESPONSE_OK )
954. {
955. throw new ezcMailTransportException( "The IMAP server could not list messages: {$response}." );
956. }
957.
958. if ( !empty( $messageList ) )
959. {
960. // get the sizes of the messages
961. $tag = $this->getNextTag();
962. $query = trim( implode( ',', $messageList ) );
963. $this->connection->sendData( "{$tag} FETCH {$query} RFC822.SIZE" );
964. $response = $this->getResponse( 'FETCH (' );
965. $currentMessage = trim( reset( $messageList ) );
966. while ( strpos( $response, 'FETCH (' ) !== false )
967. {
968. $line = $response;
969. $line = explode( ' ', $line );
970. $line = trim( $line[count( $line ) - 1] );
971. $line = substr( $line, 0, strlen( $line ) - 1 );
972. $messages[$currentMessage] = intval( $line );
973. $currentMessage = next( $messageList );
974. $response = $this->connection->getLine();
975. }
976. // skip the OK response ("{$tag} OK Fetch completed.")
977. $response = trim( $this->getResponse( $tag, $response ) );
978. if ( $this->responseType( $response ) != self::RESPONSE_OK )
979. {
980. throw new ezcMailTransportException( "The IMAP server could not list messages: {$response}." );
981. }
982. }
983. return $messages;
984. }
985.
986. /**
987. * Fetches the sizes in bytes for messages $messages.
988. *
989. * This method supports unique IDs instead of message numbers. See
990. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
991. * referencing.
992. *
993. * $messages is an array of message numbers, for example:
994. * <code>
995. * array( 1, 2, 4 );
996. * </code>
997. *
998. * The format of the returned array is:
999. * <code>
1000. * array( message_number => size )
1001. * </code>
1002. *
1003. * Before calling this method, a connection to the IMAP server must be
1004. * established and a user must be authenticated successfully, and a mailbox
1005. * must be selected.
1006. *
1007. * Example:
1008. * <code>
1009. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1010. * $imap->authenticate( 'username', 'password' );
1011. * $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
1012. *
1013. * $sizes = $imap->fetchSizes( array( 1, 2, 4 ) );
1014. * </code>
1015. *
1016. * The returned array $sizes will be something like:
1017. * <code>
1018. * array( 1 => 1043,
1019. * 2 => 203901,
1020. * 4 => 14277
1021. * );
1022. * </code>
1023. *
1024. * @throws ezcMailTransportException
1025. * if a mailbox is not selected
1026. * or if the server sent a negative response
1027. * @param array $messages
1028. * @return array(int)
1029. */
1030. public function fetchSizes( $messages )
1031. {
1032. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
1033.
1034. if ( $this->state != self::STATE_SELECTED &&
1035. $this->state != self::STATE_SELECTED_READONLY )
1036. {
1037. throw new ezcMailTransportException( "Can't call fetchSizes() on the IMAP transport when a mailbox is not selected." );
1038. }
1039.
1040. $sizes = array();
1041. $ids = implode( $messages, ',' );
1042.
1043. $tag = $this->getNextTag();
1044. $this->connection->sendData( "{$tag} {$uid}FETCH {$ids} (RFC822.SIZE)" );
1045.
1046. $response = trim( $this->connection->getLine() );
1047. while ( strpos( $response, $tag ) === false )
1048. {
1049. if ( strpos( $response, ' FETCH (' ) !== false )
1050. {
1051. if ( $this->options->uidReferencing )
1052. {
1053. preg_match( '/\*\s.*\sFETCH\s\(RFC822\.SIZE\s(.*)\sUID\s(.*)\)/U', $response, $matches );
1054. $sizes[intval( $matches[2] )] = (int) $matches[1];
1055. }
1056. else
1057. {
1058. preg_match( '/\*\s(.*)\sFETCH\s\(RFC822\.SIZE\s(.*)\)/U', $response, $matches );
1059. $sizes[intval( $matches[1] )] = (int) $matches[2];
1060. }
1061.
1062. }
1063. $response = trim( $this->connection->getLine() );
1064. }
1065.
1066. if ( $this->responseType( $response ) != self::RESPONSE_OK )
1067. {
1068. throw new ezcMailTransportException( "The IMAP server could not fetch flags for the messages '{$messages}': {$response}." );
1069. }
1070. return $sizes;
1071. }
1072.
1073. /**
1074. * Returns information about the messages in the current mailbox.
1075. *
1076. * The information returned through the parameters is:
1077. * - $numMessages = number of not deleted messages in the selected mailbox
1078. * - $sizeMessages = sum of the not deleted messages sizes
1079. * - $recent = number of recent and not deleted messages
1080. * - $unseen = number of unseen and not deleted messages
1081. *
1082. * Before calling this method, a connection to the IMAP server must be
1083. * established and a user must be authenticated successfully, and a mailbox
1084. * must be selected.
1085. *
1086. * Example of returning the status of the currently selected mailbox:
1087. * <code>
1088. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1089. * $imap->authenticate( 'username', 'password' );
1090. * $imap->selectMailbox( 'Inbox' );
1091. *
1092. * $imap->status( $numMessages, $sizeMessages, $recent, $unseen );
1093. * </code>
1094. *
1095. * After running the above code, $numMessages, $sizeMessages, $recent
1096. * and $unseen will be populated with values.
1097. *
1098. * @throws ezcMailTransportException
1099. * if a mailbox is not selected
1100. * or if the server sent a negative response
1101. * @param int &$numMessages
1102. * @param int &$sizeMessages
1103. * @param int &$recent
1104. * @param int &$unseen
1105. * @return bool
1106. */
1107. public function status( &$numMessages, &$sizeMessages, &$recent = 0, &$unseen = 0 )
1108. {
1109. if ( $this->state != self::STATE_SELECTED &&
1110. $this->state != self::STATE_SELECTED_READONLY )
1111. {
1112. throw new ezcMailTransportException( "Can't call status() on the IMAP transport when a mailbox is not selected." );
1113. }
1114. $messages = $this->listMessages();
1115. $numMessages = count( $messages );
1116. $sizeMessages = array_sum( $messages );
1117. $messages = array_keys( $messages );
1118. $recentMessages = array_intersect( $this->searchByFlag( "RECENT" ), $messages );
1119. $unseenMessages = array_intersect( $this->searchByFlag( "UNSEEN" ), $messages );
1120. $recent = count( $recentMessages );
1121. $unseen = count( $unseenMessages );
1122. return true;
1123. }
1124.
1125. /**
1126. * Deletes the message with the message number $msgNum from the current mailbox.
1127. *
1128. * This method supports unique IDs instead of message numbers. See
1129. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1130. * referencing.
1131. *
1132. * The message number $msgNum must be a valid identifier fetched with e.g.
1133. * {@link listMessages()}.
1134. *
1135. * The message is not physically deleted, but has its DELETED flag set,
1136. * and can be later undeleted by clearing its DELETED flag with
1137. * {@link clearFlag()}.
1138. *
1139. * Before calling this method, a connection to the IMAP server must be
1140. * established and a user must be authenticated successfully, and a mailbox
1141. * must be selected.
1142. *
1143. * @throws ezcMailTransportException
1144. * if a mailbox is not selected
1145. * or if the server sent a negative response
1146. * @param int $msgNum
1147. * @return bool
1148. */
1149. public function delete( $msgNum )
1150. {
1151. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
1152.
1153. if ( $this->state != self::STATE_SELECTED )
1154. {
1155. throw new ezcMailTransportException( "Can't call delete() when a mailbox is not selected." );
1156. }
1157. $tag = $this->getNextTag();
1158. $this->connection->sendData( "{$tag} {$uid}STORE {$msgNum} +FLAGS (\\Deleted)" );
1159.
1160. // get the response (should be "{$tag} OK Store completed.")
1161. $response = trim( $this->getResponse( $tag ) );
1162. if ( $this->responseType( $response ) != self::RESPONSE_OK )
1163. {
1164. throw new ezcMailTransportException( "The IMAP server could not delete the message '{$msgNum}': {$response}." );
1165. }
1166. return true;
1167. }
1168.
1169. /**
1170. * Returns the headers and the first characters from message $msgNum,
1171. * without setting the SEEN flag.
1172. *
1173. * This method supports unique IDs instead of message numbers. See
1174. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1175. * referencing.
1176. *
1177. * If the command failed or if it was not supported by the server an empty
1178. * string is returned.
1179. *
1180. * This method is useful for retrieving the headers of messages from the
1181. * mailbox as strings, which can be later parsed with {@link ezcMailParser}
1182. * and {@link ezcMailVariableSet}. In this way the retrieval of the full
1183. * messages from the server is avoided when building a list of messages.
1184. *
1185. * Before calling this method, a connection to the IMAP server must be
1186. * established and a user must be authenticated successfully, and a mailbox
1187. * must be selected.
1188. *
1189. * Example of listing the mail headers of all the messages in the current
1190. * mailbox:
1191. * <code>
1192. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1193. * $imap->authenticate( 'username', 'password' );
1194. * $imap->selectMailbox( 'Inbox' );
1195. *
1196. * $parser = new ezcMailParser();
1197. * $messages = $imap->listMessages();
1198. * foreach ( $messages as $messageNr => $size )
1199. * {
1200. * $set = new ezcMailVariableSet( $imap->top( $messageNr ) );
1201. * $mail = $parser->parseMail( $set );
1202. * $mail = $mail[0];
1203. * echo "From: {$mail->from}, Subject: {$mail->subject}, Size: {$size}\n";
1204. * }
1205. * </code>
1206. *
1207. * For a more advanced example see the "Mail listing example" in the online
1208. * documentation.
1209. *
1210. * @throws ezcMailTransportException
1211. * if a mailbox is not selected
1212. * or if the server sent a negative response
1213. * @param int $msgNum
1214. * @param int $chars
1215. * @return string
1216. */
1217. public function top( $msgNum, $chars = 0 )
1218. {
1219. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
1220.
1221. if ( $this->state != self::STATE_SELECTED &&
1222. $this->state != self::STATE_SELECTED_READONLY )
1223. {
1224. throw new ezcMailTransportException( "Can't call top() on the IMAP transport when a mailbox is not selected." );
1225. }
1226.
1227. $tag = $this->getNextTag();
1228.
1229. if ( $chars === 0 )
1230. {
1231. $command = "{$tag} {$uid}FETCH {$msgNum} (BODY.PEEK[HEADER] BODY.PEEK[TEXT])";
1232. }
1233. else
1234. {
1235. $command = "{$tag} {$uid}FETCH {$msgNum} (BODY.PEEK[HEADER] BODY.PEEK[TEXT]<0.{$chars}>)";
1236. }
1237. $this->connection->sendData( $command );
1238. if ( $this->options->uidReferencing )
1239. {
1240. // special case (BUG?) where "UID FETCH {$msgNum}" returns nothing
1241. $response = trim( $this->connection->getLine() );
1242. if ( $this->responseType( $response ) === self::RESPONSE_OK )
1243. {
1244. throw new ezcMailTransportException( "The IMAP server could not fetch the message '{$msgNum}': {$response}." );
1245. }
1246. }
1247. else
1248. {
1249. $response = $this->getResponse( 'FETCH (' );
1250. }
1251. $message = "";
1252. if ( strpos( $response, 'FETCH (' ) !== false )
1253. {
1254. $response = "";
1255. while ( strpos( $response, 'BODY[TEXT]' ) === false )
1256. {
1257. $message .= $response;
1258. $response = $this->connection->getLine();
1259. }
1260.
1261. $response = $this->connection->getLine();
1262. while ( strpos( $response, $tag ) === false )
1263. {
1264. $message .= $response;
1265. $response = $this->connection->getLine();
1266. }
1267. }
1268. // skip the OK response ("{$tag} OK Fetch completed.")
1269. $response = trim( $this->getResponse( $tag, $response ) );
1270. if ( $this->responseType( $response ) != self::RESPONSE_OK )
1271. {
1272. throw new ezcMailTransportException( "The IMAP server could not fetch the message '{$msgNum}': {$response}." );
1273. }
1274. return $message;
1275. }
1276.
1277. /**
1278. * Returns the unique identifiers for the messages from the current mailbox.
1279. *
1280. * You can fetch the unique identifier for a specific message by
1281. * providing the $msgNum parameter.
1282. *
1283. * The unique identifier can be used to recognize mail from servers
1284. * between requests. In contrast to the message numbers the unique
1285. * numbers assigned to an email usually never changes.
1286. *
1287. * The format of the returned array is:
1288. * <code>
1289. * array( message_num => unique_id );
1290. * </code>
1291. *
1292. * Example:
1293. * <code>
1294. * array( 1 => 216, 2 => 217, 3 => 218, 4 => 219 );
1295. * </code>
1296. *
1297. * Before calling this method, a connection to the IMAP server must be
1298. * established and a user must be authenticated successfully, and a mailbox
1299. * must be selected.
1300. *
1301. * @todo add UIVALIDITY value to UID (like in POP3) (if necessary).
1302. *
1303. * @throws ezcMailTransportException
1304. * if a mailbox is not selected
1305. * or if the server sent a negative response
1306. * @param int $msgNum
1307. * @return array(string)
1308. */
1309. public function listUniqueIdentifiers( $msgNum = null )
1310. {
1311. if ( $this->state != self::STATE_SELECTED &&
1312. $this->state != self::STATE_SELECTED_READONLY )
1313. {
1314. throw new ezcMailTransportException( "Can't call listUniqueIdentifiers() on the IMAP transport when a mailbox is not selected." );
1315. }
1316.
1317. $result = array();
1318. if ( $msgNum !== null )
1319. {
1320. $tag = $this->getNextTag();
1321. $this->connection->sendData( "{$tag} UID SEARCH {$msgNum}" );
1322. $response = $this->getResponse( '* SEARCH' );
1323. if ( strpos( $response, '* SEARCH' ) !== false )
1324. {
1325. $result[(int)$msgNum] = trim( substr( $response, 9 ) );
1326. }
1327. $response = trim( $this->getResponse( $tag, $response ) );
1328. }
1329. else
1330. {
1331. $uids = array();
1332. $messages = array_keys( $this->listMessages() );
1333. $tag = $this->getNextTag();
1334. $this->connection->sendData( "{$tag} UID SEARCH UNDELETED" );
1335. $response = $this->getResponse( '* SEARCH' );
1336. if ( strpos( $response, '* SEARCH' ) !== false )
1337. {
1338. $response = trim( substr( $response, 9 ) );
1339. if ( $response !== "" )
1340. {
1341. $uids = explode( ' ', $response );
1342. }
1343. for ( $i = 0; $i < count( $messages ); $i++ )
1344. {
1345. $result[trim( $messages[$i] )] = $uids[$i];
1346. }
1347. }
1348. $response = trim( $this->getResponse( $tag ) );
1349. }
1350. if ( $this->responseType( $response ) != self::RESPONSE_OK )
1351. {
1352. throw new ezcMailTransportException( "The IMAP server could not fetch the unique identifiers: {$response}." );
1353. }
1354. return $result;
1355. }
1356.
1357. /**
1358. * Returns an {@link ezcMailImapSet} with all the messages from the current mailbox.
1359. *
1360. * This method supports unique IDs instead of message numbers. See
1361. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1362. * referencing.
1363. *
1364. * If $deleteFromServer is set to true the mail will be marked for deletion
1365. * after retrieval. If not it will be left intact.
1366. *
1367. * The set returned can be parsed with {@link ezcMailParser}.
1368. *
1369. * Before calling this method, a connection to the IMAP server must be
1370. * established and a user must be authenticated successfully, and a mailbox
1371. * must be selected.
1372. *
1373. * Example:
1374. * <code>
1375. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1376. * $imap->authenticate( 'username', 'password' );
1377. * $imap->selectMailbox( 'Inbox' );
1378. *
1379. * $set = $imap->fetchAll();
1380. *
1381. * // parse $set with ezcMailParser
1382. * $parser = new ezcMailParser();
1383. * $mails = $parser->parseMail( $set );
1384. * foreach ( $mails as $mail )
1385. * {
1386. * // process $mail which is an ezcMail object
1387. * }
1388. * </code>
1389. *
1390. * @throws ezcMailTransportException
1391. * if a mailbox is not selected
1392. * or if the server sent a negative response
1393. * @param bool $deleteFromServer
1394. * @return ezcMailParserSet
1395. */
1396. public function fetchAll( $deleteFromServer = false )
1397. {
1398. if ( $this->options->uidReferencing )
1399. {
1400. $messages = array_values( $this->listUniqueIdentifiers() );
1401. }
1402. else
1403. {
1404. $messages = array_keys( $this->listMessages() );
1405. }
1406.
1407. return new ezcMailImapSet( $this->connection, $messages, $deleteFromServer, array( 'uidReferencing' => $this->options->uidReferencing ) );
1408. }
1409.
1410. /**
1411. * Returns an {@link ezcMailImapSet} containing only the $number -th message in
1412. * the current mailbox.
1413. *
1414. * This method supports unique IDs instead of message numbers. See
1415. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1416. * referencing.
1417. *
1418. * If $deleteFromServer is set to true the mail will be marked for deletion
1419. * after retrieval. If not it will be left intact.
1420. *
1421. * Note: for IMAP the first message is 1 (so for $number = 0 an exception
1422. * will be thrown).
1423. *
1424. * Before calling this method, a connection to the IMAP server must be
1425. * established and a user must be authenticated successfully, and a mailbox
1426. * must be selected.
1427. *
1428. * Example:
1429. * <code>
1430. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1431. * $imap->authenticate( 'username', 'password' );
1432. * $imap->selectMailbox( 'Inbox' );
1433. *
1434. * $set = $imap->fetchByMessageNr( 1 );
1435. *
1436. * // $set can be parsed with ezcMailParser
1437. * </code>
1438. *
1439. * @throws ezcMailTransportException
1440. * if a mailbox is not selected
1441. * or if the server sent a negative response
1442. * @throws ezcMailNoSuchMessageException
1443. * if the message $number is out of range
1444. * @param int $number
1445. * @param bool $deleteFromServer
1446. * @return ezcMailImapSet
1447. */
1448. public function fetchByMessageNr( $number, $deleteFromServer = false )
1449. {
1450. if ( $this->options->uidReferencing )
1451. {
1452. $messages = array_flip( $this->listUniqueIdentifiers() );
1453. }
1454. else
1455. {
1456. $messages = $this->listMessages();
1457. }
1458.
1459. if ( !isset( $messages[$number] ) )
1460. {
1461. throw new ezcMailNoSuchMessageException( $number );
1462. }
1463.
1464. return new ezcMailImapSet( $this->connection, array( 0 => $number ), $deleteFromServer, array( 'uidReferencing' => $this->options->uidReferencing ) );
1465. }
1466.
1467. /**
1468. * Returns an {@link ezcMailImapSet} with $count messages starting from $offset from
1469. * the current mailbox.
1470. *
1471. * This method supports unique IDs instead of message numbers. See
1472. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1473. * referencing.
1474. *
1475. * Fetches $count messages starting from the $offset and returns them as a
1476. * {@link ezcMailImapSet}. If $count is not specified or if it is 0, it fetches
1477. * all messages starting from the $offset.
1478. *
1479. * Before calling this method, a connection to the IMAP server must be
1480. * established and a user must be authenticated successfully, and a mailbox
1481. * must be selected.
1482. *
1483. * Example:
1484. * <code>
1485. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1486. * $imap->authenticate( 'username', 'password' );
1487. * $imap->selectMailbox( 'Inbox' );
1488. *
1489. * $set = $imap->fetchFromOffset( 1, 10 );
1490. *
1491. * // $set can be parsed with ezcMailParser
1492. * </code>
1493. *
1494. * @throws ezcMailTransportException
1495. * if a mailbox is not selected
1496. * or if the server sent a negative response
1497. * @throws ezcMailInvalidLimitException
1498. * if $count is negative
1499. * @throws ezcMailOffsetOutOfRangeException
1500. * if $offset is outside of the existing range of messages
1501. * @param int $offset
1502. * @param int $count
1503. * @param bool $deleteFromServer
1504. * @return ezcMailImapSet
1505. */
1506. public function fetchFromOffset( $offset, $count = 0, $deleteFromServer = false )
1507. {
1508. if ( $count < 0 )
1509. {
1510. throw new ezcMailInvalidLimitException( $offset, $count );
1511. }
1512.
1513. if ( $this->options->uidReferencing )
1514. {
1515. $messages = array_values( $this->listUniqueIdentifiers() );
1516. $ids = array_flip( $messages );
1517.
1518. if ( $count === 0 )
1519. {
1520. $count = count( $messages );
1521. }
1522.
1523. if ( !isset( $ids[$offset] ) )
1524. {
1525. throw new ezcMailOffsetOutOfRangeException( $offset, $count );
1526. }
1527.
1528. $range = array();
1529. for ( $i = $ids[$offset]; $i < min( $count, count( $messages ) ); $i++ )
1530. {
1531. $range[] = $messages[$i];
1532. }
1533. }
1534. else
1535. {
1536. $messages = array_keys( $this->listMessages() );
1537.
1538. if ( $count === 0 )
1539. {
1540. $count = count( $messages );
1541. }
1542.
1543. $range = array_slice( $messages, $offset - 1, $count, true );
1544.
1545. if ( !isset( $range[$offset - 1] ) )
1546. {
1547. throw new ezcMailOffsetOutOfRangeException( $offset, $count );
1548. }
1549. }
1550.
1551. return new ezcMailImapSet( $this->connection, $range, $deleteFromServer, array( 'uidReferencing' => $this->options->uidReferencing ) );
1552. }
1553.
1554. /**
1555. * Returns an {@link ezcMailImapSet} containing the messages which match the
1556. * provided $criteria from the current mailbox.
1557. *
1558. * This method supports unique IDs instead of message numbers. See
1559. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1560. * referencing.
1561. *
1562. * See {@link http://www.faqs.org/rfcs/rfc1730.html} - 6.4.4. (or
1563. * {@link http://www.faqs.org/rfcs/rfc1730.html} - 6.4.4.) for criterias
1564. * which can be used for searching. The criterias can be combined in the
1565. * same search string (separate the criterias with spaces).
1566. *
1567. * If $criteria is null or empty then it will default to 'ALL' (returns all
1568. * messages in the mailbox).
1569. *
1570. * Before calling this method, a connection to the IMAP server must be
1571. * established and a user must be authenticated successfully, and a mailbox
1572. * must be selected.
1573. *
1574. * Examples:
1575. * <code>
1576. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1577. * $imap->authenticate( 'username', 'password' );
1578. * $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
1579. *
1580. * // return an ezcMailImapSet containing all messages flagged as 'SEEN'
1581. * $set = $imap->searchMailbox( 'SEEN' );
1582. *
1583. * // return an ezcMailImapSet containing messages with 'release' in their Subject
1584. * $set = $imap->searchMailbox( 'SUBJECT "release"' );
1585. *
1586. * // criterias can be combined:
1587. * // return an ezcMailImapSet containing messages flagged as 'SEEN' and
1588. * // with 'release' in their Subject
1589. * $set = $imap->searchMailbox( 'SEEN SUBJECT "release"' );
1590. *
1591. * // $set can be parsed with ezcMailParser
1592. * </code>
1593. *
1594. * @throws ezcMailTransportException
1595. * if a mailbox is not selected
1596. * or if the server sent a negative response
1597. * @param string $criteria
1598. * @return ezcMailImapSet
1599. */
1600. public function searchMailbox( $criteria = null )
1601. {
1602. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
1603.
1604. if ( $this->state != self::STATE_SELECTED &&
1605. $this->state != self::STATE_SELECTED_READONLY )
1606. {
1607. throw new ezcMailTransportException( "Can't call searchMailbox() on the IMAP transport when a mailbox is not selected." );
1608. }
1609.
1610. $criteria = trim( $criteria );
1611. if ( empty( $criteria ) )
1612. {
1613. $criteria = 'ALL';
1614. }
1615.
1616. $matchingMessages = array();
1617. $tag = $this->getNextTag();
1618. $this->connection->sendData( "{$tag} {$uid}SEARCH {$criteria}" );
1619.
1620. $response = $this->getResponse( '* SEARCH' );
1621. if ( strpos( $response, '* SEARCH' ) !== false )
1622. {
1623. $ids = substr( trim( $response ), 9 );
1624. if ( trim( $ids ) !== "" )
1625. {
1626. $matchingMessages = explode( ' ', $ids );
1627. }
1628. }
1629.
1630. $response = trim( $this->getResponse( $tag, $response ) );
1631. if ( $this->responseType( $response ) != self::RESPONSE_OK )
1632. {
1633. throw new ezcMailTransportException( "The IMAP server could not search the messages by the specified criteria: {$response}." );
1634. }
1635.
1636. return new ezcMailImapSet( $this->connection, array_values( $matchingMessages ), false, array( 'uidReferencing' => $this->options->uidReferencing ) );
1637. }
1638.
1639. /**
1640. * Returns an {@link ezcMailImapSet} containing $count messages starting
1641. * from $offset sorted by $sortCriteria from the current mailbox.
1642. *
1643. * This method supports unique IDs instead of message numbers. See
1644. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1645. * referencing.
1646. *
1647. * It is useful for paging through a mailbox.
1648. *
1649. * Fetches $count messages starting from the $offset and returns them as a
1650. * {@link ezcMailImapSet}. If $count is is 0, it fetches all messages
1651. * starting from the $offset.
1652. *
1653. * $sortCriteria is an email header like: Subject, To, From, Date, Sender, etc.
1654. *
1655. * Before calling this method, a connection to the IMAP server must be
1656. * established and a user must be authenticated successfully, and a mailbox
1657. * must be selected.
1658. *
1659. * Example:
1660. * <code>
1661. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1662. * $imap->authenticate( 'username', 'password' );
1663. * $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
1664. *
1665. * // Fetch a range of messages sorted by Date
1666. * $set = $imap->sortFromOffset( 1, 10, "Date" );
1667. *
1668. * // $set can be parsed with ezcMailParser
1669. * </code>
1670. *
1671. * @throws ezcMailTransportException
1672. * if a mailbox is not selected
1673. * or if the server sent a negative response
1674. * @throws ezcMailInvalidLimitException
1675. * if $count is negative
1676. * @throws ezcMailOffsetOutOfRangeException
1677. * if $offset is outside of the existing range of messages
1678. * @param int $offset
1679. * @param int $count
1680. * @param string $sortCriteria
1681. * @param bool $reverse
1682. * @return ezcMailImapSet
1683. */
1684. public function sortFromOffset( $offset, $count = 0, $sortCriteria, $reverse = false )
1685. {
1686. if ( $count < 0 )
1687. {
1688. throw new ezcMailInvalidLimitException( $offset, $count );
1689. }
1690.
1691. if ( $this->options->uidReferencing )
1692. {
1693. $uids = array_values( $this->listUniqueIdentifiers() );
1694.
1695. $flip = array_flip( $uids );
1696. if ( !isset( $flip[$offset] ) )
1697. {
1698. throw new ezcMailOffsetOutOfRangeException( $offset, $count );
1699. }
1700.
1701. $start = $flip[$offset];
1702.
1703. $messages = $this->sort( $uids, $sortCriteria, $reverse );
1704.
1705. if ( $count === 0 )
1706. {
1707. $count = count( $messages );
1708. }
1709.
1710. $ids = array_keys( $messages );
1711.
1712. for ( $i = $start; $i < $count; $i++ )
1713. {
1714. $range[] = $ids[$i];
1715. }
1716. }
1717. else
1718. {
1719. $messageCount = $this->countByFlag( 'ALL' );
1720. $messages = array_keys( $this->sort( range( 1, $messageCount ), $sortCriteria, $reverse ) );
1721.
1722. if ( $count === 0 )
1723. {
1724. $count = count( $messages );
1725. }
1726.
1727. $range = array_slice( $messages, $offset - 1, $count, true );
1728.
1729. if ( !isset( $range[$offset - 1] ) )
1730. {
1731. throw new ezcMailOffsetOutOfRangeException( $offset, $count );
1732. }
1733. }
1734.
1735. return new ezcMailImapSet( $this->connection, $range, false, array( 'uidReferencing' => $this->options->uidReferencing ) );
1736. }
1737.
1738. /**
1739. * Returns an {@link ezcMailImapSet} containing messages $messages sorted by
1740. * $sortCriteria from the current mailbox.
1741. *
1742. * This method supports unique IDs instead of message numbers. See
1743. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1744. * referencing.
1745. *
1746. * $messages is an array of message numbers, for example:
1747. * <code>
1748. * array( 1, 2, 4 );
1749. * </code>
1750. *
1751. * $sortCriteria is an email header like: Subject, To, From, Date, Sender, etc.
1752. *
1753. * Before calling this method, a connection to the IMAP server must be
1754. * established and a user must be authenticated successfully, and a mailbox
1755. * must be selected.
1756. *
1757. * Example:
1758. * <code>
1759. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1760. * $imap->authenticate( 'username', 'password' );
1761. * $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
1762. *
1763. * // Fetch the list of messages sorted by Date
1764. * $set = $imap->sortMessages( 1, 10, "Date" );
1765. *
1766. * // $set can be parsed with ezcMailParser
1767. * </code>
1768. *
1769. * @throws ezcMailTransportException
1770. * if a mailbox is not selected
1771. * or if the server sent a negative response
1772. * or if array $messages is empty
1773. * @param array(int) $messages
1774. * @param string $sortCriteria
1775. * @param bool $reverse
1776. * @return ezcMailImapSet
1777. */
1778. public function sortMessages( $messages, $sortCriteria, $reverse = false )
1779. {
1780. $messages = $this->sort( $messages, $sortCriteria, $reverse );
1781. return new ezcMailImapSet( $this->connection, array_keys ( $messages ), false, array( 'uidReferencing' => $this->options->uidReferencing ) );
1782. }
1783.
1784. /**
1785. * Returns an {@link ezcMailImapSet} containing messages with a certain flag from
1786. * the current mailbox.
1787. *
1788. * This method supports unique IDs instead of message numbers. See
1789. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1790. * referencing.
1791. *
1792. * $flag can be one of:
1793. *
1794. * Basic flags:
1795. * - ANSWERED - message has been answered
1796. * - DELETED - message is marked to be deleted by later EXPUNGE
1797. * - DRAFT - message is marked as a draft
1798. * - FLAGGED - message is "flagged" for urgent/special attention
1799. * - RECENT - message is recent
1800. * - SEEN - message has been read
1801. *
1802. * Opposites of the above flags:
1803. * - UNANSWERED
1804. * - UNDELETED
1805. * - UNDRAFT
1806. * - UNFLAGGED
1807. * - OLD
1808. * - UNSEEN
1809. *
1810. * Composite flags:
1811. * - NEW - equivalent to RECENT + UNSEEN
1812. * - ALL - all the messages
1813. *
1814. * Before calling this method, a connection to the IMAP server must be
1815. * established and a user must be authenticated successfully, and a mailbox
1816. * must be selected.
1817. *
1818. * Example:
1819. * <code>
1820. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1821. * $imap->authenticate( 'username', 'password' );
1822. * $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
1823. *
1824. * // Fetch the messages marked with the RECENT flag
1825. * $set = $imap->fetchByFlag( 'RECENT' );
1826. *
1827. * // $set can be parsed with ezcMailParser
1828. * </code>
1829. *
1830. * @throws ezcMailTransportException
1831. * if a mailbox is not selected
1832. * or if the server sent a negative response
1833. * or if $flag is not valid
1834. * @param string $flag
1835. * @return ezcMailImapSet
1836. */
1837. public function fetchByFlag( $flag )
1838. {
1839. $messages = $this->searchByFlag( $flag );
1840. return new ezcMailImapSet( $this->connection, $messages, false, array( 'uidReferencing' => $this->options->uidReferencing ) );
1841. }
1842.
1843. /**
1844. * Wrapper function to fetch count of messages by a certain flag.
1845. *
1846. * $flag can be one of:
1847. *
1848. * Basic flags:
1849. * - ANSWERED - message has been answered
1850. * - DELETED - message is marked to be deleted by later EXPUNGE
1851. * - DRAFT - message is marked as a draft
1852. * - FLAGGED - message is "flagged" for urgent/special attention
1853. * - RECENT - message is recent
1854. * - SEEN - message has been read
1855. *
1856. * Opposites of the above flags:
1857. * - UNANSWERED
1858. * - UNDELETED
1859. * - UNDRAFT
1860. * - UNFLAGGED
1861. * - OLD
1862. * - UNSEEN
1863. *
1864. * Composite flags:
1865. * - NEW - equivalent to RECENT + UNSEEN
1866. * - ALL - all the messages
1867. *
1868. * Before calling this method, a connection to the IMAP server must be
1869. * established and a user must be authenticated successfully, and a mailbox
1870. * must be selected.
1871. *
1872. * @throws ezcMailTransportException
1873. * if a mailbox is not selected
1874. * or if the server sent a negative response
1875. * or if $flag is not valid
1876. * @param string $flag
1877. * @return int
1878. */
1879. public function countByFlag( $flag )
1880. {
1881. $flag = $this->normalizeFlag( $flag );
1882. $messages = $this->searchByFlag( $flag );
1883. return count( $messages );
1884. }
1885.
1886. /**
1887. * Fetches IMAP flags for messages $messages.
1888. *
1889. * This method supports unique IDs instead of message numbers. See
1890. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1891. * referencing.
1892. *
1893. * $messages is an array of message numbers, for example:
1894. * <code>
1895. * array( 1, 2, 4 );
1896. * </code>
1897. *
1898. * The format of the returned array is:
1899. * <code>
1900. * array( message_number => array( flags ) )
1901. * </code>
1902. *
1903. * Before calling this method, a connection to the IMAP server must be
1904. * established and a user must be authenticated successfully, and a mailbox
1905. * must be selected.
1906. *
1907. * Example:
1908. * <code>
1909. * $imap = new ezcMailImapTransport( 'imap.example.com' );
1910. * $imap->authenticate( 'username', 'password' );
1911. * $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
1912. *
1913. * $flags = $imap->fetchFlags( array( 1, 2, 4 ) );
1914. * </code>
1915. *
1916. * The returned array $flags will be something like:
1917. * <code>
1918. * array( 1 => array( '\Seen' ),
1919. * 2 => array( '\Seen' ),
1920. * 4 => array( '\Seen', 'NonJunk' )
1921. * );
1922. * </code>
1923. *
1924. * @throws ezcMailTransportException
1925. * if a mailbox is not selected
1926. * or if the server sent a negative response
1927. * @param array $messages
1928. * @return array(mixed)
1929. */
1930. public function fetchFlags( $messages )
1931. {
1932. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
1933.
1934. if ( $this->state != self::STATE_SELECTED &&
1935. $this->state != self::STATE_SELECTED_READONLY )
1936. {
1937. throw new ezcMailTransportException( "Can't call fetchFlags() on the IMAP transport when a mailbox is not selected." );
1938. }
1939.
1940. $flags = array();
1941. $ids = implode( $messages, ',' );
1942.
1943. $tag = $this->getNextTag();
1944. $this->connection->sendData( "{$tag} {$uid}FETCH {$ids} (FLAGS)" );
1945.
1946. $response = trim( $this->connection->getLine() );
1947. while ( strpos( $response, $tag ) === false )
1948. {
1949. if ( strpos( $response, ' FETCH (' ) !== false )
1950. {
1951. if ( $this->options->uidReferencing )
1952. {
1953. preg_match( '/\*\s.*\sFETCH\s\(FLAGS \((.*)\)\sUID\s(.*)\)/U', $response, $matches );
1954. $parts = explode( ' ', $matches[1] );
1955. $flags[intval( $matches[2] )] = $parts;
1956. }
1957. else
1958. {
1959. preg_match( '/\*\s(.*)\sFETCH\s\(FLAGS \((.*)\)/U', $response, $matches );
1960. $parts = explode( ' ', $matches[2] );
1961. $flags[intval( $matches[1] )] = $parts;
1962. }
1963. }
1964. $response = trim( $this->connection->getLine() );
1965. }
1966.
1967. if ( $this->responseType( $response ) != self::RESPONSE_OK )
1968. {
1969. throw new ezcMailTransportException( "The IMAP server could not fetch flags for the messages '{$messages}': {$response}." );
1970. }
1971. return $flags;
1972. }
1973.
1974. /**
1975. * Sets $flag on $messages.
1976. *
1977. * This method supports unique IDs instead of message numbers. See
1978. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
1979. * referencing.
1980. *
1981. * $messages can be:
1982. * - a single message number (eg. 1)
1983. * - a message range (eg. 1:4)
1984. * - a message list (eg. 1,2,4)
1985. *
1986. * $flag can be one of:
1987. * - ANSWERED - message has been answered
1988. * - DELETED - message is marked to be deleted by later EXPUNGE
1989. * - DRAFT - message is marked as a draft
1990. * - FLAGGED - message is "flagged" for urgent/special attention
1991. * - SEEN - message has been read
1992. *
1993. * This function automatically adds the '\' in front of the flag when
1994. * calling the server command.
1995. *
1996. * Before calling this method, a connection to the IMAP server must be
1997. * established and a user must be authenticated successfully, and a mailbox
1998. * must be selected.
1999. *
2000. * Example:
2001. * <code>
2002. * $imap = new ezcMailImapTransport( 'imap.example.com' );
2003. * $imap->authenticate( 'username', 'password' );
2004. * $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
2005. *
2006. * $imap->setFlag( '1:4', 'DRAFT' );
2007. * </code>
2008. *
2009. * @throws ezcMailTransportException
2010. * if a mailbox is not selected
2011. * or if the server sent a negative response
2012. * or if $flag is not valid
2013. * @param string $messages
2014. * @param string $flag
2015. * @return bool
2016. */
2017. public function setFlag( $messages, $flag )
2018. {
2019. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
2020.
2021. if ( $this->state != self::STATE_SELECTED )
2022. {
2023. throw new ezcMailTransportException( "Can't call setFlag() when a mailbox is not selected." );
2024. }
2025.
2026. $flag = $this->normalizeFlag( $flag );
2027. if ( in_array( $flag, self::$basicFlags ) )
2028. {
2029. $tag = $this->getNextTag();
2030. $this->connection->sendData( "{$tag} {$uid}STORE {$messages} +FLAGS (\\{$flag})" );
2031. $response = trim( $this->getResponse( $tag ) );
2032. if ( $this->responseType( $response ) != self::RESPONSE_OK )
2033. {
2034. throw new ezcMailTransportException( "The IMAP server could not set flag '{$flag}' on the messages '{$messages}': {$response}." );
2035. }
2036. }
2037. else
2038. {
2039. throw new ezcMailTransportException( "Flag '{$flag}' is not allowed for setting." );
2040. }
2041. return true;
2042. }
2043.
2044. /**
2045. * Clears $flag from $messages.
2046. *
2047. * This method supports unique IDs instead of message numbers. See
2048. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
2049. * referencing.
2050. *
2051. * $messages can be:
2052. * - a single message number (eg. '1')
2053. * - a message range (eg. '1:4')
2054. * - a message list (eg. '1,2,4')
2055. *
2056. * $flag can be one of:
2057. * - ANSWERED - message has been answered
2058. * - DELETED - message is marked to be deleted by later EXPUNGE
2059. * - DRAFT - message is marked as a draft
2060. * - FLAGGED - message is "flagged" for urgent/special attention
2061. * - SEEN - message has been read
2062. *
2063. * This function automatically adds the '\' in front of the flag when
2064. * calling the server command.
2065. *
2066. * Before calling this method, a connection to the IMAP server must be
2067. * established and a user must be authenticated successfully, and a mailbox
2068. * must be selected.
2069. *
2070. * Example:
2071. * <code>
2072. * $imap = new ezcMailImapTransport( 'imap.example.com' );
2073. * $imap->authenticate( 'username', 'password' );
2074. * $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
2075. *
2076. * $imap->clearFlag( '1:4', 'DRAFT' );
2077. * </code>
2078. *
2079. * @throws ezcMailTransportException
2080. * if a mailbox is not selected
2081. * or if the server sent a negative response
2082. * or if $flag is not valid
2083. * @param string $messages
2084. * @param string $flag
2085. * @return bool
2086. */
2087. public function clearFlag( $messages, $flag )
2088. {
2089. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
2090.
2091. if ( $this->state != self::STATE_SELECTED )
2092. {
2093. throw new ezcMailTransportException( "Can't call clearFlag() when a mailbox is not selected." );
2094. }
2095.
2096. $flag = $this->normalizeFlag( $flag );
2097. if ( in_array( $flag, self::$basicFlags ) )
2098. {
2099. $tag = $this->getNextTag();
2100. $this->connection->sendData( "{$tag} {$uid}STORE {$messages} -FLAGS (\\{$flag})" );
2101. $response = trim( $this->getResponse( $tag ) );
2102. if ( $this->responseType( $response ) != self::RESPONSE_OK )
2103. {
2104. throw new ezcMailTransportException( "The IMAP server could not clear flag '{$flag}' on the messages '{$messages}': {$response}." );
2105. }
2106. }
2107. else
2108. {
2109. throw new ezcMailTransportException( "Flag '{$flag}' is not allowed for clearing." );
2110. }
2111. return true;
2112. }
2113.
2114. /**
2115. * Returns an array of message numbers from the selected mailbox which have a
2116. * certain flag set.
2117. *
2118. * This method supports unique IDs instead of message numbers. See
2119. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
2120. * referencing.
2121. *
2122. * $flag can be one of:
2123. *
2124. * Basic flags:
2125. * - ANSWERED - message has been answered
2126. * - DELETED - message is marked to be deleted by later EXPUNGE
2127. * - DRAFT - message is marked as a draft
2128. * - FLAGGED - message is "flagged" for urgent/special attention
2129. * - RECENT - message is recent
2130. * - SEEN - message has been read
2131. *
2132. * Opposites of the above flags:
2133. * - UNANSWERED
2134. * - UNDELETED
2135. * - UNDRAFT
2136. * - UNFLAGGED
2137. * - OLD
2138. * - UNSEEN
2139. *
2140. * Composite flags:
2141. * - NEW - equivalent to RECENT + UNSEEN
2142. * - ALL - all the messages
2143. *
2144. * The returned array is something like this:
2145. * <code>
2146. * array( 0 => 1, 1 => 5 );
2147. * </code>
2148. *
2149. * Before calling this method, a connection to the IMAP server must be
2150. * established and a user must be authenticated successfully, and a mailbox
2151. * must be selected.
2152. *
2153. * @throws ezcMailTransportException
2154. * if a mailbox is not selected
2155. * or if the server sent a negative response
2156. * or if $flag is not valid
2157. * @param string $flag
2158. * @return array(int)
2159. */
2160. protected function searchByFlag( $flag )
2161. {
2162. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
2163.
2164. if ( $this->state != self::STATE_SELECTED &&
2165. $this->state != self::STATE_SELECTED_READONLY )
2166. {
2167. throw new ezcMailTransportException( "Can't call searchByFlag() on the IMAP transport when a mailbox is not selected." );
2168. }
2169.
2170. $matchingMessages = array();
2171. $flag = $this->normalizeFlag( $flag );
2172. if ( in_array( $flag, self::$extendedFlags ) )
2173. {
2174. $tag = $this->getNextTag();
2175. $this->connection->sendData( "{$tag} {$uid}SEARCH ({$flag})" );
2176. $response = $this->getResponse( '* SEARCH' );
2177.
2178. if ( strpos( $response, '* SEARCH' ) !== false )
2179. {
2180. $ids = substr( trim( $response ), 9 );
2181. if ( trim( $ids ) !== "" )
2182. {
2183. $matchingMessages = explode( ' ', $ids );
2184. }
2185. }
2186. $response = trim( $this->getResponse( $tag, $response ) );
2187. if ( $this->responseType( $response ) != self::RESPONSE_OK )
2188. {
2189. throw new ezcMailTransportException( "The IMAP server could not search the messages by flags: {$response}." );
2190. }
2191. }
2192. else
2193. {
2194. throw new ezcMailTransportException( "Flag '{$flag}' is not allowed for searching." );
2195. }
2196. return $matchingMessages;
2197. }
2198.
2199. /**
2200. * Sends a NOOP command to the server, use it to keep the connection alive.
2201. *
2202. * Before calling this method, a connection to the IMAP server must be
2203. * established.
2204. *
2205. * @throws ezcMailTransportException
2206. * if there was no connection to the server
2207. * or if the server sent a negative response
2208. */
2209. public function noop()
2210. {
2211. if ( $this->state != self::STATE_NOT_AUTHENTICATED &&
2212. $this->state != self::STATE_AUTHENTICATED &&
2213. $this->state != self::STATE_SELECTED &&
2214. $this->state != self::STATE_SELECTED_READONLY )
2215. {
2216. throw new ezcMailTransportException( "Can not issue NOOP command if not connected." );
2217. }
2218.
2219. $tag = $this->getNextTag();
2220. $this->connection->sendData( "{$tag} NOOP" );
2221. $response = trim( $this->getResponse( $tag ) );
2222. if ( $this->responseType( $response ) != self::RESPONSE_OK )
2223. {
2224. throw new ezcMailTransportException( "NOOP failed: {$response}." );
2225. }
2226. }
2227.
2228. /**
2229. * Returns an array with the capabilities of the IMAP server.
2230. *
2231. * The returned array will be something like this:
2232. * <code>
2233. * array( 'IMAP4rev1', 'SASL-IR SORT', 'THREAD=REFERENCES', 'MULTIAPPEND',
2234. * 'UNSELECT', 'LITERAL+', 'IDLE', 'CHILDREN', 'NAMESPACE',
2235. * 'LOGIN-REFERRALS'
2236. * );
2237. * </code>
2238. *
2239. * Before calling this method, a connection to the IMAP server must be
2240. * established.
2241. *
2242. * @throws ezcMailTransportException
2243. * if there was no connection to the server
2244. * or if the server sent a negative response
2245. * @return array(string)
2246. */
2247. public function capability()
2248. {
2249. if ( $this->state != self::STATE_NOT_AUTHENTICATED &&
2250. $this->state != self::STATE_AUTHENTICATED &&
2251. $this->state != self::STATE_SELECTED &&
2252. $this->state != self::STATE_SELECTED_READONLY )
2253. {
2254. throw new ezcMailTransportException( "Trying to request capability when not connected to server." );
2255. }
2256.
2257. $tag = $this->getNextTag();
2258. $this->connection->sendData( "{$tag} CAPABILITY" );
2259.
2260. $response = $this->connection->getLine();
2261. while ( $this->responseType( $response ) != self::RESPONSE_UNTAGGED &&
2262. strpos( $response, '* CAPABILITY ' ) === false )
2263. {
2264. $response = $this->connection->getLine();
2265. }
2266. $result = trim( $response );
2267.
2268. $response = trim( $this->getResponse( $tag ) );
2269. if ( $this->responseType( $response ) != self::RESPONSE_OK )
2270. {
2271. throw new ezcMailTransportException( "The IMAP server responded negative to the CAPABILITY command: {$response}." );
2272. }
2273.
2274. return explode( ' ', str_replace( '* CAPABILITY ', '', $result ) );
2275. }
2276.
2277. /**
2278. * Sends an EXPUNGE command to the server.
2279. *
2280. * This method permanently deletes the messages marked for deletion by
2281. * the method {@link delete()}.
2282. *
2283. * Before calling this method, a connection to the IMAP server must be
2284. * established and a user must be authenticated successfully, and a mailbox
2285. * must be selected.
2286. *
2287. * @throws ezcMailTransportException
2288. * if a mailbox was not selected
2289. * or if the server sent a negative response
2290. */
2291. public function expunge()
2292. {
2293. if ( $this->state != self::STATE_SELECTED )
2294. {
2295. throw new ezcMailTransportException( "Can not issue EXPUNGE command if a mailbox is not selected." );
2296. }
2297.
2298. $tag = $this->getNextTag();
2299. $this->connection->sendData( "{$tag} EXPUNGE" );
2300. $response = trim( $this->getResponse( $tag ) );
2301. if ( $this->responseType( $response ) != self::RESPONSE_OK )
2302. {
2303. throw new ezcMailTransportException( "EXPUNGE failed: {$response}." );
2304. }
2305. }
2306.
2307. /**
2308. * Appends $mail to the $mailbox mailbox.
2309. *
2310. * Use this method to create email messages in a mailbox such as Sent or
2311. * Draft.
2312. *
2313. * $flags is an array of flags to be set to the $mail (if provided):
2314. *
2315. * $flag can be one of:
2316. * - ANSWERED - message has been answered
2317. * - DELETED - message is marked to be deleted by later EXPUNGE
2318. * - DRAFT - message is marked as a draft
2319. * - FLAGGED - message is "flagged" for urgent/special attention
2320. * - SEEN - message has been read
2321. *
2322. * This function automatically adds the '\' in front of each flag when
2323. * calling the server command.
2324. *
2325. * Before calling this method, a connection to the IMAP server must be
2326. * established and a user must be authenticated successfully.
2327. *
2328. * @throws ezcMailTransportException
2329. * if user is not authenticated
2330. * or if the server sent a negative response
2331. * or if $mailbox does not exists
2332. * @param string $mailbox
2333. * @param string $mail
2334. * @param array(string) $flags
2335. */
2336. public function append( $mailbox, $mail, $flags = null )
2337. {
2338. if ( $this->state != self::STATE_AUTHENTICATED &&
2339. $this->state != self::STATE_SELECTED &&
2340. $this->state != self::STATE_SELECTED_READONLY )
2341. {
2342. throw new ezcMailTransportException( "Can't call append() if not authenticated." );
2343. }
2344.
2345. $tag = $this->getNextTag();
2346. $mailSize = strlen( $mail );
2347. if ( !is_null( $flags ) )
2348. {
2349. for ( $i = 0; $i < count( $flags ); $i++ )
2350. {
2351. $flags[$i] = '\\' . $this->normalizeFlag( $flags[$i] );
2352. }
2353. $flagList = implode( ' ', $flags );
2354. $command = "{$tag} APPEND {$mailbox} ({$flagList}) {{$mailSize}}";
2355. }
2356. else
2357. {
2358. $command = "{$tag} APPEND {$mailbox} {{$mailSize}}";
2359. }
2360.
2361. $this->connection->sendData( $command );
2362. $response = trim( $this->connection->getLine() );
2363.
2364. if ( strpos( $response, 'TRYCREATE' ) !== false )
2365. {
2366. throw new ezcMailTransportException( "Mailbox does not exist: {$response}." );
2367. }
2368.
2369. if ( $this->responseType( $response ) == self::RESPONSE_FEEDBACK )
2370. {
2371. $this->connection->sendData( $mail );
2372. $response = trim( $this->getResponse( $tag ) );
2373. if ( $this->responseType( $response ) != self::RESPONSE_OK )
2374. {
2375. throw new ezcMailTransportException( "The IMAP server could not append message to mailbox '{$mailbox}': {$response}." );
2376. }
2377. }
2378. elseif ( $this->responseType( $response ) != self::RESPONSE_OK )
2379. {
2380. throw new ezcMailTransportException( "The IMAP server could not append message to mailbox '{$mailbox}': {$response}." );
2381. }
2382. }
2383.
2384. /**
2385. * Clears $flag of unwanted characters and makes it uppercase.
2386. *
2387. * @param string $flag
2388. * @return string
2389. */
2390. protected function normalizeFlag( $flag )
2391. {
2392. $flag = strtoupper( $flag );
2393. $flag = str_replace( '\\', '', $flag );
2394. return trim( $flag );
2395. }
2396.
2397. /**
2398. * Sorts message numbers array $messages by the specified $sortCriteria.
2399. *
2400. * This method supports unique IDs instead of message numbers. See
2401. * {@link ezcMailImapTransportOptions} for how to enable unique IDs
2402. * referencing.
2403. *
2404. * $messages is an array of message numbers, for example:
2405. * <code>
2406. * array( 1, 2, 4 );
2407. * </code>
2408. *
2409. * $sortCriteria is an email header like: Subject, To, From, Date, Sender.
2410. *
2411. * The sorting is done with the php function natcasesort().
2412. *
2413. * Before calling this method, a connection to the IMAP server must be
2414. * established and a user must be authenticated successfully, and a mailbox
2415. * must be selected.
2416. *
2417. * @throws ezcMailTransportException
2418. * if a mailbox is not selected
2419. * or if the server sent a negative response
2420. * or if the array $messages is empty
2421. * @param array(int) $messages
2422. * @param string $sortCriteria
2423. * @param bool $reverse
2424. * @return array(string)
2425. */
2426. protected function sort( $messages, $sortCriteria, $reverse = false )
2427. {
2428. $uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
2429.
2430. if ( $this->state != self::STATE_SELECTED &&
2431. $this->state != self::STATE_SELECTED_READONLY )
2432. {
2433. throw new ezcMailTransportException( "Can't call sort() on the IMAP transport when a mailbox is not selected." );
2434. }
2435.
2436. $result = array();
2437. $query = ucfirst( strtolower( $sortCriteria ) );
2438. $messageNumbers = implode( ',', $messages );
2439.
2440. $tag = $this->getNextTag();
2441. $this->connection->sendData( "{$tag} {$uid}FETCH {$messageNumbers} (BODY.PEEK[HEADER.FIELDS ({$query})])" );
2442.
2443. $response = trim( $this->connection->getLine() );
2444. while ( strpos( $response, $tag ) === false )
2445. {
2446. if ( strpos( $response, ' FETCH (' ) !== false )
2447. {
2448. if ( $this->options->uidReferencing )
2449. {
2450. preg_match('/^\* [0-9]+ FETCH \(UID ([0-9]+)/', $response, $matches );
2451. }
2452. else
2453. {
2454. preg_match('/^\* ([0-9]+) FETCH/', $response, $matches );
2455. }
2456. $messageNumber = $matches[1];
2457. }
2458.
2459. if ( strpos( $response, $query ) !== false )
2460. {
2461. $strippedResponse = trim( trim( str_replace( "{$query}: ", '', $response ) ), '"' );
2462. switch ( $query )
2463. {
2464. case 'Date':
2465. $strippedResponse = strtotime( $strippedResponse );
2466. break;
2467. case 'Subject':
2468. case 'From':
2469. case 'Sender':
2470. case 'To':
2471. $strippedResponse = ezcMailTools::mimeDecode( $strippedResponse );
2472. break;
2473. default:
2474. break;
2475. }
2476. $result[$messageNumber] = $strippedResponse;
2477. }
2478.
2479. // in case the mail doesn't have the $sortCriteria header (like junk mail missing Subject header)
2480. if ( strpos( $response, ')' ) !== false && !isset( $result[$messageNumber] ) )
2481. {
2482. $result[$messageNumber] = '';
2483. }
2484.
2485. $response = trim( $this->connection->getLine() );
2486. }
2487.
2488. if ( $this->responseType( $response ) != self::RESPONSE_OK )
2489. {
2490. throw new ezcMailTransportException( "The IMAP server could not sort the messages: {$response}." );
2491. }
2492.
2493. if ( $reverse === true )
2494. {
2495. natcasesort( $result );
2496. $result = array_reverse( $result, true );
2497. }
2498. else
2499. {
2500. natcasesort( $result );
2501. }
2502. return $result;
2503. }
2504.
2505. /**
2506. * Parses $line to return the response code.
2507. *
2508. * Returns one of the following:
2509. * - {@link RESPONSE_OK}
2510. * - {@link RESPONSE_NO}
2511. * - {@link RESPONSE_BAD}
2512. * - {@link RESPONSE_UNTAGGED}
2513. * - {@link RESPONSE_FEEDBACK}
2514. *
2515. * @throws ezcMailTransportException
2516. * if the IMAP response ($line) is not recognized
2517. * @param string $line
2518. * @return int
2519. */
2520. protected function responseType( $line )
2521. {
2522. if ( strpos( $line, 'OK ' ) !== false && strpos( $line, 'OK ' ) == 6 )
2523. {
2524. return self::RESPONSE_OK;
2525. }
2526. if ( strpos( $line, 'NO ' ) !== false && strpos( $line, 'NO ' ) == 6 )
2527. {
2528. return self::RESPONSE_NO;
2529. }
2530. if ( strpos( $line, 'BAD ' ) !== false && strpos( $line, 'BAD ' ) == 6 )
2531. {
2532. return self::RESPONSE_BAD;
2533. }
2534. if ( strpos( $line, '* ' ) !== false && strpos( $line, '* ' ) == 0 )
2535. {
2536. return self::RESPONSE_UNTAGGED;
2537. }
2538. if ( strpos( $line, '+ ' ) !== false && strpos( $line, '+ ' ) == 0 )
2539. {
2540. return self::RESPONSE_FEEDBACK;
2541. }
2542. throw new ezcMailTransportException( "Unrecognized IMAP response in line: {$line}" );
2543. }
2544.
2545. /**
2546. * Reads the responses from the server until encountering $tag.
2547. *
2548. * In IMAP, each command sent by the client is prepended with a
2549. * alphanumeric tag like 'A1234'. The server sends the response
2550. * to the client command as lines, and the last line in the response
2551. * is prepended with the same tag, and it contains the status of
2552. * the command completion ('OK', 'NO' or 'BAD').
2553. *
2554. * Sometimes the server sends alerts and response lines from other
2555. * commands before sending the tagged line, so this method just
2556. * reads all the responses until it encounters $tag.
2557. *
2558. * It returns the tagged line to be processed by the calling method.
2559. *
2560. * If $response is specified, then it will not read the response
2561. * from the server before searching for $tag in $response.
2562. *
2563. * Before calling this method, a connection to the IMAP server must be
2564. * established.
2565. *
2566. * @param string $tag
2567. * @param string $response
2568. * @return string
2569. */
2570. protected function getResponse( $tag, $response = null )
2571. {
2572. if ( is_null( $response ) )
2573. {
2574. $response = $this->connection->getLine();
2575. }
2576. while ( strpos( $response, $tag ) === false )
2577. {
2578. if ( strpos( $response, ' BAD ' ) !== false ||
2579. strpos( $response, ' NO ' ) !== false )
2580. {
2581. break;
2582. }
2583. $response = $this->connection->getLine();
2584. }
2585. return $response;
2586. }
2587.
2588. /**
2589. * Generates the next IMAP tag to prepend to client commands.
2590. *
2591. * The structure of the IMAP tag is Axxxx, where:
2592. * - A is a letter (uppercase for conformity)
2593. * - x is a digit from 0 to 9
2594. *
2595. * example of generated tag: T5439
2596. *
2597. * It uses the class variable $this->currentTag.
2598. *
2599. * Everytime it is called, the tag increases by 1.
2600. *
2601. * If it reaches the last tag, it wraps around to the first tag.
2602. *
2603. * By default, the first generated tag is A0001.
2604. *
2605. * @return string
2606. */
2607. protected function getNextTag()
2608. {
2609. $tagLetter = substr( $this->currentTag, 0, 1 );
2610. $tagNumber = intval( substr( $this->currentTag, 1 ) );
2611. $tagNumber++;
2612. if ( $tagLetter == 'Z' && $tagNumber == 10000 )
2613. {
2614. $tagLetter = 'A';
2615. $tagNumber = 1;
2616. }
2617. if ( $tagNumber == 10000 )
2618. {
2619. $tagLetter++;
2620. $tagNumber = 0;
2621. }
2622. $this->currentTag = $tagLetter . sprintf( "%04s", $tagNumber );
2623. return $this->currentTag;
2624. }
2625. }
2626. ?>