1 /**
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.geronimo.javamail.store.imap.connection;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.OutputStream;
23 import java.io.PrintStream;
24 import java.io.UnsupportedEncodingException;
25 import java.io.Writer;
26 import java.net.InetAddress;
27 import java.net.Socket;
28 import java.net.SocketException;
29 import java.net.UnknownHostException;
30 import java.util.ArrayList;
31 import java.util.Date;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.LinkedList;
35 import java.util.List;
36 import java.util.StringTokenizer;
37
38 import javax.mail.Address;
39 import javax.mail.AuthenticationFailedException;
40 import javax.mail.FetchProfile;
41 import javax.mail.Flags;
42 import javax.mail.Folder;
43 import javax.mail.Message;
44 import javax.mail.MessagingException;
45 import javax.mail.MethodNotSupportedException;
46 import javax.mail.Quota;
47 import javax.mail.Session;
48 import javax.mail.UIDFolder;
49 import javax.mail.URLName;
50
51 import javax.mail.internet.InternetHeaders;
52
53 import javax.mail.search.SearchTerm;
54
55 import org.apache.geronimo.javamail.authentication.AuthenticatorFactory;
56 import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
57 import org.apache.geronimo.javamail.authentication.LoginAuthenticator;
58 import org.apache.geronimo.javamail.authentication.PlainAuthenticator;
59 import org.apache.geronimo.javamail.store.imap.ACL;
60 import org.apache.geronimo.javamail.store.imap.Rights;
61
62 import org.apache.geronimo.javamail.util.CommandFailedException;
63 import org.apache.geronimo.javamail.util.InvalidCommandException;
64 import org.apache.geronimo.javamail.util.MailConnection;
65 import org.apache.geronimo.javamail.util.ProtocolProperties;
66 import org.apache.geronimo.javamail.util.TraceInputStream;
67 import org.apache.geronimo.javamail.util.TraceOutputStream;
68 import org.apache.geronimo.mail.util.Base64;
69
70 /**
71 * Simple implementation of IMAP transport. Just does plain RFC977-ish
72 * delivery.
73 * <p/>
74 * There is no way to indicate failure for a given recipient (it's possible to have a
75 * recipient address rejected). The sun impl throws exceptions even if others successful),
76 * but maybe we do a different way...
77 * <p/>
78 *
79 * @version $Rev: 739377 $ $Date: 2009-01-30 13:57:39 -0500 (Fri, 30 Jan 2009) $
80 */
81 public class IMAPConnection extends MailConnection {
82
83 protected static final String CAPABILITY_LOGIN_DISABLED = "LOGINDISABLED";
84
85 // The connection pool we're a member of. This keeps holds most of the
86 // connnection parameter information for us.
87 protected IMAPConnectionPool pool;
88
89 // special input stream for reading individual response lines.
90 protected IMAPResponseStream reader;
91
92 // connection pool connections.
93 protected long lastAccess = 0;
94 // our handlers for any untagged responses
95 protected LinkedList responseHandlers = new LinkedList();
96 // the list of queued untagged responses.
97 protected List queuedResponses = new LinkedList();
98 // this is set on if we had a forced disconnect situation from
99 // the server.
100 protected boolean closed = false;
101
102 /**
103 * Normal constructor for an IMAPConnection() object.
104 *
105 * @param props The protocol properties abstraction containing our
106 * property modifiers.
107 * @param pool
108 */
109 public IMAPConnection(ProtocolProperties props, IMAPConnectionPool pool) {
110 super(props);
111 this.pool = pool;
112 }
113
114
115 /**
116 * Connect to the server and do the initial handshaking.
117 *
118 * @exception MessagingException
119 */
120 public boolean protocolConnect(String host, int port, String authid, String realm, String username, String password) throws MessagingException {
121 this.serverHost = host;
122 this.serverPort = port;
123 this.realm = realm;
124 this.authid = authid;
125 this.username = username;
126 this.password = password;
127
128 boolean preAuthorized = false;
129
130 try {
131 // create socket and connect to server.
132 getConnection();
133
134 // we need to ask the server what its capabilities are. This can be done
135 // before we login.
136 getCapability();
137 // do a preauthoriziation check.
138 if (extractResponse("PREAUTH") != null) {
139 preAuthorized = true;
140 }
141
142 // make sure we process these now
143 processPendingResponses();
144
145 // if we're not already using an SSL connection, and we have permission to issue STARTTLS, AND
146 // the server supports this, then switch to TLS mode before continuing.
147 if (!sslConnection && props.getBooleanProperty(MAIL_STARTTLS_ENABLE, false) && hasCapability(CAPABILITY_STARTTLS)) {
148 // if the server supports TLS, then use it for the connection.
149 // on our connection.
150
151 // tell the server of our intention to start a TLS session
152 sendSimpleCommand("STARTTLS");
153
154 // The connection is then handled by the superclass level.
155 getConnectedTLSSocket();
156
157 // create the special reader for pulling the responses.
158 reader = new IMAPResponseStream(inputStream);
159
160 // the IMAP spec states that the capability response is independent of login state or
161 // user, but I'm not sure I believe that to be the case. It doesn't hurt to refresh
162 // the information again after establishing a secure connection.
163 getCapability();
164 // and we need to repeat this check.
165 if (extractResponse("PREAUTH") != null) {
166 preAuthorized = true;
167 }
168 }
169
170 // damn, no login required.
171 if (preAuthorized) {
172 return true;
173 }
174
175 // go login with the server
176 return login();
177 } catch (IOException e) {
178 if (debug) {
179 debugOut("I/O exception establishing connection", e);
180 }
181 throw new MessagingException("Connection error", e);
182 }
183 finally {
184 // make sure the queue is cleared
185 processPendingResponses();
186 }
187 }
188
189 /**
190 * Update the last access time for the connection.
191 */
192 protected void updateLastAccess() {
193 lastAccess = System.currentTimeMillis();
194 }
195
196 /**
197 * Test if the connection has been sitting idle for longer than
198 * the set timeout period.
199 *
200 * @param timeout The allowed "freshness" interval.
201 *
202 * @return True if the connection has been active within the required
203 * interval, false if it has been sitting idle for too long.
204 */
205 public boolean isStale(long timeout) {
206 return (System.currentTimeMillis() - lastAccess) > timeout;
207 }
208
209
210 /**
211 * Close the connection. On completion, we'll be disconnected from
212 * the server and unable to send more data.
213 *
214 * @exception MessagingException
215 */
216 public void close() throws MessagingException {
217 // if we're already closed, get outta here.
218 if (socket == null) {
219 return;
220 }
221 try {
222 // say goodbye
223 logout();
224 } finally {
225 // and close up the connection. We do this in a finally block to make sure the connection
226 // is shut down even if quit gets an error.
227 closeServerConnection();
228 // get rid of our response processor too.
229 reader = null;
230 }
231 }
232
233
234 /**
235 * Create a transport connection object and connect it to the
236 * target server.
237 *
238 * @exception MessagingException
239 */
240 protected void getConnection() throws IOException, MessagingException
241 {
242 // do all of the non-protocol specific set up. This will get our socket established
243 // and ready use.
244 super.getConnection();
245 // create the special reader for pulling the responses.
246 reader = new IMAPResponseStream(inputStream);
247
248 // set the initial access time stamp
249 updateLastAccess();
250 }
251
252
253 /**
254 * Process a simple command/response sequence between the
255 * client and the server. These are commands where the
256 * client is expecting them to "just work", and also will not
257 * directly process the reply information. Unsolicited untagged
258 * responses are dispatched to handlers, and a MessagingException
259 * will be thrown for any non-OK responses from the server.
260 *
261 * @param data The command data we're writing out.
262 *
263 * @exception MessagingException
264 */
265 public void sendSimpleCommand(String data) throws MessagingException {
266 // create a command object and issue the command with that.
267 IMAPCommand command = new IMAPCommand(data);
268 sendSimpleCommand(command);
269 }
270
271
272 /**
273 * Process a simple command/response sequence between the
274 * client and the server. These are commands where the
275 * client is expecting them to "just work", and also will not
276 * directly process the reply information. Unsolicited untagged
277 * responses are dispatched to handlers, and a MessagingException
278 * will be thrown for any non-OK responses from the server.
279 *
280 * @param data The command data we're writing out.
281 *
282 * @exception MessagingException
283 */
284 public void sendSimpleCommand(IMAPCommand data) throws MessagingException {
285 // the command sending process will raise exceptions for bad responses....
286 // we just need to send the command and forget about it.
287 sendCommand(data);
288 }
289
290
291 /**
292 * Sends a command down the socket, returning the server response.
293 *
294 * @param data The String form of the command.
295 *
296 * @return The tagged response information that terminates the command interaction.
297 * @exception MessagingException
298 */
299 public IMAPTaggedResponse sendCommand(String data) throws MessagingException {
300 IMAPCommand command = new IMAPCommand(data);
301 return sendCommand(command);
302 }
303
304
305 /**
306 * Sends a command down the socket, returning the server response.
307 *
308 * @param data An IMAPCommand object with the prepared command information.
309 *
310 * @return The tagged (or continuation) response information that terminates the
311 * command response sequence.
312 * @exception MessagingException
313 */
314 public synchronized IMAPTaggedResponse sendCommand(IMAPCommand data) throws MessagingException {
315 // check first
316 checkConnected();
317 try {
318 // have the command write the command data. This also prepends a tag.
319 data.writeTo(outputStream, this);
320 outputStream.flush();
321 // update the activity timestamp
322 updateLastAccess();
323 // get the received response
324 return receiveResponse();
325 } catch (IOException e) {
326 throw new MessagingException(e.toString(), e);
327 }
328 }
329
330
331 /**
332 * Sends a message down the socket and terminates with the
333 * appropriate CRLF
334 *
335 * @param data The string data to send.
336 *
337 * @return An IMAPTaggedResponse item returned from the server.
338 * @exception MessagingException
339 */
340 public IMAPTaggedResponse sendLine(String data) throws MessagingException {
341 return sendLine(data.getBytes());
342 }
343
344
345 /**
346 * Sends a message down the socket and terminates with the
347 * appropriate CRLF
348 *
349 * @param data The array of data to send to the server.
350 *
351 * @return The response item returned from the IMAP server.
352 * @exception MessagingException
353 */
354 public IMAPTaggedResponse sendLine(byte[] data) throws MessagingException {
355 return sendLine(data, 0, data.length);
356 }
357
358
359 /**
360 * Sends a message down the socket and terminates with the
361 * appropriate CRLF
362 *
363 * @param data The source data array.
364 * @param offset The offset within the data array.
365 * @param length The length of data to send.
366 *
367 * @return The response line returned from the IMAP server.
368 * @exception MessagingException
369 */
370 public synchronized IMAPTaggedResponse sendLine(byte[] data, int offset, int length) throws MessagingException {
371 // check first
372 checkConnected();
373
374 try {
375 outputStream.write(data, offset, length);
376 outputStream.write(CR);
377 outputStream.write(LF);
378 outputStream.flush();
379 // update the activity timestamp
380 updateLastAccess();
381 return receiveResponse();
382 } catch (IOException e) {
383 throw new MessagingException(e.toString(), e);
384 }
385 }
386
387
388 /**
389 * Get a reply line for an IMAP command.
390 *
391 * @return An IMAP reply object from the stream.
392 */
393 public IMAPTaggedResponse receiveResponse() throws MessagingException {
394 while (true) {
395 // read and parse a response from the server.
396 IMAPResponse response = reader.readResponse();
397 // The response set is terminated by either a continuation response or a
398 // tagged response (we only have a single command active at one time).
399 if (response instanceof IMAPTaggedResponse) {
400 // update the access time stamp for later timeout processing.
401 updateLastAccess();
402 IMAPTaggedResponse tagged = (IMAPTaggedResponse)response;
403 // we turn these into exceptions here, which means the issuer doesn't have to
404 // worry about checking status.
405 if (tagged.isBAD()) {
406 throw new InvalidCommandException("Unexpected command IMAP command error");
407 }
408 else if (tagged.isNO()) {
409 throw new CommandFailedException("Unexpected error executing IMAP command");
410 }
411 return tagged;
412 }
413 else {
414 // all other unsolicited responses are either async status updates or
415 // additional elements of a command we just sent. These will be processed
416 // either during processing of the command response, or at the end of the
417 // current command processing.
418 queuePendingResponse((IMAPUntaggedResponse)response);
419 }
420 }
421 }
422
423
424 /**
425 * Get the servers capabilities from the wire....
426 */
427 public void getCapability() throws MessagingException {
428 sendCommand("CAPABILITY");
429 // get the capabilities from the response.
430 IMAPCapabilityResponse response = (IMAPCapabilityResponse)extractResponse("CAPABILITY");
431 capabilities = response.getCapabilities();
432 authentications = response.getAuthentications();
433 }
434
435 /**
436 * Logs out from the server.
437 */
438 public void logout() throws MessagingException {
439 // We can just send the command and generally ignore the
440 // status response.
441 sendCommand("LOGOUT");
442 }
443
444 /**
445 * Deselect a mailbox when a folder returns a connection.
446 *
447 * @exception MessagingException
448 */
449 public void closeMailbox() throws MessagingException {
450 // We can just send the command and generally ignore the
451 // status response.
452 sendCommand("CLOSE");
453 }
454
455
456 /**
457 * Authenticate with the server, if necessary (or possible).
458 *
459 * @return true if we were able to authenticate correctly, false for authentication failures.
460 * @exception MessagingException
461 */
462 protected boolean login() throws MessagingException
463 {
464 // if no username or password, fail this immediately.
465 // the base connect property should resolve a username/password combo for us and
466 // try again.
467 if (username == null || password == null) {
468 return false;
469 }
470
471 // are we permitted to use SASL mechanisms?
472 if (props.getBooleanProperty(MAIL_SASL_ENABLE, false)) {
473 // we might be enable for SASL, but the client and the server might
474 // not have any supported mechanisms in common. Try again with another
475 // mechanism.
476 if (processSaslAuthentication()) {
477 return true;
478 }
479 }
480
481 // see if we're allowed to try plain.
482 if (!props.getBooleanProperty(MAIL_PLAIN_DISABLE, false) && supportsMechanism(AUTHENTICATION_PLAIN)) {
483 return processPlainAuthentication();
484 }
485
486 // see if we're allowed to try login.
487 if (!props.getBooleanProperty(MAIL_LOGIN_DISABLE, false) && supportsMechanism(AUTHENTICATION_LOGIN)) {
488 // no authzid capability with this authentication method.
489 return processLoginAuthentication();
490 }
491
492 // the server can choose to disable the LOGIN command. If not disabled, try
493 // using LOGIN rather than AUTHENTICATE.
494 if (!hasCapability(CAPABILITY_LOGIN_DISABLED)) {
495 return processLogin();
496 }
497
498 throw new MessagingException("No supported LOGIN methods enabled");
499 }
500
501
502 /**
503 * Process SASL-type authentication.
504 *
505 * @return Returns true if the server support a SASL authentication mechanism and
506 * accepted reponse challenges.
507 * @exception MessagingException
508 */
509 protected boolean processSaslAuthentication() throws MessagingException {
510 // if unable to get an appropriate authenticator, just fail it.
511 ClientAuthenticator authenticator = getSaslAuthenticator();
512 if (authenticator == null) {
513 return false;
514 }
515
516 // go process the login.
517 return processLogin(authenticator);
518 }
519
520 protected ClientAuthenticator getSaslAuthenticator() {
521 return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
522 }
523
524 /**
525 * Process SASL-type PLAIN authentication.
526 *
527 * @return Returns true if the login is accepted.
528 * @exception MessagingException
529 */
530 protected boolean processPlainAuthentication() throws MessagingException {
531 // go process the login.
532 return processLogin(new PlainAuthenticator(username, password));
533 }
534
535
536 /**
537 * Process SASL-type LOGIN authentication.
538 *
539 * @return Returns true if the login is accepted.
540 * @exception MessagingException
541 */
542 protected boolean processLoginAuthentication() throws MessagingException {
543 // go process the login.
544 return processLogin(new LoginAuthenticator(username, password));
545 }
546
547
548 /**
549 * Process a LOGIN using the LOGIN command instead of AUTHENTICATE.
550 *
551 * @return true if the command succeeded, false for any authentication failures.
552 * @exception MessagingException
553 */
554 protected boolean processLogin() throws MessagingException {
555 // arguments are "LOGIN userid password"
556 IMAPCommand command = new IMAPCommand("LOGIN");
557 command.appendAtom(username);
558 command.appendAtom(password);
559
560 // go issue the command
561 try {
562 sendCommand(command);
563 } catch (CommandFailedException e) {
564 // we'll get a NO response for a rejected login
565 return false;
566 }
567 // seemed to work ok....
568 return true;
569 }
570
571
572 /**
573 * Process a login using the provided authenticator object.
574 *
575 * NB: This method is synchronized because we have a multi-step process going on
576 * here. No other commands should be sent to the server until we complete.
577 *
578 * @return Returns true if the server support a SASL authentication mechanism and
579 * accepted reponse challenges.
580 * @exception MessagingException
581 */
582 protected synchronized boolean processLogin(ClientAuthenticator authenticator) throws MessagingException {
583 if (debug) {
584 debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
585 }
586
587 IMAPCommand command = new IMAPCommand("AUTHENTICATE");
588 // and tell the server which mechanism we're using.
589 command.appendAtom(authenticator.getMechanismName());
590 // send the command now
591
592 try {
593 IMAPTaggedResponse response = sendCommand(command);
594
595 // now process the challenge sequence. We get a 235 response back when the server accepts the
596 // authentication, and a 334 indicates we have an additional challenge.
597 while (true) {
598 // this should be a continuation reply, if things are still good.
599 if (response.isContinuation()) {
600 // we're passed back a challenge value, Base64 encoded.
601 byte[] challenge = response.decodeChallengeResponse();
602
603 // have the authenticator evaluate and send back the encoded response.
604 response = sendLine(Base64.encode(authenticator.evaluateChallenge(challenge)));
605 }
606 else {
607 // there are only two choices here, OK or a continuation. OK means
608 // we've passed muster and are in.
609 return true;
610 }
611 }
612 } catch (CommandFailedException e ) {
613 // a failure at any point in this process will result in a "NO" response.
614 // That causes an exception to get thrown, so just fail the login
615 // if we get one.
616 return false;
617 }
618 }
619
620
621 /**
622 * Return the server host for this connection.
623 *
624 * @return The String name of the server host.
625 */
626 public String getHost() {
627 return serverHost;
628 }
629
630
631 /**
632 * Attach a handler for untagged responses to this connection.
633 *
634 * @param h The new untagged response handler.
635 */
636 public synchronized void addResponseHandler(IMAPUntaggedResponseHandler h) {
637 responseHandlers.add(h);
638 }
639
640
641 /**
642 * Remove a response handler from the connection.
643 *
644 * @param h The handler to remove.
645 */
646 public synchronized void removeResponseHandler(IMAPUntaggedResponseHandler h) {
647 responseHandlers.remove(h);
648 }
649
650
651 /**
652 * Add a response to the pending untagged response queue.
653 *
654 * @param response The response to add.
655 */
656 public synchronized void queuePendingResponse(IMAPUntaggedResponse response) {
657 queuedResponses.add(response);
658 }
659
660 /**
661 * Process any untagged responses in the queue. This will clear out
662 * the queue, and send each response to the registered
663 * untagged response handlers.
664 */
665 public void processPendingResponses() throws MessagingException {
666 List pendingResponses = null;
667 List handlerList = null;
668
669 synchronized(this) {
670 if (queuedResponses.isEmpty()) {
671 return;
672 }
673 pendingResponses = queuedResponses;
674 queuedResponses = new LinkedList();
675 // get a copy of the response handlers so we can
676 // release the connection lock before broadcasting
677 handlerList = (List)responseHandlers.clone();
678 }
679
680 for (int i = 0; i < pendingResponses.size(); i++) {
681 IMAPUntaggedResponse response = (IMAPUntaggedResponse)pendingResponses.get(i);
682 for (int j = 0; j < handlerList.size(); j++) {
683 // broadcast to each handler. If a handler returns true, then it
684 // handled whatever this message required and we should skip sending
685 // it to other handlers.
686 IMAPUntaggedResponseHandler h = (IMAPUntaggedResponseHandler)handlerList.get(j);
687 if (h.handleResponse(response)) {
688 break;
689 }
690 }
691 }
692 }
693
694 /**
695 * Extract a single response from the pending queue that
696 * match a give keyword type. All matching responses
697 * are removed from the pending queue.
698 *
699 * @param type The string name of the keyword.
700 *
701 * @return A List of all matching queued responses.
702 */
703 public IMAPUntaggedResponse extractResponse(String type) {
704 Iterator i = queuedResponses.iterator();
705 while (i.hasNext()) {
706 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
707 // if this is of the target type, move it to the response set.
708 if (response.isKeyword(type)) {
709 i.remove();
710 return response;
711 }
712 }
713 return null;
714 }
715
716 /**
717 * Extract all responses from the pending queue that
718 * match a give keyword type. All matching responses
719 * are removed from the pending queue.
720 *
721 * @param type The string name of the keyword.
722 *
723 * @return A List of all matching queued responses.
724 */
725 public List extractResponses(String type) {
726 List responses = new ArrayList();
727
728 Iterator i = queuedResponses.iterator();
729 while (i.hasNext()) {
730 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
731 // if this is of the target type, move it to the response set.
732 if (response.isKeyword(type)) {
733 i.remove();
734 responses.add(response);
735 }
736 }
737 return responses;
738 }
739
740
741 /**
742 * Extract all responses from the pending queue that
743 * are "FETCH" responses for a given message number. All matching responses
744 * are removed from the pending queue.
745 *
746 * @param type The string name of the keyword.
747 *
748 * @return A List of all matching queued responses.
749 */
750 public List extractFetchResponses(int sequenceNumber) {
751 List responses = new ArrayList();
752
753 Iterator i = queuedResponses.iterator();
754 while (i.hasNext()) {
755 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
756 // if this is of the target type, move it to the response set.
757 if (response.isKeyword("FETCH")) {
758 IMAPFetchResponse fetch = (IMAPFetchResponse)response;
759 // a response for the correct message number?
760 if (fetch.sequenceNumber == sequenceNumber) {
761 // pluck these from the list and add to the response set.
762 i.remove();
763 responses.add(response);
764 }
765 }
766 }
767 return responses;
768 }
769
770 /**
771 * Extract a fetch response data item from the queued elements.
772 *
773 * @param sequenceNumber
774 * The message number we're interested in. Fetch responses for other messages
775 * will be skipped.
776 * @param type The type of body element we need. It is assumed that only one item for
777 * the given message number will exist in the queue. The located item will
778 * be returned, and that fetch response will be removed from the pending queue.
779 *
780 * @return The target data item, or null if a match is not found.
781 */
782 protected IMAPFetchDataItem extractFetchDataItem(long sequenceNumber, int type)
783 {
784 Iterator i = queuedResponses.iterator();
785 while (i.hasNext()) {
786 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
787 // if this is of the target type, move it to the response set.
788 if (response.isKeyword("FETCH")) {
789 IMAPFetchResponse fetch = (IMAPFetchResponse)response;
790 // a response for the correct message number?
791 if (fetch.sequenceNumber == sequenceNumber) {
792 // does this response have the item we're looking for?
793 IMAPFetchDataItem item = fetch.getDataItem(type);
794 if (item != null) {
795 // remove this from the pending queue and return the
796 // located item
797 i.remove();
798 return item;
799 }
800 }
801 }
802 }
803 // not located, sorry
804 return null;
805 }
806
807 /**
808 * Extract a all fetch responses that contain a given data item.
809 *
810 * @param type The type of body element we need. It is assumed that only one item for
811 * the given message number will exist in the queue. The located item will
812 * be returned, and that fetch response will be removed from the pending queue.
813 *
814 * @return A List of all matching Fetch responses.
815 */
816 protected List extractFetchDataItems(int type)
817 {
818 Iterator i = queuedResponses.iterator();
819 List items = new ArrayList();
820
821 while (i.hasNext()) {
822 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
823 // if this is of the target type, move it to the response set.
824 if (response.isKeyword("FETCH")) {
825 IMAPFetchResponse fetch = (IMAPFetchResponse)response;
826 // does this response have the item we're looking for?
827 IMAPFetchDataItem item = fetch.getDataItem(type);
828 if (item != null) {
829 // remove this from the pending queue and return the
830 // located item
831 i.remove();
832 // we want the fetch response, not the data item, because
833 // we're going to require the message sequence number information
834 // too.
835 items.add(fetch);
836 }
837 }
838 }
839 // return whatever we have.
840 return items;
841 }
842
843 /**
844 * Make sure we have the latest status information available. We
845 * retreive this by sending a NOOP command to the server, and
846 * processing any untagged responses we get back.
847 */
848 public void updateMailboxStatus() throws MessagingException {
849 sendSimpleCommand("NOOP");
850 }
851
852
853 /**
854 * check to see if this connection is truely alive.
855 *
856 * @param timeout The timeout value to control how often we ping
857 * the server to see if we're still good.
858 *
859 * @return true if the server is responding to requests, false for any
860 * connection errors. This will also update the folder status
861 * by processing returned unsolicited messages.
862 */
863 public synchronized boolean isAlive(long timeout) {
864 long lastUsed = System.currentTimeMillis() - lastAccess;
865 if (lastUsed < timeout) {
866 return true;
867 }
868
869 try {
870 sendSimpleCommand("NOOP");
871 return true;
872 } catch (MessagingException e) {
873 // the NOOP command will throw a MessagingException if we get anything
874 // other than an OK response back from the server.
875 }
876 return false;
877 }
878
879
880 /**
881 * Issue a fetch command to retrieve the message ENVELOPE structure.
882 *
883 * @param sequenceNumber The sequence number of the message.
884 *
885 * @return The IMAPResponse item containing the ENVELOPE information.
886 */
887 public synchronized List fetchEnvelope(int sequenceNumber) throws MessagingException {
888 IMAPCommand command = new IMAPCommand("FETCH");
889 command.appendInteger(sequenceNumber);
890 command.startList();
891 command.appendAtom("ENVELOPE INTERNALDATE RFC822.SIZE");
892 command.endList();
893
894 // we want all of the envelope information about the message, which involves multiple FETCH chunks.
895 sendCommand(command);
896 // these are fairly involved sets, so the caller needs to handle these.
897 // we just return all of the FETCH results matching the target message number.
898 return extractFetchResponses(sequenceNumber);
899 }
900
901 /**
902 * Issue a FETCH command to retrieve the message BODYSTRUCTURE structure.
903 *
904 * @param sequenceNumber The sequence number of the message.
905 *
906 * @return The IMAPBodyStructure item for the message.
907 * All other untagged responses are queued for processing.
908 */
909 public synchronized IMAPBodyStructure fetchBodyStructure(int sequenceNumber) throws MessagingException {
910 IMAPCommand command = new IMAPCommand("FETCH");
911 command.appendInteger(sequenceNumber);
912 command.startList();
913 command.appendAtom("BODYSTRUCTURE");
914 command.endList();
915
916 // we want all of the envelope information about the message, which involves multiple FETCH chunks.
917 sendCommand(command);
918 // locate the response from this
919 IMAPBodyStructure bodyStructure = (IMAPBodyStructure)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.BODYSTRUCTURE);
920
921 if (bodyStructure == null) {
922 throw new MessagingException("No BODYSTRUCTURE information received from IMAP server");
923 }
924 // and return the body structure directly.
925 return bodyStructure;
926 }
927
928
929 /**
930 * Issue a FETCH command to retrieve the message RFC822.HEADERS structure containing the message headers (using PEEK).
931 *
932 * @param sequenceNumber The sequence number of the message.
933 *
934 * @return The IMAPRFC822Headers item for the message.
935 * All other untagged responses are queued for processing.
936 */
937 public synchronized InternetHeaders fetchHeaders(int sequenceNumber, String part) throws MessagingException {
938 IMAPCommand command = new IMAPCommand("FETCH");
939 command.appendInteger(sequenceNumber);
940 command.startList();
941 command.appendAtom("BODY.PEEK");
942 command.appendBodySection(part, "HEADER");
943 command.endList();
944
945 // we want all of the envelope information about the message, which involves multiple FETCH chunks.
946 sendCommand(command);
947 IMAPInternetHeader header = (IMAPInternetHeader)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.HEADER);
948
949 if (header == null) {
950 throw new MessagingException("No HEADER information received from IMAP server");
951 }
952 // and return the body structure directly.
953 return header.headers;
954 }
955
956
957 /**
958 * Issue a FETCH command to retrieve the message text
959 *
960 * @param sequenceNumber The sequence number of the message.
961 *
962 * @return The IMAPMessageText item for the message.
963 * All other untagged responses are queued for processing.
964 */
965 public synchronized IMAPMessageText fetchText(int sequenceNumber) throws MessagingException {
966 IMAPCommand command = new IMAPCommand("FETCH");
967 command.appendInteger(sequenceNumber);
968 command.startList();
969 command.appendAtom("BODY.PEEK");
970 command.appendBodySection("TEXT");
971 command.endList();
972
973 // we want all of the envelope information about the message, which involves multiple FETCH chunks.
974 sendCommand(command);
975 IMAPMessageText text = (IMAPMessageText)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.TEXT);
976
977 if (text == null) {
978 throw new MessagingException("No TEXT information received from IMAP server");
979 }
980 // and return the body structure directly.
981 return text;
982 }
983
984
985 /**
986 * Issue a FETCH command to retrieve the message text
987 *
988 * @param sequenceNumber The sequence number of the message.
989 *
990 * @return The IMAPMessageText item for the message.
991 * All other untagged responses are queued for processing.
992 */
993 public synchronized IMAPMessageText fetchBodyPartText(int sequenceNumber, String section) throws MessagingException {
994 IMAPCommand command = new IMAPCommand("FETCH");
995 command.appendInteger(sequenceNumber);
996 command.startList();
997 command.appendAtom("BODY.PEEK");
998 command.appendBodySection(section, "TEXT");
999 command.endList();
1000
1001 // we want all of the envelope information about the message, which involves multiple FETCH chunks.
1002 sendCommand(command);
1003 IMAPMessageText text = (IMAPMessageText)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.TEXT);
1004
1005 if (text == null) {
1006 throw new MessagingException("No TEXT information received from IMAP server");
1007 }
1008 // and return the body structure directly.
1009 return text;
1010 }
1011
1012
1013 /**
1014 * Issue a FETCH command to retrieve the entire message body in one shot.
1015 * This may also be used to fetch an embedded message part as a unit.
1016 *
1017 * @param sequenceNumber
1018 * The sequence number of the message.
1019 * @param section The section number to fetch. If null, the entire body of the message
1020 * is retrieved.
1021 *
1022 * @return The IMAPBody item for the message.
1023 * All other untagged responses are queued for processing.
1024 * @exception MessagingException
1025 */
1026 public synchronized IMAPBody fetchBody(int sequenceNumber, String section) throws MessagingException {
1027 IMAPCommand command = new IMAPCommand("FETCH");
1028 command.appendInteger(sequenceNumber);
1029 command.startList();
1030 command.appendAtom("BODY.PEEK");
1031 // no part name here, only the section identifier. This will fetch
1032 // the entire body, with all of the bits in place.
1033 command.appendBodySection(section, null);
1034 command.endList();
1035
1036 // we want all of the envelope information about the message, which involves multiple FETCH chunks.
1037 sendCommand(command);
1038 IMAPBody body = (IMAPBody)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.BODY);
1039
1040 if (body == null) {
1041 throw new MessagingException("No BODY information received from IMAP server");
1042 }
1043 // and return the body structure directly.
1044 return body;
1045 }
1046
1047
1048 /**
1049 * Fetch the message content. This sorts out which method should be used
1050 * based on the server capability.
1051 *
1052 * @param sequenceNumber
1053 * The sequence number of the target message.
1054 *
1055 * @return The byte[] content information.
1056 * @exception MessagingException
1057 */
1058 public byte[] fetchContent(int sequenceNumber) throws MessagingException {
1059 // fetch the text item and return the data
1060 IMAPMessageText text = fetchText(sequenceNumber);
1061 return text.getContent();
1062 }
1063
1064
1065 /**
1066 * Fetch the message content. This sorts out which method should be used
1067 * based on the server capability.
1068 *
1069 * @param sequenceNumber
1070 * The sequence number of the target message.
1071 *
1072 * @return The byte[] content information.
1073 * @exception MessagingException
1074 */
1075 public byte[] fetchContent(int sequenceNumber, String section) throws MessagingException {
1076 if (section == null) {
1077 IMAPMessageText text = fetchText(sequenceNumber);
1078 return text.getContent();
1079 } else {
1080 IMAPBody body = fetchBody(sequenceNumber, section);
1081 return body.getContent();
1082 }
1083 }
1084
1085
1086 /**
1087 * Send an LIST command to the IMAP server, returning all LIST
1088 * response information.
1089 *
1090 * @param mailbox The reference mailbox name sent on the command.
1091 * @param pattern The match pattern used on the name.
1092 *
1093 * @return A List of all LIST response information sent back from the server.
1094 */
1095 public synchronized List list(String mailbox, String pattern) throws MessagingException {
1096 IMAPCommand command = new IMAPCommand("LIST");
1097
1098 // construct the command, encoding the tokens as required by the content.
1099 command.appendEncodedString(mailbox);
1100 command.appendEncodedString(pattern);
1101
1102 sendCommand(command);
1103
1104 // pull out the ones we're interested in
1105 return extractResponses("LIST");
1106 }
1107
1108
1109 /**
1110 * Send an LSUB command to the IMAP server, returning all LSUB
1111 * response information.
1112 *
1113 * @param mailbox The reference mailbox name sent on the command.
1114 * @param pattern The match pattern used on the name.
1115 *
1116 * @return A List of all LSUB response information sent back from the server.
1117 */
1118 public List listSubscribed(String mailbox, String pattern) throws MessagingException {
1119 IMAPCommand command = new IMAPCommand("LSUB");
1120
1121 // construct the command, encoding the tokens as required by the content.
1122 command.appendEncodedString(mailbox);
1123 command.appendEncodedString(pattern);
1124
1125 sendCommand(command);
1126 // pull out the ones we're interested in
1127 return extractResponses("LSUB");
1128 }
1129
1130
1131 /**
1132 * Subscribe to a give mailbox.
1133 *
1134 * @param mailbox The desired mailbox name.
1135 *
1136 * @exception MessagingException
1137 */
1138 public void subscribe(String mailbox) throws MessagingException {
1139 IMAPCommand command = new IMAPCommand("SUBSCRIBE");
1140 // add on the encoded mailbox name, as the appropriate token type.
1141 command.appendEncodedString(mailbox);
1142
1143 // send this, and ignore the response.
1144 sendSimpleCommand(command);
1145 }
1146
1147
1148 /**
1149 * Unsubscribe from a mailbox.
1150 *
1151 * @param mailbox The mailbox to remove.
1152 *
1153 * @exception MessagingException
1154 */
1155 public void unsubscribe(String mailbox) throws MessagingException {
1156 IMAPCommand command = new IMAPCommand("UNSUBSCRIBE");
1157 // add on the encoded mailbox name, as the appropriate token type.
1158 command.appendEncodedString(mailbox);
1159
1160 // send this, and ignore the response.
1161 sendSimpleCommand(command);
1162 }
1163
1164
1165 /**
1166 * Create a mailbox.
1167 *
1168 * @param mailbox The desired new mailbox name (fully qualified);
1169 *
1170 * @exception MessagingException
1171 */
1172 public void createMailbox(String mailbox) throws MessagingException {
1173 IMAPCommand command = new IMAPCommand("CREATE");
1174 // add on the encoded mailbox name, as the appropriate token type.
1175 command.appendEncodedString(mailbox);
1176
1177 // send this, and ignore the response.
1178 sendSimpleCommand(command);
1179 }
1180
1181
1182 /**
1183 * Delete a mailbox.
1184 *
1185 * @param mailbox The target mailbox name (fully qualified);
1186 *
1187 * @exception MessagingException
1188 */
1189 public void deleteMailbox(String mailbox) throws MessagingException {
1190 IMAPCommand command = new IMAPCommand("DELETE");
1191 // add on the encoded mailbox name, as the appropriate token type.
1192 command.appendEncodedString(mailbox);
1193
1194 // send this, and ignore the response.
1195 sendSimpleCommand(command);
1196 }
1197
1198
1199 /**
1200 * Rename a mailbox.
1201 *
1202 * @param mailbox The target mailbox name (fully qualified);
1203 *
1204 * @exception MessagingException
1205 */
1206 public void renameMailbox(String oldName, String newName) throws MessagingException {
1207 IMAPCommand command = new IMAPCommand("RENAME");
1208 // add on the encoded mailbox name, as the appropriate token type.
1209 command.appendEncodedString(oldName);
1210 command.appendEncodedString(newName);
1211
1212 // send this, and ignore the response.
1213 sendSimpleCommand(command);
1214 }
1215
1216
1217 /**
1218 * Retrieve a complete set of status items for a mailbox.
1219 *
1220 * @param mailbox The mailbox name.
1221 *
1222 * @return An IMAPMailboxStatus item filled in with the STATUS responses.
1223 * @exception MessagingException
1224 */
1225 public synchronized IMAPMailboxStatus getMailboxStatus(String mailbox) throws MessagingException {
1226 IMAPCommand command = new IMAPCommand("STATUS");
1227
1228 // construct the command, encoding the tokens as required by the content.
1229 command.appendEncodedString(mailbox);
1230 // request all of the status items
1231 command.append(" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)");
1232
1233 sendCommand(command);
1234
1235 // now harvest each of the respon
1236 IMAPMailboxStatus status = new IMAPMailboxStatus();
1237 status.mergeSizeResponses(extractResponses("EXISTS"));
1238 status.mergeSizeResponses(extractResponses("RECENT"));
1239 status.mergeOkResponses(extractResponses("UIDNEXT"));
1240 status.mergeOkResponses(extractResponses("UIDVALIDITY"));
1241 status.mergeOkResponses(extractResponses("UNSEEN"));
1242 status.mergeStatus((IMAPStatusResponse)extractResponse("STATUS"));
1243 status.mergeStatus((IMAPPermanentFlagsResponse)extractResponse("PERMANENTFLAGS"));
1244
1245 return status;
1246 }
1247
1248
1249 /**
1250 * Select a mailbox, returning the accumulated status information
1251 * about the mailbox returned with the response.
1252 *
1253 * @param mailbox The desired mailbox name.
1254 * @param readOnly The open mode. If readOnly is true, the mailbox is opened
1255 * using EXAMINE rather than SELECT.
1256 *
1257 * @return A status object containing the mailbox particulars.
1258 * @exception MessagingException
1259 */
1260 public synchronized IMAPMailboxStatus openMailbox(String mailbox, boolean readOnly) throws MessagingException {
1261 IMAPCommand command = new IMAPCommand();
1262
1263 // if readOnly is required, we use EXAMINE to switch to the mailbox rather than SELECT.
1264 // This returns the same response information, but the mailbox will not accept update operations.
1265 if (readOnly) {
1266 command.appendAtom("EXAMINE");
1267 }
1268 else {
1269 command.appendAtom("SELECT");
1270 }
1271
1272 // construct the command, encoding the tokens as required by the content.
1273 command.appendEncodedString(mailbox);
1274
1275 // issue the select
1276 IMAPTaggedResponse response = sendCommand(command);
1277
1278 IMAPMailboxStatus status = new IMAPMailboxStatus();
1279 // set the mode to the requested open mode.
1280 status.mode = readOnly ? Folder.READ_ONLY : Folder.READ_WRITE;
1281
1282 // the server might disagree on the mode, so check to see if
1283 // it's telling us READ-ONLY.
1284 if (response.hasStatus("READ-ONLY")) {
1285 status.mode = Folder.READ_ONLY;
1286 }
1287
1288 // some of these are required, some are optional.
1289 status.mergeFlags((IMAPFlagsResponse)extractResponse("FLAGS"));
1290 status.mergeStatus((IMAPSizeResponse)extractResponse("EXISTS"));
1291 status.mergeStatus((IMAPSizeResponse)extractResponse("RECENT"));
1292 status.mergeStatus((IMAPOkResponse)extractResponse("UIDVALIDITY"));
1293 status.mergeStatus((IMAPOkResponse)extractResponse("UNSEEN"));
1294 status.mergeStatus((IMAPPermanentFlagsResponse)extractResponse("PERMANENTFLAGS"));
1295 // mine the response for status information about the selected mailbox.
1296 return status;
1297 }
1298
1299
1300 /**
1301 * Tells the IMAP server to expunge messages marked for deletion.
1302 * The server will send us an untagged EXPUNGE message back for
1303 * each deleted message. For explicit expunges we request, we'll
1304 * grabbed the untagged responses here, rather than force them to
1305 * be handled as pending responses. The caller will handle the
1306 * updates directly.
1307 *
1308 * @exception MessagingException
1309 */
1310 public synchronized List expungeMailbox() throws MessagingException {
1311 // send the message, and make sure we got an OK response
1312 sendCommand("EXPUNGE");
1313 // extract all of the expunged responses and return.
1314 return extractResponses("EXPUNGED");
1315 }
1316
1317 public int[] searchMailbox(SearchTerm term) throws MessagingException {
1318 return searchMailbox("ALL", term);
1319 }
1320
1321 /**
1322 * Send a search to the IMAP server using the specified
1323 * messages selector and search term. This figures out what
1324 * to do with CHARSET on the SEARCH command.
1325 *
1326 * @param messages The list of messages (comma-separated numbers or "ALL").
1327 * @param term The desired search criteria
1328 *
1329 * @return Returns an int[] array of message numbers for all matched messages.
1330 * @exception MessagingException
1331 */
1332 public int[] searchMailbox(String messages, SearchTerm term) throws MessagingException {
1333 // don't use a charset by default, but we need to look at the data to see if we have a problem.
1334 String charset = null;
1335
1336 if (IMAPCommand.checkSearchEncoding(term)) {
1337 // not sure exactly how to decide what to use here. Two immediate possibilities come to mind,
1338 // UTF-8 or the MimeUtility.getDefaultJavaCharset() value. Running a small test against the
1339 // Sun impl shows them sending a CHARSET value of UTF-8, so that sounds like the winner. I don't
1340 // believe there's anything in the CAPABILITY response that would tell us what to use.
1341 charset = "UTF-8";
1342 }
1343
1344 return searchMailbox(messages, term, charset);
1345 }
1346
1347 /**
1348 * Send a search to the IMAP server using the specified
1349 * messages selector and search term.
1350 *
1351 * @param messages The list of messages (comma-separated numbers or "ALL").
1352 * @param charset The charset specifier to send to the server. If null, then
1353 * the CHARSET keyword is omitted.
1354 * @param term The desired search criteria
1355 *
1356 * @return Returns an int[] array of message numbers for all matched messages.
1357 * @exception MessagingException
1358 */
1359 public synchronized int[] searchMailbox(String messages, SearchTerm term, String charset) throws MessagingException {
1360 IMAPCommand command = new IMAPCommand("SEARCH");
1361
1362 // if we have an explicit charset to use, append that.
1363 if (charset != null) {
1364 command.appendAtom("CHARSET");
1365 command.appendAtom(charset);
1366 }
1367
1368 // now go through the process of translating the javamail SearchTerm objects into
1369 // the IMAP command sequence. The SearchTerm sequence may be a complex tree of comparison terms,
1370 // so this is not a simple process.
1371 command.appendSearchTerm(term, charset);
1372 // need to append the message set
1373 command.appendAtom(messages);
1374
1375 // now issue the composed command.
1376 sendCommand(command);
1377
1378 // get the list of search responses
1379 IMAPSearchResponse hits = (IMAPSearchResponse)extractResponse("SEARCH");
1380 // and return the message hits
1381 return hits.messageNumbers;
1382 }
1383
1384
1385 /**
1386 * Append a message to a mailbox, given the direct message data.
1387 *
1388 * @param mailbox The target mailbox name.
1389 * @param messageFlags
1390 * The initial flag set for the appended message.
1391 * @param messageDate
1392 * The received date the message is created with,
1393 * @param messageData
1394 * The RFC822 Message data stored on the server.
1395 *
1396 * @exception MessagingException
1397 */
1398 public void appendMessage(String mailbox, Date messageDate, Flags messageFlags, byte[] messageData) throws MessagingException {
1399 IMAPCommand command = new IMAPCommand("APPEND");
1400
1401 // the mailbox is encoded.
1402 command.appendEncodedString(mailbox);
1403
1404 if (messageFlags != null) {
1405 // the flags are pulled from an existing object. We can set most flag values, but the servers
1406 // reserve RECENT for themselves. We need to force that one off.
1407 messageFlags.remove(Flags.Flag.RECENT);
1408 // and add the flag list to the commmand.
1409 command.appendFlags(messageFlags);
1410 }
1411
1412 if (messageDate != null) {
1413 command.appendDate(messageDate);
1414 }
1415
1416 // this gets appended as a literal.
1417 command.appendLiteral(messageData);
1418 // just send this as a simple command...we don't deal with the response other than to verifiy
1419 // it was ok.
1420 sendSimpleCommand(command);
1421 }
1422
1423 /**
1424 * Fetch the flag set for a given message sequence number.
1425 *
1426 * @param sequenceNumber
1427 * The message sequence number.
1428 *
1429 * @return The Flags defined for this message.
1430 * @exception MessagingException
1431 */
1432 public synchronized Flags fetchFlags(int sequenceNumber) throws MessagingException {
1433 // we want just the flag item here.
1434 sendCommand("FETCH " + String.valueOf(sequenceNumber) + " (FLAGS)");
1435 // get the return data item, and get the flags from within it
1436 IMAPFlags flags = (IMAPFlags)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.FLAGS);
1437 return flags.flags;
1438 }
1439
1440
1441 /**
1442 * Set the flags for a range of messages.
1443 *
1444 * @param messageSet The set of message numbers.
1445 * @param flags The new flag settings.
1446 * @param set true if the flags should be set, false for a clear operation.
1447 *
1448 * @return A list containing all of the responses with the new flag values.
1449 * @exception MessagingException
1450 */
1451 public synchronized List setFlags(String messageSet, Flags flags, boolean set) throws MessagingException {
1452 IMAPCommand command = new IMAPCommand("STORE");
1453 command.appendAtom(messageSet);
1454 // the command varies depending on whether this is a set or clear operation
1455 if (set) {
1456 command.appendAtom("+FLAGS");
1457 }
1458 else {
1459 command.appendAtom("-FLAGS");
1460 }
1461
1462 // append the flag set
1463 command.appendFlags(flags);
1464
1465 // we want just the flag item here.
1466 sendCommand(command);
1467 // we should have a FETCH response for each of the updated messages. Return this
1468 // response, and update the message numbers.
1469 return extractFetchDataItems(IMAPFetchDataItem.FLAGS);
1470 }
1471
1472
1473 /**
1474 * Set the flags for a single message.
1475 *
1476 * @param sequenceNumber
1477 * The sequence number of target message.
1478 * @param flags The new flag settings.
1479 * @param set true if the flags should be set, false for a clear operation.
1480 *
1481 * @exception MessagingException
1482 */
1483 public synchronized Flags setFlags(int sequenceNumber, Flags flags, boolean set) throws MessagingException {
1484 IMAPCommand command = new IMAPCommand("STORE");
1485 command.appendInteger(sequenceNumber);
1486 // the command varies depending on whether this is a set or clear operation
1487 if (set) {
1488 command.appendAtom("+FLAGS");
1489 }
1490 else {
1491 command.appendAtom("-FLAGS");
1492 }
1493
1494 // append the flag set
1495 command.appendFlags(flags);
1496
1497 // we want just the flag item here.
1498 sendCommand(command);
1499 // get the return data item, and get the flags from within it
1500 IMAPFlags flagResponse = (IMAPFlags)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.FLAGS);
1501 return flagResponse.flags;
1502 }
1503
1504
1505 /**
1506 * Copy a range of messages to a target mailbox.
1507 *
1508 * @param messageSet The set of message numbers.
1509 * @param target The target mailbox name.
1510 *
1511 * @exception MessagingException
1512 */
1513 public void copyMessages(String messageSet, String target) throws MessagingException {
1514 IMAPCommand command = new IMAPCommand("COPY");
1515 // the auth command initiates the handshaking.
1516 command.appendAtom(messageSet);
1517 // the mailbox is encoded.
1518 command.appendEncodedString(target);
1519 // just send this as a simple command...we don't deal with the response other than to verifiy
1520 // it was ok.
1521 sendSimpleCommand(command);
1522 }
1523
1524
1525 /**
1526 * Fetch the message number for a give UID.
1527 *
1528 * @param uid The target UID
1529 *
1530 * @return An IMAPUid object containing the mapping information.
1531 */
1532 public synchronized IMAPUid getSequenceNumberForUid(long uid) throws MessagingException {
1533 IMAPCommand command = new IMAPCommand("UID FETCH");
1534 command.appendLong(uid);
1535 command.appendAtom("(UID)");
1536
1537 // this situation is a little strange, so it deserves a little explanation.
1538 // We need the message sequence number for this message from a UID value.
1539 // we're going to send a UID FETCH command, requesting the UID value back.
1540 // That seems strange, but the * nnnn FETCH response for the request will
1541 // be tagged with the message sequence number. THAT'S the information we
1542 // really want, and it will be included in the IMAPUid object.
1543
1544 sendCommand(command);
1545 // ok, now we need to search through these looking for a FETCH response with a UID element.
1546 List responses = extractResponses("FETCH");
1547
1548 // we're looking for a fetch response with a UID data item with the UID information
1549 // inside of it.
1550 for (int i = 0; i < responses.size(); i++) {
1551 IMAPFetchResponse response = (IMAPFetchResponse)responses.get(i);
1552 IMAPUid item = (IMAPUid)response.getDataItem(IMAPFetchDataItem.UID);
1553 // is this the response we're looking for? The information we
1554 // need is the message number returned with the response, which is
1555 // also contained in the UID item.
1556 if (item != null && item.uid == uid) {
1557 return item;
1558 }
1559 // not one meant for us, add it back to the pending queue.
1560 queuePendingResponse(response);
1561 }
1562 // didn't find this one
1563 return null;
1564 }
1565
1566
1567 /**
1568 * Fetch the message numbers for a consequetive range
1569 * of UIDs.
1570 *
1571 * @param start The start of the range.
1572 * @param end The end of the uid range.
1573 *
1574 * @return A list of UID objects containing the mappings.
1575 */
1576 public synchronized List getSequenceNumbersForUids(long start, long end) throws MessagingException {
1577 IMAPCommand command = new IMAPCommand("UID FETCH");
1578 // send the request for the range "start:end" so we can fetch all of the info
1579 // at once.
1580 command.appendLong(start);
1581 command.append(":");
1582 // not the special range marker? Just append the
1583 // number. The LASTUID value needs to be "*" on the command.
1584 if (end != UIDFolder.LASTUID) {
1585 command.appendLong(end);
1586 }
1587 else {
1588 command.append("*");
1589 }
1590 command.appendAtom("(UID)");
1591
1592 // this situation is a little strange, so it deserves a little explanation.
1593 // We need the message sequence number for this message from a UID value.
1594 // we're going to send a UID FETCH command, requesting the UID value back.
1595 // That seems strange, but the * nnnn FETCH response for the request will
1596 // be tagged with the message sequence number. THAT'S the information we
1597 // really want, and it will be included in the IMAPUid object.
1598
1599 sendCommand(command);
1600 // ok, now we need to search through these looking for a FETCH response with a UID element.
1601 List responses = extractResponses("FETCH");
1602
1603 List uids = new ArrayList((int)(end - start + 1));
1604
1605 // we're looking for a fetch response with a UID data item with the UID information
1606 // inside of it.
1607 for (int i = 0; i < responses.size(); i++) {
1608 IMAPFetchResponse response = (IMAPFetchResponse)responses.get(i);
1609 IMAPUid item = (IMAPUid)response.getDataItem(IMAPFetchDataItem.UID);
1610 // is this the response we're looking for? The information we
1611 // need is the message number returned with the response, which is
1612 // also contained in the UID item.
1613 if (item != null) {
1614 uids.add(item);
1615 }
1616 else {
1617 // not one meant for us, add it back to the pending queue.
1618 queuePendingResponse(response);
1619 }
1620 }
1621 // return the list of uids we located.
1622 return uids;
1623 }
1624
1625
1626 /**
1627 * Fetch the UID value for a target message number
1628 *
1629 * @param sequenceNumber
1630 * The target message number.
1631 *
1632 * @return An IMAPUid object containing the mapping information.
1633 */
1634 public synchronized IMAPUid getUidForSequenceNumber(int sequenceNumber) throws MessagingException {
1635 IMAPCommand command = new IMAPCommand("FETCH");
1636 command.appendInteger(sequenceNumber);
1637 command.appendAtom("(UID)");
1638
1639 // similar to the other fetches, but without the strange bit. We're starting
1640 // with the message number in this case.
1641
1642 sendCommand(command);
1643
1644 // ok, now we need to search through these looking for a FETCH response with a UID element.
1645 return (IMAPUid)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.UID);
1646 }
1647
1648
1649 /**
1650 * Retrieve the user name space info from the server.
1651 *
1652 * @return An IMAPNamespace response item with the information. If the server
1653 * doesn't support the namespace extension, an empty one is returned.
1654 */
1655 public synchronized IMAPNamespaceResponse getNamespaces() throws MessagingException {
1656 // if no namespace capability, then return an empty
1657 // response, which will trigger the default behavior.
1658 if (!hasCapability("NAMESPACE")) {
1659 return new IMAPNamespaceResponse();
1660 }
1661 // no arguments on this command, so just send an hope it works.
1662 sendCommand("NAMESPACE");
1663
1664 // this should be here, since it's a required response when the
1665 // command worked. Just extract, and return.
1666 return (IMAPNamespaceResponse)extractResponse("NAMESPACE");
1667 }
1668
1669
1670 /**
1671 * Prefetch message information based on the request profile. We'll return
1672 * all of the fetch information to the requesting Folder, which will sort
1673 * out what goes where.
1674 *
1675 * @param messageSet The set of message numbers we need to fetch.
1676 * @param profile The profile of the required information.
1677 *
1678 * @return All FETCH responses resulting from the command.
1679 * @exception MessagingException
1680 */
1681 public synchronized List fetch(String messageSet, FetchProfile profile) throws MessagingException {
1682 IMAPCommand command = new IMAPCommand("FETCH");
1683 command.appendAtom(messageSet);
1684 // this is the set of items to append
1685 command.appendFetchProfile(profile);
1686
1687 // now send the fetch command, which will likely send back a lot of "FETCH" responses.
1688 // Suck all of those reponses out of the queue and send them back for processing.
1689 sendCommand(command);
1690 // we can have a large number of messages here, so just grab all of the fetches
1691 // we get back, and let the Folder sort out who gets what.
1692 return extractResponses("FETCH");
1693 }
1694
1695
1696 /**
1697 * Set the ACL rights for a mailbox. This replaces
1698 * any existing ACLs defined.
1699 *
1700 * @param mailbox The target mailbox.
1701 * @param acl The new ACL to be used for the mailbox.
1702 *
1703 * @exception MessagingException
1704 */
1705 public synchronized void setACLRights(String mailbox, ACL acl) throws MessagingException {
1706 IMAPCommand command = new IMAPCommand("SETACL");
1707 command.appendEncodedString(mailbox);
1708
1709 command.appendACL(acl);
1710
1711 sendSimpleCommand(command);
1712 }
1713
1714
1715 /**
1716 * Add a set of ACL rights to a mailbox.
1717 *
1718 * @param mailbox The mailbox to alter.
1719 * @param acl The ACL to add.
1720 *
1721 * @exception MessagingException
1722 */
1723 public synchronized void addACLRights(String mailbox, ACL acl) throws MessagingException {
1724 if (!hasCapability("ACL")) {
1725 throw new MethodNotSupportedException("ACL not available from this IMAP server");
1726 }
1727 IMAPCommand command = new IMAPCommand("SETACL");
1728 command.appendEncodedString(mailbox);
1729
1730 command.appendACL(acl, "+");
1731
1732 sendSimpleCommand(command);
1733 }
1734
1735
1736 /**
1737 * Remove an ACL from a given mailbox.
1738 *
1739 * @param mailbox The mailbox to alter.
1740 * @param acl The particular ACL to revoke.
1741 *
1742 * @exception MessagingException
1743 */
1744 public synchronized void removeACLRights(String mailbox, ACL acl) throws MessagingException {
1745 if (!hasCapability("ACL")) {
1746 throw new MethodNotSupportedException("ACL not available from this IMAP server");
1747 }
1748 IMAPCommand command = new IMAPCommand("SETACL");
1749 command.appendEncodedString(mailbox);
1750
1751 command.appendACL(acl, "-");
1752
1753 sendSimpleCommand(command);
1754 }
1755
1756
1757 /**
1758 * Get the ACL rights assigned to a given mailbox.
1759 *
1760 * @param mailbox The target mailbox.
1761 *
1762 * @return The an array of ACL items describing the access
1763 * rights to the mailbox.
1764 * @exception MessagingException
1765 */
1766 public synchronized ACL[] getACLRights(String mailbox) throws MessagingException {
1767 if (!hasCapability("ACL")) {
1768 throw new MethodNotSupportedException("ACL not available from this IMAP server");
1769 }
1770 IMAPCommand command = new IMAPCommand("GETACL");
1771 command.appendEncodedString(mailbox);
1772
1773 // now send the GETACL command, which will return a single ACL untagged response.
1774 sendCommand(command);
1775 // there should be just a single ACL response back from this command.
1776 IMAPACLResponse response = (IMAPACLResponse)extractResponse("ACL");
1777 return response.acls;
1778 }
1779
1780
1781 /**
1782 * Get the current user's ACL rights to a given mailbox.
1783 *
1784 * @param mailbox The target mailbox.
1785 *
1786 * @return The Rights associated with this mailbox.
1787 * @exception MessagingException
1788 */
1789 public synchronized Rights getMyRights(String mailbox) throws MessagingException {
1790 if (!hasCapability("ACL")) {
1791 throw new MethodNotSupportedException("ACL not available from this IMAP server");
1792 }
1793 IMAPCommand command = new IMAPCommand("MYRIGHTS");
1794 command.appendEncodedString(mailbox);
1795
1796 // now send the MYRIGHTS command, which will return a single MYRIGHTS untagged response.
1797 sendCommand(command);
1798 // there should be just a single MYRIGHTS response back from this command.
1799 IMAPMyRightsResponse response = (IMAPMyRightsResponse)extractResponse("MYRIGHTS");
1800 return response.rights;
1801 }
1802
1803
1804 /**
1805 * List the ACL rights that a particular user has
1806 * to a mailbox.
1807 *
1808 * @param mailbox The target mailbox.
1809 * @param name The user we're querying.
1810 *
1811 * @return An array of rights the use has to this mailbox.
1812 * @exception MessagingException
1813 */
1814 public synchronized Rights[] listACLRights(String mailbox, String name) throws MessagingException {
1815 if (!hasCapability("ACL")) {
1816 throw new MethodNotSupportedException("ACL not available from this IMAP server");
1817 }
1818 IMAPCommand command = new IMAPCommand("LISTRIGHTS");
1819 command.appendEncodedString(mailbox);
1820 command.appendString(name);
1821
1822 // now send the GETACL command, which will return a single ACL untagged response.
1823 sendCommand(command);
1824 // there should be just a single ACL response back from this command.
1825 IMAPListRightsResponse response = (IMAPListRightsResponse)extractResponse("LISTRIGHTS");
1826 return response.rights;
1827 }
1828
1829
1830 /**
1831 * Delete an ACL item for a given user name from
1832 * a target mailbox.
1833 *
1834 * @param mailbox The mailbox we're altering.
1835 * @param name The user name.
1836 *
1837 * @exception MessagingException
1838 */
1839 public synchronized void deleteACL(String mailbox, String name) throws MessagingException {
1840 if (!hasCapability("ACL")) {
1841 throw new MethodNotSupportedException("ACL not available from this IMAP server");
1842 }
1843 IMAPCommand command = new IMAPCommand("DELETEACL");
1844 command.appendEncodedString(mailbox);
1845 command.appendString(name);
1846
1847 // just send the command. No response to handle.
1848 sendSimpleCommand(command);
1849 }
1850
1851 /**
1852 * Fetch the quota root information for a target mailbox.
1853 *
1854 * @param mailbox The mailbox of interest.
1855 *
1856 * @return An array of quotas describing all of the quota roots
1857 * that apply to the target mailbox.
1858 * @exception MessagingException
1859 */
1860 public synchronized Quota[] fetchQuotaRoot(String mailbox) throws MessagingException {
1861 if (!hasCapability("QUOTA")) {
1862 throw new MethodNotSupportedException("QUOTA not available from this IMAP server");
1863 }
1864 IMAPCommand command = new IMAPCommand("GETQUOTAROOT");
1865 command.appendEncodedString(mailbox);
1866
1867 // This will return a single QUOTAROOT response, plust a series of QUOTA responses for
1868 // each root names in the first response.
1869 sendCommand(command);
1870 // we don't really need this, but pull it from the response queue anyway.
1871 extractResponse("QUOTAROOT");
1872
1873 // now get the real meat of the matter
1874 List responses = extractResponses("QUOTA");
1875
1876 // now copy all of the returned quota items into the response array.
1877 Quota[] quotas = new Quota[responses.size()];
1878 for (int i = 0; i < quotas.length; i++) {
1879 IMAPQuotaResponse q = (IMAPQuotaResponse)responses.get(i);
1880 quotas[i] = q.quota;
1881 }
1882
1883 return quotas;
1884 }
1885
1886 /**
1887 * Fetch QUOTA information from a named QUOTE root.
1888 *
1889 * @param root The target root name.
1890 *
1891 * @return An array of Quota items associated with that root name.
1892 * @exception MessagingException
1893 */
1894 public synchronized Quota[] fetchQuota(String root) throws MessagingException {
1895 if (!hasCapability("QUOTA")) {
1896 throw new MethodNotSupportedException("QUOTA not available from this IMAP server");
1897 }
1898 IMAPCommand command = new IMAPCommand("GETQUOTA");
1899 command.appendString(root);
1900
1901 // This will return a single QUOTAROOT response, plust a series of QUOTA responses for
1902 // each root names in the first response.
1903 sendCommand(command);
1904
1905 // now get the real meat of the matter
1906 List responses = extractResponses("QUOTA");
1907
1908 // now copy all of the returned quota items into the response array.
1909 Quota[] quotas = new Quota[responses.size()];
1910 for (int i = 0; i < quotas.length; i++) {
1911 IMAPQuotaResponse q = (IMAPQuotaResponse)responses.get(i);
1912 quotas[i] = q.quota;
1913 }
1914
1915 return quotas;
1916 }
1917
1918 /**
1919 * Set a Quota item for the currently accessed
1920 * userid/folder resource.
1921 *
1922 * @param quota The new QUOTA information.
1923 *
1924 * @exception MessagingException
1925 */
1926 public synchronized void setQuota(Quota quota) throws MessagingException {
1927 if (!hasCapability("QUOTA")) {
1928 throw new MethodNotSupportedException("QUOTA not available from this IMAP server");
1929 }
1930 IMAPCommand command = new IMAPCommand("GETQUOTA");
1931 // this gets appended as a list of resource values
1932 command.appendQuota(quota);
1933
1934 // This will return a single QUOTAROOT response, plust a series of QUOTA responses for
1935 // each root names in the first response.
1936 sendCommand(command);
1937 // we don't really need this, but pull it from the response queue anyway.
1938 extractResponses("QUOTA");
1939 }
1940
1941
1942 /**
1943 * Test if this connection has a given capability.
1944 *
1945 * @param capability The capability name.
1946 *
1947 * @return true if this capability is in the list, false for a mismatch.
1948 */
1949 public boolean hasCapability(String capability) {
1950 if (capabilities == null) {
1951 return false;
1952 }
1953 return capabilities.containsKey(capability);
1954 }
1955
1956 /**
1957 * Tag this connection as having been closed by the
1958 * server. This will not be returned to the
1959 * connection pool.
1960 */
1961 public void setClosed() {
1962 closed = true;
1963 }
1964
1965 /**
1966 * Test if the connnection has been forcibly closed.
1967 *
1968 * @return True if the server disconnected the connection.
1969 */
1970 public boolean isClosed() {
1971 return closed;
1972 }
1973 }
1974