/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package com.sun.mail.imap.protocol; import java.io.*; import java.util.*; import java.text.*; import java.lang.reflect.*; import java.util.logging.Level; import javax.mail.*; import javax.mail.internet.*; import javax.mail.search.*; import com.sun.mail.util.*; import com.sun.mail.iap.*; import com.sun.mail.auth.Ntlm; import com.sun.mail.imap.ACL; import com.sun.mail.imap.Rights; import com.sun.mail.imap.AppendUID; import com.sun.mail.imap.SortTerm; /** * This class extends the iap.Protocol object and implements IMAP * semantics. In general, there is a method corresponding to each * IMAP protocol command. The typical implementation issues the * appropriate protocol command, collects all responses, processes * those responses that are specific to this command and then * dispatches the rest (the unsolicited ones) to the dispatcher * using the <code>notifyResponseHandlers(r)</code>. * * @author John Mani * @author Bill Shannon */ public class IMAPProtocol extends Protocol { private boolean connected = false; // did constructor succeed? private boolean rev1 = false; // REV1 server ? private boolean noauthdebug = true; // hide auth info in debug output private boolean authenticated; // authenticated? // WARNING: authenticated may be set to true in superclass // constructor, don't initialize it here. private Map capabilities; // WARNING: capabilities may be initialized as a result of superclass // constructor, don't initialize it here. private List authmechs; // WARNING: authmechs may be initialized as a result of superclass // constructor, don't initialize it here. protected SearchSequence searchSequence; protected String[] searchCharsets; // array of search charsets private String name; private SaslAuthenticator saslAuthenticator; // if SASL is being used private ByteArray ba; // a buffer for fetchBody private static final byte[] CRLF = { (byte)'\r', (byte)'\n'}; private static final FetchItem[] fetchItems = { }; /** * Constructor. * Opens a connection to the given host at given port. * * @param host host to connect to * @param port portnumber to connect to * @param debug debug mode * @param props Properties object used by this protocol */ public IMAPProtocol(String name, String host, int port, Properties props, boolean isSSL, MailLogger logger) throws IOException, ProtocolException { super(host, port, props, "mail." + name, isSSL, logger); try { this.name = name; noauthdebug = !PropUtil.getBooleanProperty(props, "mail.debug.auth", false); if (capabilities == null) capability(); if (hasCapability("IMAP4rev1")) rev1 = true; searchCharsets = new String[2]; // 2, for now. searchCharsets[0] = "UTF-8"; searchCharsets[1] = MimeUtility.mimeCharset( MimeUtility.getDefaultJavaCharset() ); connected = true; // must be last statement in constructor } finally { /* * If we get here because an exception was thrown, we need * to disconnect to avoid leaving a connected socket that * no one will be able to use because this object was never * completely constructed. */ if (!connected) disconnect(); } } /** * Return an array of FetchItem objects describing the * FETCH items supported by this protocol. Subclasses may * override this method to combine their FetchItems with * the FetchItems returned by the superclass. * * @since JavaMail 1.4.6 */ public FetchItem[] getFetchItems() { return fetchItems; } /** * CAPABILITY command. * * @see "RFC2060, section 6.1.1" */ public void capability() throws ProtocolException { // Check CAPABILITY Response[] r = command("CAPABILITY", null); if (!r[r.length-1].isOK()) throw new ProtocolException(r[r.length-1].toString()); capabilities = new HashMap(10); authmechs = new ArrayList(5); for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; // Handle *all* untagged CAPABILITY responses. // Though the spec seemingly states that only // one CAPABILITY response string is allowed (6.1.1), // some server vendors claim otherwise. if (ir.keyEquals("CAPABILITY")) parseCapabilities(ir); } } /** * If the response contains a CAPABILITY response code, extract * it and save the capabilities. */ protected void setCapabilities(Response r) { byte b; while ((b = r.readByte()) > 0 && b != (byte)'[') ; if (b == 0) return; String s; s = r.readAtom(); if (!s.equalsIgnoreCase("CAPABILITY")) return; capabilities = new HashMap(10); authmechs = new ArrayList(5); parseCapabilities(r); } /** * Parse the capabilities from a CAPABILITY response or from * a CAPABILITY response code attached to (e.g.) an OK response. */ protected void parseCapabilities(Response r) { String s; while ((s = r.readAtom(']')) != null) { if (s.length() == 0) { if (r.peekByte() == (byte)']') break; /* * Probably found something here that's not an atom. * Rather than loop forever or fail completely, we'll * try to skip this bogus capability. This is known * to happen with: * Netscape Messaging Server 4.03 (built Apr 27 1999) * that returns: * * CAPABILITY * CAPABILITY IMAP4 IMAP4rev1 ... * The "*" in the middle of the capability list causes * us to loop forever here. */ r.skipToken(); } else { capabilities.put(s.toUpperCase(Locale.ENGLISH), s); if (s.regionMatches(true, 0, "AUTH=", 0, 5)) { authmechs.add(s.substring(5)); if (logger.isLoggable(Level.FINE)) logger.fine("AUTH: " + s.substring(5)); } } } } /** * Check the greeting when first connecting; look for PREAUTH response. */ protected void processGreeting(Response r) throws ProtocolException { super.processGreeting(r); // check if it's BAD if (r.isOK()) { // check if it's OK setCapabilities(r); return; } // only other choice is PREAUTH IMAPResponse ir = (IMAPResponse)r; if (ir.keyEquals("PREAUTH")) { authenticated = true; setCapabilities(r); } else throw new ConnectionException(this, r); } /** * Returns <code>true</code> if the connection has been authenticated, * either due to a successful login, or due to a PREAUTH greeting response. */ public boolean isAuthenticated() { return authenticated; } /** * Returns <code>true</code> if this is a IMAP4rev1 server */ public boolean isREV1() { return rev1; } /** * Returns whether this Protocol supports non-synchronizing literals. */ protected boolean supportsNonSyncLiterals() { return hasCapability("LITERAL+"); } /** * Read a response from the server. */ public Response readResponse() throws IOException, ProtocolException { // assert Thread.holdsLock(this); // can't assert because it's called from constructor IMAPResponse r = new IMAPResponse(this); if (r.keyEquals("FETCH")) r = new FetchResponse(r, getFetchItems()); return r; } /** * Check whether the given capability is supported by * this server. Returns <code>true</code> if so, otherwise * returns false. */ public boolean hasCapability(String c) { if (c.endsWith("*")) { c = c.substring(0, c.length() - 1).toUpperCase(Locale.ENGLISH); Iterator it = capabilities.keySet().iterator(); while (it.hasNext()) { if (((String)it.next()).startsWith(c)) return true; } return false; } return capabilities.containsKey(c.toUpperCase(Locale.ENGLISH)); } /** * Return the map of capabilities returned by the server. * * @since JavaMail 1.4.1 */ public Map getCapabilities() { return capabilities; } /** * Close socket connection. * * This method just makes the Protocol.disconnect() method * public. */ public void disconnect() { super.disconnect(); authenticated = false; // just in case } /** * The NOOP command. * * @see "RFC2060, section 6.1.2" */ public void noop() throws ProtocolException { logger.fine("IMAPProtocol noop"); simpleCommand("NOOP", null); } /** * LOGOUT Command. * * @see "RFC2060, section 6.1.3" */ public void logout() throws ProtocolException { try { Response[] r = command("LOGOUT", null); authenticated = false; // dispatch any unsolicited responses. // NOTE that the BYE response is dispatched here as well notifyResponseHandlers(r); } finally { disconnect(); } } /** * LOGIN Command. * * @see "RFC2060, section 6.2.2" */ public void login(String u, String p) throws ProtocolException { Argument args = new Argument(); args.writeString(u); args.writeString(p); Response[] r = null; try { if (noauthdebug && isTracing()) { logger.fine("LOGIN command trace suppressed"); suspendTracing(); } r = command("LOGIN", args); } finally { resumeTracing(); } // dispatch untagged responses notifyResponseHandlers(r); // Handle result of this command if (noauthdebug && isTracing()) logger.fine("LOGIN command result: " + r[r.length-1]); handleResult(r[r.length-1]); // If the response includes a CAPABILITY response code, process it setCapabilities(r[r.length-1]); // if we get this far without an exception, we're authenticated authenticated = true; } /** * The AUTHENTICATE command with AUTH=LOGIN authenticate scheme * * @see "RFC2060, section 6.2.1" */ public synchronized void authlogin(String u, String p) throws ProtocolException { Vector v = new Vector(); String tag = null; Response r = null; boolean done = false; try { if (noauthdebug && isTracing()) { logger.fine("AUTHENTICATE LOGIN command trace suppressed"); suspendTracing(); } try { tag = writeCommand("AUTHENTICATE LOGIN", null); } catch (Exception ex) { // Convert this into a BYE response r = Response.byeResponse(ex); done = true; } OutputStream os = getOutputStream(); // stream to IMAP server /* Wrap a BASE64Encoder around a ByteArrayOutputstream * to craft b64 encoded username and password strings * * Note that the encoded bytes should be sent "as-is" to the * server, *not* as literals or quoted-strings. * * Also note that unlike the B64 definition in MIME, CRLFs * should *not* be inserted during the encoding process. So, I * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine, * which should be sufficiently large ! * * Finally, format the line in a buffer so it can be sent as * a single packet, to avoid triggering a bug in SUN's SIMS 2.0 * server caused by patch 105346. */ ByteArrayOutputStream bos = new ByteArrayOutputStream(); OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); boolean first = true; while (!done) { // loop till we are done try { r = readResponse(); if (r.isContinuation()) { // Server challenge .. String s; if (first) { // Send encoded username s = u; first = false; } else // Send encoded password s = p; // obtain b64 encoded bytes b64os.write(ASCIIUtility.getBytes(s)); b64os.flush(); // complete the encoding bos.write(CRLF); // CRLF termination os.write(bos.toByteArray()); // write out line os.flush(); // flush the stream bos.reset(); // reset buffer } else if (r.isTagged() && r.getTag().equals(tag)) // Ah, our tagged response done = true; else if (r.isBYE()) // outta here done = true; else // hmm .. unsolicited response here ?! v.addElement(r); } catch (Exception ioex) { // convert this into a BYE response r = Response.byeResponse(ioex); done = true; } } } finally { resumeTracing(); } /* Dispatch untagged responses. * NOTE: in our current upper level IMAP classes, we add the * responseHandler to the Protocol object only *after* the * connection has been authenticated. So, for now, the below * code really ends up being just a no-op. */ Response[] responses = new Response[v.size()]; v.copyInto(responses); notifyResponseHandlers(responses); // Handle the final OK, NO, BAD or BYE response if (noauthdebug && isTracing()) logger.fine("AUTHENTICATE LOGIN command result: " + r); handleResult(r); // If the response includes a CAPABILITY response code, process it setCapabilities(r); // if we get this far without an exception, we're authenticated authenticated = true; } /** * The AUTHENTICATE command with AUTH=PLAIN authentication scheme. * This is based heavly on the {@link #authlogin} method. * * @param authzid the authorization id * @param u the username * @param p the password * @throws ProtocolException as thrown by {@link Protocol#handleResult}. * @see "RFC3501, section 6.2.2" * @see "RFC2595, section 6" * @since JavaMail 1.3.2 */ public synchronized void authplain(String authzid, String u, String p) throws ProtocolException { Vector v = new Vector(); String tag = null; Response r = null; boolean done = false; try { if (noauthdebug && isTracing()) { logger.fine("AUTHENTICATE PLAIN command trace suppressed"); suspendTracing(); } try { tag = writeCommand("AUTHENTICATE PLAIN", null); } catch (Exception ex) { // Convert this into a BYE response r = Response.byeResponse(ex); done = true; } OutputStream os = getOutputStream(); // stream to IMAP server /* Wrap a BASE64Encoder around a ByteArrayOutputstream * to craft b64 encoded username and password strings * * Note that the encoded bytes should be sent "as-is" to the * server, *not* as literals or quoted-strings. * * Also note that unlike the B64 definition in MIME, CRLFs * should *not* be inserted during the encoding process. So, I * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine, * which should be sufficiently large ! * * Finally, format the line in a buffer so it can be sent as * a single packet, to avoid triggering a bug in SUN's SIMS 2.0 * server caused by patch 105346. */ ByteArrayOutputStream bos = new ByteArrayOutputStream(); OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); while (!done) { // loop till we are done try { r = readResponse(); if (r.isContinuation()) { // Server challenge .. final String nullByte = "\0"; String s = (authzid == null ? "" : authzid) + nullByte + u + nullByte + p; // obtain b64 encoded bytes b64os.write(ASCIIUtility.getBytes(s)); b64os.flush(); // complete the encoding bos.write(CRLF); // CRLF termination os.write(bos.toByteArray()); // write out line os.flush(); // flush the stream bos.reset(); // reset buffer } else if (r.isTagged() && r.getTag().equals(tag)) // Ah, our tagged response done = true; else if (r.isBYE()) // outta here done = true; else // hmm .. unsolicited response here ?! v.addElement(r); } catch (Exception ioex) { // convert this into a BYE response r = Response.byeResponse(ioex); done = true; } } } finally { resumeTracing(); } /* Dispatch untagged responses. * NOTE: in our current upper level IMAP classes, we add the * responseHandler to the Protocol object only *after* the * connection has been authenticated. So, for now, the below * code really ends up being just a no-op. */ Response[] responses = new Response[v.size()]; v.copyInto(responses); notifyResponseHandlers(responses); // Handle the final OK, NO, BAD or BYE response if (noauthdebug && isTracing()) logger.fine("AUTHENTICATE PLAIN command result: " + r); handleResult(r); // If the response includes a CAPABILITY response code, process it setCapabilities(r); // if we get this far without an exception, we're authenticated authenticated = true; } /** * The AUTHENTICATE command with AUTH=NTLM authentication scheme. * This is based heavly on the {@link #authlogin} method. * * @param authzid the authorization id * @param u the username * @param p the password * @throws ProtocolException as thrown by {@link Protocol#handleResult}. * @see "RFC3501, section 6.2.2" * @see "RFC2595, section 6" * @since JavaMail 1.4.3 */ public synchronized void authntlm(String authzid, String u, String p) throws ProtocolException { Vector v = new Vector(); String tag = null; Response r = null; boolean done = false; String type1Msg = null; int flags = PropUtil.getIntProperty(props, "mail." + name + ".auth.ntlm.flags", 0); String domain = props.getProperty( "mail." + name + ".auth.ntlm.domain", ""); Ntlm ntlm = new Ntlm(domain, getLocalHost(), u, p, logger); try { if (noauthdebug && isTracing()) { logger.fine("AUTHENTICATE NTLM command trace suppressed"); suspendTracing(); } try { tag = writeCommand("AUTHENTICATE NTLM", null); } catch (Exception ex) { // Convert this into a BYE response r = Response.byeResponse(ex); done = true; } OutputStream os = getOutputStream(); // stream to IMAP server boolean first = true; while (!done) { // loop till we are done try { r = readResponse(); if (r.isContinuation()) { // Server challenge .. String s; if (first) { s = ntlm.generateType1Msg(flags); first = false; } else { s = ntlm.generateType3Msg(r.getRest()); } os.write(ASCIIUtility.getBytes(s)); os.write(CRLF); // CRLF termination os.flush(); // flush the stream } else if (r.isTagged() && r.getTag().equals(tag)) // Ah, our tagged response done = true; else if (r.isBYE()) // outta here done = true; else // hmm .. unsolicited response here ?! v.addElement(r); } catch (Exception ioex) { // convert this into a BYE response r = Response.byeResponse(ioex); done = true; } } } finally { resumeTracing(); } /* * Dispatch untagged responses. * NOTE: in our current upper level IMAP classes, we add the * responseHandler to the Protocol object only *after* the * connection has been authenticated. So, for now, the below * code really ends up being just a no-op. */ Response[] responses = new Response[v.size()]; v.copyInto(responses); notifyResponseHandlers(responses); // Handle the final OK, NO, BAD or BYE response if (noauthdebug && isTracing()) logger.fine("AUTHENTICATE NTLM command result: " + r); handleResult(r); // If the response includes a CAPABILITY response code, process it setCapabilities(r); // if we get this far without an exception, we're authenticated authenticated = true; } /** * SASL-based login. */ public void sasllogin(String[] allowed, String realm, String authzid, String u, String p) throws ProtocolException { if (saslAuthenticator == null) { try { Class sac = Class.forName( "com.sun.mail.imap.protocol.IMAPSaslAuthenticator"); Constructor c = sac.getConstructor(new Class[] { IMAPProtocol.class, String.class, Properties.class, MailLogger.class, String.class }); saslAuthenticator = (SaslAuthenticator)c.newInstance( new Object[] { this, name, props, logger, host }); } catch (Exception ex) { logger.log(Level.FINE, "Can't load SASL authenticator", ex); // probably because we're running on a system without SASL return; // not authenticated, try without SASL } } // were any allowed mechanisms specified? List v; if (allowed != null && allowed.length > 0) { // remove anything not supported by the server v = new ArrayList(allowed.length); for (int i = 0; i < allowed.length; i++) if (authmechs.contains(allowed[i])) // XXX - case must match v.add(allowed[i]); } else { // everything is allowed v = authmechs; } String[] mechs = (String[])v.toArray(new String[v.size()]); try { if (noauthdebug && isTracing()) { logger.fine("SASL authentication command trace suppressed"); suspendTracing(); } if (saslAuthenticator.authenticate(mechs, realm, authzid, u, p)) { if (noauthdebug && isTracing()) logger.fine("SASL authentication succeeded"); authenticated = true; } else { if (noauthdebug && isTracing()) logger.fine("SASL authentication failed"); } } finally { resumeTracing(); } } // XXX - for IMAPSaslAuthenticator access to protected method OutputStream getIMAPOutputStream() { return getOutputStream(); } /** * PROXYAUTH Command. * * @see "Netscape/iPlanet/SunONE Messaging Server extension" */ public void proxyauth(String u) throws ProtocolException { Argument args = new Argument(); args.writeString(u); simpleCommand("PROXYAUTH", args); } /** * ID Command, for Yahoo! Mail IMAP server. * * See <A HREF="http://en.wikipedia.org/wiki/Yahoo%21_Mail#Free_IMAP_and_SMTPs_access"> * http://en.wikipedia.org/wiki/Yahoo%21_Mail#Free_IMAP_and_SMTPs_access</A> * * @since JavaMail 1.4.4 */ public void id(String guid) throws ProtocolException { /* * XXX - need to be able to write a string instead * of an astring for the following to work. Argument garg = new Argument(); garg.writeString("GUID"); garg.writeString(guid); Argument args = new Argument(); args.writeArgument(garg); simpleCommand("ID", args); */ simpleCommand("ID (\"GUID\" \"" + guid + "\")", null); } /** * STARTTLS Command. * * @see "RFC3501, section 6.2.1" */ public void startTLS() throws ProtocolException { try { super.startTLS("STARTTLS"); } catch (ProtocolException pex) { logger.log(Level.FINE, "STARTTLS ProtocolException", pex); // ProtocolException just means the command wasn't recognized, // or failed. This should never happen if we check the // CAPABILITY first. throw pex; } catch (Exception ex) { logger.log(Level.FINE, "STARTTLS Exception", ex); // any other exception means we have to shut down the connection // generate an artificial BYE response and disconnect Response[] r = { Response.byeResponse(ex) }; notifyResponseHandlers(r); disconnect(); throw new ProtocolException("STARTTLS failure", ex); } } /** * SELECT Command. * * @see "RFC2060, section 6.3.1" */ public MailboxInfo select(String mbox) throws ProtocolException { // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); Response[] r = command("SELECT", args); // Note that MailboxInfo also removes those responses // it knows about MailboxInfo minfo = new MailboxInfo(r); // dispatch any remaining untagged responses notifyResponseHandlers(r); Response response = r[r.length-1]; if (response.isOK()) { // command succesful if (response.toString().indexOf("READ-ONLY") != -1) minfo.mode = Folder.READ_ONLY; else minfo.mode = Folder.READ_WRITE; } handleResult(response); return minfo; } /** * EXAMINE Command. * * @see "RFC2060, section 6.3.2" */ public MailboxInfo examine(String mbox) throws ProtocolException { // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); Response[] r = command("EXAMINE", args); // Note that MailboxInfo also removes those responses // it knows about MailboxInfo minfo = new MailboxInfo(r); minfo.mode = Folder.READ_ONLY; // Obviously // dispatch any remaining untagged responses notifyResponseHandlers(r); handleResult(r[r.length-1]); return minfo; } /** * UNSELECT Command. * * @see "RFC 3691" * @since JavaMail 1.4.4 */ public void unselect() throws ProtocolException { if (!hasCapability("UNSELECT")) throw new BadCommandException("UNSELECT not supported"); simpleCommand("UNSELECT", null); } /** * STATUS Command. * * @see "RFC2060, section 6.3.10" */ public Status status(String mbox, String[] items) throws ProtocolException { if (!isREV1() && !hasCapability("IMAP4SUNVERSION")) // STATUS is rev1 only, however the non-rev1 SIMS2.0 // does support this. throw new BadCommandException("STATUS not supported"); // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); Argument itemArgs = new Argument(); if (items == null) items = Status.standardItems; for (int i = 0, len = items.length; i < len; i++) itemArgs.writeAtom(items[i]); args.writeArgument(itemArgs); Response[] r = command("STATUS", args); Status status = null; Response response = r[r.length-1]; // Grab all STATUS responses if (response.isOK()) { // command succesful for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals("STATUS")) { if (status == null) status = new Status(ir); else // collect 'em all Status.add(status, new Status(ir)); r[i] = null; } } } // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); return status; } /** * CREATE Command. * * @see "RFC2060, section 6.3.3" */ public void create(String mbox) throws ProtocolException { // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); simpleCommand("CREATE", args); } /** * DELETE Command. * * @see "RFC2060, section 6.3.4" */ public void delete(String mbox) throws ProtocolException { // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); simpleCommand("DELETE", args); } /** * RENAME Command. * * @see "RFC2060, section 6.3.5" */ public void rename(String o, String n) throws ProtocolException { // encode the mbox as per RFC2060 o = BASE64MailboxEncoder.encode(o); n = BASE64MailboxEncoder.encode(n); Argument args = new Argument(); args.writeString(o); args.writeString(n); simpleCommand("RENAME", args); } /** * SUBSCRIBE Command. * * @see "RFC2060, section 6.3.6" */ public void subscribe(String mbox) throws ProtocolException { Argument args = new Argument(); // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); args.writeString(mbox); simpleCommand("SUBSCRIBE", args); } /** * UNSUBSCRIBE Command. * * @see "RFC2060, section 6.3.7" */ public void unsubscribe(String mbox) throws ProtocolException { Argument args = new Argument(); // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); args.writeString(mbox); simpleCommand("UNSUBSCRIBE", args); } /** * LIST Command. * * @see "RFC2060, section 6.3.8" */ public ListInfo[] list(String ref, String pattern) throws ProtocolException { return doList("LIST", ref, pattern); } /** * LSUB Command. * * @see "RFC2060, section 6.3.9" */ public ListInfo[] lsub(String ref, String pattern) throws ProtocolException { return doList("LSUB", ref, pattern); } /** * Execute the specified LIST-like command (e.g., "LIST" or "LSUB"), * using the reference and pattern. * * @since JavaMail 1.4.6 */ protected ListInfo[] doList(String cmd, String ref, String pat) throws ProtocolException { // encode the mbox as per RFC2060 ref = BASE64MailboxEncoder.encode(ref); pat = BASE64MailboxEncoder.encode(pat); Argument args = new Argument(); args.writeString(ref); args.writeString(pat); Response[] r = command(cmd, args); ListInfo[] linfo = null; Response response = r[r.length-1]; if (response.isOK()) { // command succesful Vector v = new Vector(1); for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals(cmd)) { v.addElement(new ListInfo(ir)); r[i] = null; } } if (v.size() > 0) { linfo = new ListInfo[v.size()]; v.copyInto(linfo); } } // Dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); return linfo; } /** * APPEND Command. * * @see "RFC2060, section 6.3.11" */ public void append(String mbox, Flags f, Date d, Literal data) throws ProtocolException { appenduid(mbox, f, d, data, false); // ignore return value } /** * APPEND Command, return uid from APPENDUID response code. * * @see "RFC2060, section 6.3.11" */ public AppendUID appenduid(String mbox, Flags f, Date d, Literal data) throws ProtocolException { return appenduid(mbox, f, d, data, true); } public AppendUID appenduid(String mbox, Flags f, Date d, Literal data, boolean uid) throws ProtocolException { // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); if (f != null) { // set Flags in appended message // can't set the \Recent flag in APPEND if (f.contains(Flags.Flag.RECENT)) { f = new Flags(f); // copy, don't modify orig f.remove(Flags.Flag.RECENT); // remove RECENT from copy } /* * HACK ALERT: We want the flag_list to be written out * without any checking/processing of the bytes in it. If * I use writeString(), the flag_list will end up being * quoted since it contains "illegal" characters. So I * am depending on implementation knowledge that writeAtom() * does not do any checking/processing - it just writes out * the bytes. What we really need is a writeFoo() that just * dumps out its argument. */ args.writeAtom(createFlagList(f)); } if (d != null) // set INTERNALDATE in appended message args.writeString(INTERNALDATE.format(d)); args.writeBytes(data); Response[] r = command("APPEND", args); // dispatch untagged responses notifyResponseHandlers(r); // Handle result of this command handleResult(r[r.length-1]); if (uid) return getAppendUID(r[r.length-1]); else return null; } /** * If the response contains an APPENDUID response code, extract * it and return an AppendUID object with the information. */ private AppendUID getAppendUID(Response r) { if (!r.isOK()) return null; byte b; while ((b = r.readByte()) > 0 && b != (byte)'[') ; if (b == 0) return null; String s; s = r.readAtom(); if (!s.equalsIgnoreCase("APPENDUID")) return null; long uidvalidity = r.readLong(); long uid = r.readLong(); return new AppendUID(uidvalidity, uid); } /** * CHECK Command. * * @see "RFC2060, section 6.4.1" */ public void check() throws ProtocolException { simpleCommand("CHECK", null); } /** * CLOSE Command. * * @see "RFC2060, section 6.4.2" */ public void close() throws ProtocolException { simpleCommand("CLOSE", null); } /** * EXPUNGE Command. * * @see "RFC2060, section 6.4.3" */ public void expunge() throws ProtocolException { simpleCommand("EXPUNGE", null); } /** * UID EXPUNGE Command. * * @see "RFC2359, section 4.1" */ public void uidexpunge(UIDSet[] set) throws ProtocolException { if (!hasCapability("UIDPLUS")) throw new BadCommandException("UID EXPUNGE not supported"); simpleCommand("UID EXPUNGE " + UIDSet.toString(set), null); } /** * Fetch the BODYSTRUCTURE of the specified message. */ public BODYSTRUCTURE fetchBodyStructure(int msgno) throws ProtocolException { Response[] r = fetch(msgno, "BODYSTRUCTURE"); notifyResponseHandlers(r); Response response = r[r.length-1]; if (response.isOK()) return (BODYSTRUCTURE)FetchResponse.getItem(r, msgno, BODYSTRUCTURE.class); else if (response.isNO()) return null; else { handleResult(response); return null; } } /** * Fetch given BODY section, without marking the message * as SEEN. */ public BODY peekBody(int msgno, String section) throws ProtocolException { return fetchBody(msgno, section, true); } /** * Fetch given BODY section. */ public BODY fetchBody(int msgno, String section) throws ProtocolException { return fetchBody(msgno, section, false); } protected BODY fetchBody(int msgno, String section, boolean peek) throws ProtocolException { Response[] r; if (peek) r = fetch(msgno, "BODY.PEEK[" + (section == null ? "]" : section + "]")); else r = fetch(msgno, "BODY[" + (section == null ? "]" : section + "]")); notifyResponseHandlers(r); Response response = r[r.length-1]; if (response.isOK()) return (BODY)FetchResponse.getItem(r, msgno, BODY.class); else if (response.isNO()) return null; else { handleResult(response); return null; } } /** * Partial FETCH of given BODY section, without setting SEEN flag. */ public BODY peekBody(int msgno, String section, int start, int size) throws ProtocolException { return fetchBody(msgno, section, start, size, true, null); } /** * Partial FETCH of given BODY section. */ public BODY fetchBody(int msgno, String section, int start, int size) throws ProtocolException { return fetchBody(msgno, section, start, size, false, null); } /** * Partial FETCH of given BODY section, without setting SEEN flag. */ public BODY peekBody(int msgno, String section, int start, int size, ByteArray ba) throws ProtocolException { return fetchBody(msgno, section, start, size, true, ba); } /** * Partial FETCH of given BODY section. */ public BODY fetchBody(int msgno, String section, int start, int size, ByteArray ba) throws ProtocolException { return fetchBody(msgno, section, start, size, false, ba); } protected BODY fetchBody(int msgno, String section, int start, int size, boolean peek, ByteArray ba) throws ProtocolException { this.ba = ba; // save for later use by getResponseBuffer Response[] r = fetch( msgno, (peek ? "BODY.PEEK[" : "BODY[" ) + (section == null ? "]<" : (section +"]<")) + String.valueOf(start) + "." + String.valueOf(size) + ">" ); notifyResponseHandlers(r); Response response = r[r.length-1]; if (response.isOK()) return (BODY)FetchResponse.getItem(r, msgno, BODY.class); else if (response.isNO()) return null; else { handleResult(response); return null; } } /** * Return a buffer to read a response into. * The buffer is provided by fetchBody and is * used only once. */ protected ByteArray getResponseBuffer() { ByteArray ret = ba; ba = null; return ret; } /** * Fetch the specified RFC822 Data item. 'what' names * the item to be fetched. 'what' can be <code>null</code> * to fetch the whole message. */ public RFC822DATA fetchRFC822(int msgno, String what) throws ProtocolException { Response[] r = fetch(msgno, what == null ? "RFC822" : "RFC822." + what ); // dispatch untagged responses notifyResponseHandlers(r); Response response = r[r.length-1]; if (response.isOK()) return (RFC822DATA)FetchResponse.getItem(r, msgno, RFC822DATA.class); else if (response.isNO()) return null; else { handleResult(response); return null; } } /** * Fetch the FLAGS for the given message. */ public Flags fetchFlags(int msgno) throws ProtocolException { Flags flags = null; Response[] r = fetch(msgno, "FLAGS"); // Search for our FLAGS response for (int i = 0, len = r.length; i < len; i++) { if (r[i] == null || !(r[i] instanceof FetchResponse) || ((FetchResponse)r[i]).getNumber() != msgno) continue; FetchResponse fr = (FetchResponse)r[i]; if ((flags = (Flags)fr.getItem(Flags.class)) != null) { r[i] = null; // remove this response break; } } // dispatch untagged responses notifyResponseHandlers(r); handleResult(r[r.length-1]); return flags; } /** * Fetch the IMAP UID for the given message. */ public UID fetchUID(int msgno) throws ProtocolException { Response[] r = fetch(msgno, "UID"); // dispatch untagged responses notifyResponseHandlers(r); Response response = r[r.length-1]; if (response.isOK()) return (UID)FetchResponse.getItem(r, msgno, UID.class); else if (response.isNO()) // XXX: Issue NOOP ? return null; else { handleResult(response); return null; // NOTREACHED } } /** * Get the sequence number for the given UID. A UID object * containing the sequence number is returned. If the given UID * is invalid, <code>null</code> is returned. */ public UID fetchSequenceNumber(long uid) throws ProtocolException { UID u = null; Response[] r = fetch(String.valueOf(uid), "UID", true); for (int i = 0, len = r.length; i < len; i++) { if (r[i] == null || !(r[i] instanceof FetchResponse)) continue; FetchResponse fr = (FetchResponse)r[i]; if ((u = (UID)fr.getItem(UID.class)) != null) { if (u.uid == uid) // this is the one we want break; else u = null; } } notifyResponseHandlers(r); handleResult(r[r.length-1]); return u; } /** * Get the sequence numbers for UIDs ranging from start till end. * UID objects that contain the sequence numbers are returned. * If no UIDs in the given range are found, an empty array is returned. */ public UID[] fetchSequenceNumbers(long start, long end) throws ProtocolException { Response[] r = fetch(String.valueOf(start) + ":" + (end == UIDFolder.LASTUID ? "*" : String.valueOf(end)), "UID", true); UID u; Vector v = new Vector(); for (int i = 0, len = r.length; i < len; i++) { if (r[i] == null || !(r[i] instanceof FetchResponse)) continue; FetchResponse fr = (FetchResponse)r[i]; if ((u = (UID)fr.getItem(UID.class)) != null) v.addElement(u); } notifyResponseHandlers(r); handleResult(r[r.length-1]); UID[] ua = new UID[v.size()]; v.copyInto(ua); return ua; } /** * Get the sequence numbers for UIDs ranging from start till end. * UID objects that contain the sequence numbers are returned. * If no UIDs in the given range are found, an empty array is returned. */ public UID[] fetchSequenceNumbers(long[] uids) throws ProtocolException { StringBuffer sb = new StringBuffer(); for (int i = 0; i < uids.length; i++) { if (i > 0) sb.append(","); sb.append(String.valueOf(uids[i])); } Response[] r = fetch(sb.toString(), "UID", true); UID u; Vector v = new Vector(); for (int i = 0, len = r.length; i < len; i++) { if (r[i] == null || !(r[i] instanceof FetchResponse)) continue; FetchResponse fr = (FetchResponse)r[i]; if ((u = (UID)fr.getItem(UID.class)) != null) v.addElement(u); } notifyResponseHandlers(r); handleResult(r[r.length-1]); UID[] ua = new UID[v.size()]; v.copyInto(ua); return ua; } public Response[] fetch(MessageSet[] msgsets, String what) throws ProtocolException { return fetch(MessageSet.toString(msgsets), what, false); } public Response[] fetch(int start, int end, String what) throws ProtocolException { return fetch(String.valueOf(start) + ":" + String.valueOf(end), what, false); } public Response[] fetch(int msg, String what) throws ProtocolException { return fetch(String.valueOf(msg), what, false); } private Response[] fetch(String msgSequence, String what, boolean uid) throws ProtocolException { if (uid) return command("UID FETCH " + msgSequence +" (" + what + ")",null); else return command("FETCH " + msgSequence + " (" + what + ")", null); } /** * COPY command. */ public void copy(MessageSet[] msgsets, String mbox) throws ProtocolException { copy(MessageSet.toString(msgsets), mbox); } public void copy(int start, int end, String mbox) throws ProtocolException { copy(String.valueOf(start) + ":" + String.valueOf(end), mbox); } private void copy(String msgSequence, String mbox) throws ProtocolException { // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeAtom(msgSequence); args.writeString(mbox); simpleCommand("COPY", args); } public void storeFlags(MessageSet[] msgsets, Flags flags, boolean set) throws ProtocolException { storeFlags(MessageSet.toString(msgsets), flags, set); } public void storeFlags(int start, int end, Flags flags, boolean set) throws ProtocolException { storeFlags(String.valueOf(start) + ":" + String.valueOf(end), flags, set); } /** * Set the specified flags on this message. <p> */ public void storeFlags(int msg, Flags flags, boolean set) throws ProtocolException { storeFlags(String.valueOf(msg), flags, set); } private void storeFlags(String msgset, Flags flags, boolean set) throws ProtocolException { Response[] r; if (set) r = command("STORE " + msgset + " +FLAGS " + createFlagList(flags), null); else r = command("STORE " + msgset + " -FLAGS " + createFlagList(flags), null); // Dispatch untagged responses notifyResponseHandlers(r); handleResult(r[r.length-1]); } /** * Creates an IMAP flag_list from the given Flags object. */ private String createFlagList(Flags flags) { StringBuffer sb = new StringBuffer(); sb.append("("); // start of flag_list Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags boolean first = true; for (int i = 0; i < sf.length; i++) { String s; Flags.Flag f = sf[i]; if (f == Flags.Flag.ANSWERED) s = "\\Answered"; else if (f == Flags.Flag.DELETED) s = "\\Deleted"; else if (f == Flags.Flag.DRAFT) s = "\\Draft"; else if (f == Flags.Flag.FLAGGED) s = "\\Flagged"; else if (f == Flags.Flag.RECENT) s = "\\Recent"; else if (f == Flags.Flag.SEEN) s = "\\Seen"; else continue; // skip it if (first) first = false; else sb.append(' '); sb.append(s); } String[] uf = flags.getUserFlags(); // get the user flag strings for (int i = 0; i < uf.length; i++) { if (first) first = false; else sb.append(' '); sb.append(uf[i]); } sb.append(")"); // terminate flag_list return sb.toString(); } /** * Issue the given search criterion on the specified message sets. * Returns array of matching sequence numbers. An empty array * is returned if no matches are found. * * @param msgsets array of MessageSets * @param term SearchTerm * @return array of matching sequence numbers. */ public int[] search(MessageSet[] msgsets, SearchTerm term) throws ProtocolException, SearchException { return search(MessageSet.toString(msgsets), term); } /** * Issue the given search criterion on all messages in this folder. * Returns array of matching sequence numbers. An empty array * is returned if no matches are found. * * @param term SearchTerm * @return array of matching sequence numbers. */ public int[] search(SearchTerm term) throws ProtocolException, SearchException { return search("ALL", term); } /* * Apply the given SearchTerm on the specified sequence. * Returns array of matching sequence numbers. Note that an empty * array is returned for no matches. */ private int[] search(String msgSequence, SearchTerm term) throws ProtocolException, SearchException { // Check if the search "text" terms contain only ASCII chars if (getSearchSequence().isAscii(term)) { try { return issueSearch(msgSequence, term, null); } catch (IOException ioex) { /* will not happen */ } } /* * The search "text" terms do contain non-ASCII chars. We need to * use SEARCH CHARSET <charset> ... * The charsets we try to use are UTF-8 and the locale's * default charset. If the server supports UTF-8, great, * always use it. Else we try to use the default charset. */ // Cycle thru the list of charsets for (int i = 0; i < searchCharsets.length; i++) { if (searchCharsets[i] == null) continue; try { return issueSearch(msgSequence, term, searchCharsets[i]); } catch (CommandFailedException cfx) { /* * Server returned NO. For now, I'll just assume that * this indicates that this charset is unsupported. * We can check the BADCHARSET response code once * that's spec'd into the IMAP RFC .. */ searchCharsets[i] = null; continue; } catch (IOException ioex) { /* Charset conversion failed. Try the next one */ continue; } catch (ProtocolException pex) { throw pex; } catch (SearchException sex) { throw sex; } } // No luck. throw new SearchException("Search failed"); } /* Apply the given SearchTerm on the specified sequence, using the * given charset. <p> * Returns array of matching sequence numbers. Note that an empty * array is returned for no matches. */ private int[] issueSearch(String msgSequence, SearchTerm term, String charset) throws ProtocolException, SearchException, IOException { // Generate a search-sequence with the given charset Argument args = getSearchSequence().generateSequence(term, charset == null ? null : MimeUtility.javaCharset(charset) ); args.writeAtom(msgSequence); Response[] r; if (charset == null) // text is all US-ASCII r = command("SEARCH", args); else r = command("SEARCH CHARSET " + charset, args); Response response = r[r.length-1]; int[] matches = null; // Grab all SEARCH responses if (response.isOK()) { // command succesful Vector v = new Vector(); int num; for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; // There *will* be one SEARCH response. if (ir.keyEquals("SEARCH")) { while ((num = ir.readNumber()) != -1) v.addElement(new Integer(num)); r[i] = null; } } // Copy the vector into 'matches' int vsize = v.size(); matches = new int[vsize]; for (int i = 0; i < vsize; i++) matches[i] = ((Integer)v.elementAt(i)).intValue(); } // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); return matches; } /** * Get the SearchSequence object. * The SearchSequence object instance is saved in the searchSequence * field. Subclasses of IMAPProtocol may override this method to * return a subclass of SearchSequence, in order to add support for * product-specific search terms. * * @since JavaMail 1.4.6 */ protected SearchSequence getSearchSequence() { if (searchSequence == null) searchSequence = new SearchSequence(); return searchSequence; } /** * Sort messages in the folder according to the specified sort criteria. * If the search term is not null, limit the sort to only the messages * that match the search term. * Returns an array of sorted sequence numbers. An empty array * is returned if no matches are found. * * @param term sort criteria * @param sterm SearchTerm * @return array of matching sequence numbers. * * @see "RFC 5256" * @since JavaMail 1.4.4 */ public int[] sort(SortTerm[] term, SearchTerm sterm) throws ProtocolException, SearchException { if (!hasCapability("SORT*")) throw new BadCommandException("SORT not supported"); if (term == null || term.length == 0) throw new BadCommandException("Must have at least one sort term"); Argument args = new Argument(); Argument sargs = new Argument(); for (int i = 0; i < term.length; i++) sargs.writeAtom(term[i].toString()); args.writeArgument(sargs); // sort criteria args.writeAtom("UTF-8"); // charset specification if (sterm != null) { try { args.append( getSearchSequence().generateSequence(sterm, "UTF-8")); } catch (IOException ioex) { // should never happen throw new SearchException(ioex.toString()); } } else args.writeAtom("ALL"); Response[] r = command("SORT", args); Response response = r[r.length-1]; int[] matches = null; // Grab all SORT responses if (response.isOK()) { // command succesful Vector v = new Vector(); int num; for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals("SORT")) { while ((num = ir.readNumber()) != -1) v.addElement(new Integer(num)); r[i] = null; } } // Copy the vector into 'matches' int vsize = v.size(); matches = new int[vsize]; for (int i = 0; i < vsize; i++) matches[i] = ((Integer)v.elementAt(i)).intValue(); } // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); return matches; } /** * NAMESPACE Command. * * @see "RFC2342" */ public Namespaces namespace() throws ProtocolException { if (!hasCapability("NAMESPACE")) throw new BadCommandException("NAMESPACE not supported"); Response[] r = command("NAMESPACE", null); Namespaces namespace = null; Response response = r[r.length-1]; // Grab NAMESPACE response if (response.isOK()) { // command succesful for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals("NAMESPACE")) { if (namespace == null) namespace = new Namespaces(ir); r[i] = null; } } } // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); return namespace; } /** * GETQUOTAROOT Command. * * Returns an array of Quota objects, representing the quotas * for this mailbox and, indirectly, the quotaroots for this * mailbox. * * @see "RFC2087" */ public Quota[] getQuotaRoot(String mbox) throws ProtocolException { if (!hasCapability("QUOTA")) throw new BadCommandException("GETQUOTAROOT not supported"); // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); Response[] r = command("GETQUOTAROOT", args); Response response = r[r.length-1]; Hashtable tab = new Hashtable(); // Grab all QUOTAROOT and QUOTA responses if (response.isOK()) { // command succesful for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals("QUOTAROOT")) { // quotaroot_response // ::= "QUOTAROOT" SP astring *(SP astring) // read name of mailbox and throw away ir.readAtomString(); // for each quotaroot add a placeholder quota String root = null; while ((root = ir.readAtomString()) != null && root.length() > 0) tab.put(root, new Quota(root)); r[i] = null; } else if (ir.keyEquals("QUOTA")) { Quota quota = parseQuota(ir); Quota q = (Quota)tab.get(quota.quotaRoot); if (q != null && q.resources != null) { // XXX - should merge resources } tab.put(quota.quotaRoot, quota); r[i] = null; } } } // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); Quota[] qa = new Quota[tab.size()]; Enumeration e = tab.elements(); for (int i = 0; e.hasMoreElements(); i++) qa[i] = (Quota)e.nextElement(); return qa; } /** * GETQUOTA Command. * * Returns an array of Quota objects, representing the quotas * for this quotaroot. * * @see "RFC2087" */ public Quota[] getQuota(String root) throws ProtocolException { if (!hasCapability("QUOTA")) throw new BadCommandException("QUOTA not supported"); Argument args = new Argument(); args.writeString(root); Response[] r = command("GETQUOTA", args); Quota quota = null; Vector v = new Vector(); Response response = r[r.length-1]; // Grab all QUOTA responses if (response.isOK()) { // command succesful for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals("QUOTA")) { quota = parseQuota(ir); v.addElement(quota); r[i] = null; } } } // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); Quota[] qa = new Quota[v.size()]; v.copyInto(qa); return qa; } /** * SETQUOTA Command. * * Set the indicated quota on the corresponding quotaroot. * * @see "RFC2087" */ public void setQuota(Quota quota) throws ProtocolException { if (!hasCapability("QUOTA")) throw new BadCommandException("QUOTA not supported"); Argument args = new Argument(); args.writeString(quota.quotaRoot); Argument qargs = new Argument(); if (quota.resources != null) { for (int i = 0; i < quota.resources.length; i++) { qargs.writeAtom(quota.resources[i].name); qargs.writeNumber(quota.resources[i].limit); } } args.writeArgument(qargs); Response[] r = command("SETQUOTA", args); Response response = r[r.length-1]; // XXX - It's not clear from the RFC whether the SETQUOTA command // will provoke untagged QUOTA responses. If it does, perhaps // we should grab them here and return them? /* Quota quota = null; Vector v = new Vector(); // Grab all QUOTA responses if (response.isOK()) { // command succesful for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals("QUOTA")) { quota = parseQuota(ir); v.addElement(quota); r[i] = null; } } } */ // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); /* Quota[] qa = new Quota[v.size()]; v.copyInto(qa); return qa; */ } /** * Parse a QUOTA response. */ private Quota parseQuota(Response r) throws ParsingException { // quota_response ::= "QUOTA" SP astring SP quota_list String quotaRoot = r.readAtomString(); // quotaroot ::= astring Quota q = new Quota(quotaRoot); r.skipSpaces(); // quota_list ::= "(" #quota_resource ")" if (r.readByte() != '(') throw new ParsingException("parse error in QUOTA"); Vector v = new Vector(); while (r.peekByte() != ')') { // quota_resource ::= atom SP number SP number String name = r.readAtom(); if (name != null) { long usage = r.readLong(); long limit = r.readLong(); Quota.Resource res = new Quota.Resource(name, usage, limit); v.addElement(res); } } r.readByte(); q.resources = new Quota.Resource[v.size()]; v.copyInto(q.resources); return q; } /** * SETACL Command. * * @see "RFC2086" */ public void setACL(String mbox, char modifier, ACL acl) throws ProtocolException { if (!hasCapability("ACL")) throw new BadCommandException("ACL not supported"); // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); args.writeString(acl.getName()); String rights = acl.getRights().toString(); if (modifier == '+' || modifier == '-') rights = modifier + rights; args.writeString(rights); Response[] r = command("SETACL", args); Response response = r[r.length-1]; // dispatch untagged responses notifyResponseHandlers(r); handleResult(response); } /** * DELETEACL Command. * * @see "RFC2086" */ public void deleteACL(String mbox, String user) throws ProtocolException { if (!hasCapability("ACL")) throw new BadCommandException("ACL not supported"); // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); args.writeString(user); Response[] r = command("DELETEACL", args); Response response = r[r.length-1]; // dispatch untagged responses notifyResponseHandlers(r); handleResult(response); } /** * GETACL Command. * * @see "RFC2086" */ public ACL[] getACL(String mbox) throws ProtocolException { if (!hasCapability("ACL")) throw new BadCommandException("ACL not supported"); // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); Response[] r = command("GETACL", args); Response response = r[r.length-1]; // Grab all ACL responses Vector v = new Vector(); if (response.isOK()) { // command succesful for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals("ACL")) { // acl_data ::= "ACL" SPACE mailbox // *(SPACE identifier SPACE rights) // read name of mailbox and throw away ir.readAtomString(); String name = null; while ((name = ir.readAtomString()) != null) { String rights = ir.readAtomString(); if (rights == null) break; ACL acl = new ACL(name, new Rights(rights)); v.addElement(acl); } r[i] = null; } } } // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); ACL[] aa = new ACL[v.size()]; v.copyInto(aa); return aa; } /** * LISTRIGHTS Command. * * @see "RFC2086" */ public Rights[] listRights(String mbox, String user) throws ProtocolException { if (!hasCapability("ACL")) throw new BadCommandException("ACL not supported"); // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); args.writeString(user); Response[] r = command("LISTRIGHTS", args); Response response = r[r.length-1]; // Grab LISTRIGHTS response Vector v = new Vector(); if (response.isOK()) { // command succesful for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals("LISTRIGHTS")) { // listrights_data ::= "LISTRIGHTS" SPACE mailbox // SPACE identifier SPACE rights *(SPACE rights) // read name of mailbox and throw away ir.readAtomString(); // read identifier and throw away ir.readAtomString(); String rights; while ((rights = ir.readAtomString()) != null) v.addElement(new Rights(rights)); r[i] = null; } } } // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); Rights[] ra = new Rights[v.size()]; v.copyInto(ra); return ra; } /** * MYRIGHTS Command. * * @see "RFC2086" */ public Rights myRights(String mbox) throws ProtocolException { if (!hasCapability("ACL")) throw new BadCommandException("ACL not supported"); // encode the mbox as per RFC2060 mbox = BASE64MailboxEncoder.encode(mbox); Argument args = new Argument(); args.writeString(mbox); Response[] r = command("MYRIGHTS", args); Response response = r[r.length-1]; // Grab MYRIGHTS response Rights rights = null; if (response.isOK()) { // command succesful for (int i = 0, len = r.length; i < len; i++) { if (!(r[i] instanceof IMAPResponse)) continue; IMAPResponse ir = (IMAPResponse)r[i]; if (ir.keyEquals("MYRIGHTS")) { // myrights_data ::= "MYRIGHTS" SPACE mailbox SPACE rights // read name of mailbox and throw away ir.readAtomString(); String rs = ir.readAtomString(); if (rights == null) rights = new Rights(rs); r[i] = null; } } } // dispatch remaining untagged responses notifyResponseHandlers(r); handleResult(response); return rights; } /* * The tag used on the IDLE command. Set by idleStart() and * used in processIdleResponse() to determine if the response * is the matching end tag. */ private volatile String idleTag; /** * IDLE Command. <p> * * If the server supports the IDLE command extension, the IDLE * command is issued and this method blocks until a response has * been received. Once the first response has been received, the * IDLE command is terminated and all responses are collected and * handled and this method returns. <p> * * Note that while this method is blocked waiting for a response, * no other threads may issue any commands to the server that would * use this same connection. * * @see "RFC2177" * @since JavaMail 1.4.1 */ public synchronized void idleStart() throws ProtocolException { if (!hasCapability("IDLE")) throw new BadCommandException("IDLE not supported"); Vector v = new Vector(); boolean done = false; Response r = null; // write the command try { idleTag = writeCommand("IDLE", null); } catch (LiteralException lex) { v.addElement(lex.getResponse()); done = true; } catch (Exception ex) { // Convert this into a BYE response v.addElement(Response.byeResponse(ex)); done = true; } while (!done) { try { r = readResponse(); } catch (IOException ioex) { // convert this into a BYE response r = Response.byeResponse(ioex); } catch (ProtocolException pex) { continue; // skip this response } v.addElement(r); if (r.isContinuation() || r.isBYE()) done = true; } Response[] responses = new Response[v.size()]; v.copyInto(responses); r = responses[responses.length-1]; // dispatch remaining untagged responses notifyResponseHandlers(responses); if (!r.isContinuation()) handleResult(r); } /** * While an IDLE command is in progress, read a response * sent from the server. The response is read with no locks * held so that when the read blocks waiting for the response * from the server it's not holding locks that would prevent * other threads from interrupting the IDLE command. * * @since JavaMail 1.4.1 */ public synchronized Response readIdleResponse() { if (idleTag == null) return null; // IDLE not in progress Response r = null; while (r == null) { try { r = readResponse(); } catch (InterruptedIOException iioex) { /* * If a socket timeout was set, the read will timeout * before the IDLE times out. In that case, just go * back and read some more. After all, the point of * IDLE is to sit here and wait until something happens. */ if (iioex.bytesTransferred == 0) r = null; // keep trying else // convert this into a BYE response r = Response.byeResponse(iioex); } catch (IOException ioex) { // convert this into a BYE response r = Response.byeResponse(ioex); } catch (ProtocolException pex) { // convert this into a BYE response r = Response.byeResponse(pex); } } return r; } /** * Process a response returned by readIdleResponse(). * This method will be called with appropriate locks * held so that the processing of the response is safe. * * @since JavaMail 1.4.1 */ public boolean processIdleResponse(Response r) throws ProtocolException { Response[] responses = new Response[1]; responses[0] = r; boolean done = false; // done reading responses? notifyResponseHandlers(responses); if (r.isBYE()) // shouldn't wait for command completion response done = true; // If this is a matching command completion response, we are done if (r.isTagged() && r.getTag().equals(idleTag)) done = true; if (done) idleTag = null; // no longer in IDLE handleResult(r); return !done; } // the DONE command to break out of IDLE private static final byte[] DONE = { 'D', 'O', 'N', 'E', '\r', '\n' }; /** * Abort an IDLE command. While one thread is blocked in * readIdleResponse(), another thread will use this method * to abort the IDLE command, which will cause the server * to send the closing tag for the IDLE command, which * readIdleResponse() and processIdleResponse() will see * and terminate the IDLE state. * * @since JavaMail 1.4.1 */ public void idleAbort() throws ProtocolException { OutputStream os = getOutputStream(); try { os.write(DONE); os.flush(); } catch (IOException ex) { // nothing to do, hope to detect it again later } } }