001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.net.imap;
019
020import java.io.IOException;
021import java.util.regex.Matcher;
022import java.util.regex.Pattern;
023
024import org.apache.commons.net.MalformedServerReplyException;
025
026/**
027 * Stores IMAP reply code constants.
028 */
029public final class IMAPReply {
030
031    /** The reply code indicating success of an operation. */
032    public static final int OK = 0;
033
034    /** The reply code indicating failure of an operation. */
035    public static final int NO = 1;
036
037    /** The reply code indicating command rejection. */
038    public static final int BAD = 2;
039
040    /** The reply code indicating command continuation. */
041    public static final int CONT = 3;
042
043    /**
044     * The reply code indicating a partial response. This is used when a chunk listener is registered and the listener requests that the reply lines are cleared
045     * on return.
046     *
047     * @since 3.4
048     */
049    public static final int PARTIAL = 3;
050
051    /** The IMAP reply String indicating success of an operation. */
052    private static final String IMAP_OK = "OK";
053
054    /** The IMAP reply String indicating failure of an operation. */
055    private static final String IMAP_NO = "NO";
056
057    /** The IMAP reply String indicating command rejection. */
058    private static final String IMAP_BAD = "BAD";
059
060    // Start of line for untagged replies
061    private static final String IMAP_UNTAGGED_PREFIX = "* ";
062
063    // Start of line for continuation replies
064    private static final String IMAP_CONTINUATION_PREFIX = "+";
065
066    /**
067     * Guards against Polynomial regular expression used on uncontrolled data.
068     * <ol>
069     * <li>the start of a line.</li>
070     * <li>letters, up to 80.</li>
071     * <li>a space.</li>
072     * <li>non-whitespace characters, up to 80, for example {@code OK}.</li>
073     * <li>up to 500 extra characters.</li>
074     * </ol>
075     */
076    private static final String TAGGED_RESPONSE = "^\\w{1,80} (\\S{1,80}).{0,500}";
077
078    /**
079     * Tag cannot contain: + ( ) { SP CTL % * " \ ]
080     */
081    private static final Pattern TAGGED_PATTERN = Pattern.compile(TAGGED_RESPONSE);
082
083    /**
084     * Guards against Polynomial regular expression used on uncontrolled data.
085     * <ol>
086     * <li>the start of a line, then a star, then a space.</li>
087     * <li>non-whitespace characters, up to 80, for example {@code OK}.</li>
088     * <li>up to 500 extra characters.</li>
089     * </ol>
090     */
091    private static final String UNTAGGED_RESPONSE = "^\\* (\\S{1,80}).{0,500}";
092
093    private static final Pattern UNTAGGED_PATTERN = Pattern.compile(UNTAGGED_RESPONSE);
094    private static final Pattern LITERAL_PATTERN = Pattern.compile("\\{(\\d+)\\}$"); // {dd}
095
096    /**
097     * Gets the String reply code - OK, NO, BAD - in a tagged response as an integer.
098     *
099     * @param line the tagged line to be checked
100     * @return {@link #OK} or {@link #NO} or {@link #BAD} or {@link #CONT}
101     * @throws IOException if the input has an unexpected format
102     */
103    public static int getReplyCode(final String line) throws IOException {
104        return getReplyCode(line, TAGGED_PATTERN);
105    }
106
107    // Helper method to process both tagged and untagged replies.
108    private static int getReplyCode(final String line, final Pattern pattern) throws IOException {
109        if (isContinuation(line)) {
110            return CONT;
111        }
112        final Matcher m = pattern.matcher(line);
113        if (m.matches()) { // TODO would lookingAt() be more efficient? If so, then drop trailing .* from patterns
114            final String code = m.group(1);
115            switch (code) {
116            case IMAP_OK:
117                return OK;
118            case IMAP_BAD:
119                return BAD;
120            case IMAP_NO:
121                return NO;
122            default:
123                break;
124            }
125        }
126        throw new MalformedServerReplyException("Received unexpected IMAP protocol response from server: '" + line + "'.");
127    }
128
129    /**
130     * Gets the String reply code - OK, NO, BAD - in an untagged response as an integer.
131     *
132     * @param line the untagged line to be checked
133     * @return {@link #OK} or {@link #NO} or {@link #BAD} or {@link #CONT}
134     * @throws IOException if the input has an unexpected format
135     */
136    public static int getUntaggedReplyCode(final String line) throws IOException {
137        return getReplyCode(line, UNTAGGED_PATTERN);
138    }
139
140    /**
141     * Tests whether the reply line is a continuation, i.e. starts with "+"
142     *
143     * @param replyCode the code to be checked
144     * @return {@code true} if the response was a continuation
145     */
146    public static boolean isContinuation(final int replyCode) {
147        return replyCode == CONT;
148    }
149
150    /**
151     * Tests whether if the reply line is a continuation, i.e. starts with "+"
152     *
153     * @param line the line to be checked
154     * @return {@code true} if the line is a continuation
155     */
156    public static boolean isContinuation(final String line) {
157        return line.startsWith(IMAP_CONTINUATION_PREFIX);
158    }
159
160    /**
161     * Tests whether whether the reply code indicates success or not
162     *
163     * @param replyCode the code to check
164     * @return {@code true} if the code equals {@link #OK}
165     */
166    public static boolean isSuccess(final int replyCode) {
167        return replyCode == OK;
168    }
169
170    /**
171     * Tests whether if the reply line is untagged - e.g. "* OK ..."
172     *
173     * @param line to be checked
174     * @return {@code true} if the line is untagged
175     */
176    public static boolean isUntagged(final String line) {
177        return line.startsWith(IMAP_UNTAGGED_PREFIX);
178    }
179
180    /**
181     * Checks if the line introduces a literal, i.e. ends with {dd}
182     *
183     * @param line the line to check
184     * @return the literal count, or -1 if there was no literal.
185     */
186    public static int literalCount(final String line) {
187        final Matcher m = LITERAL_PATTERN.matcher(line);
188        if (m.find()) {
189            return Integer.parseInt(m.group(1)); // Should always parse because we matched \d+
190        }
191        return -1;
192    }
193
194    /** Cannot be instantiated. */
195    private IMAPReply() {
196    }
197
198}
199