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    /** The reply code indicating success of an operation. */
031    public static final int OK = 0;
032
033    /** The reply code indicating failure of an operation. */
034    public static final int NO = 1;
035
036    /** The reply code indicating command rejection. */
037    public static final int BAD = 2;
038
039    /** The reply code indicating command continuation. */
040    public static final int CONT = 3;
041
042    /**
043     * 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
044     * on return.
045     *
046     * @since 3.4
047     */
048    public static final int PARTIAL = 3;
049
050    /** The IMAP reply String indicating success of an operation. */
051    private static final String IMAP_OK = "OK";
052
053    /** The IMAP reply String indicating failure of an operation. */
054    private static final String IMAP_NO = "NO";
055
056    /** The IMAP reply String indicating command rejection. */
057    private static final String IMAP_BAD = "BAD";
058
059    // Start of line for untagged replies
060    private static final String IMAP_UNTAGGED_PREFIX = "* ";
061
062    // Start of line for continuation replies
063    private static final String IMAP_CONTINUATION_PREFIX = "+";
064
065    /**
066     * Guards against Polynomial regular expression used on uncontrolled data.
067     * <ol>
068     * <li>the start of a line.</li>
069     * <li>letters, up to 80.</li>
070     * <li>a space.</li>
071     * <li>non-whitespace characters, up to 80, for example {@code OK}.</li>
072     * <li>up to 500 extra characters.</li>
073     * </ol>
074     */
075    private static final String TAGGED_RESPONSE = "^\\w{1,80} (\\S{1,80}).{0,500}";
076
077    /**
078     * Tag cannot contain: + ( ) { SP CTL % * " \ ]
079     */
080    private static final Pattern TAGGED_PATTERN = Pattern.compile(TAGGED_RESPONSE);
081
082    /**
083     * Guards against Polynomial regular expression used on uncontrolled data.
084     * <ol>
085     * <li>the start of a line, then a star, then a space.</li>
086     * <li>non-whitespace characters, up to 80, for example {@code OK}.</li>
087     * <li>up to 500 extra characters.</li>
088     * </ol>
089     */
090    private static final String UNTAGGED_RESPONSE = "^\\* (\\S{1,80}).{0,500}";
091
092    private static final Pattern UNTAGGED_PATTERN = Pattern.compile(UNTAGGED_RESPONSE);
093    private static final Pattern LITERAL_PATTERN = Pattern.compile("\\{(\\d+)\\}$"); // {dd}
094
095    /**
096     * Gets the String reply code - OK, NO, BAD - in a tagged response as an integer.
097     *
098     * @param line the tagged line to be checked
099     * @return {@link #OK} or {@link #NO} or {@link #BAD} or {@link #CONT}
100     * @throws IOException if the input has an unexpected format
101     */
102    public static int getReplyCode(final String line) throws IOException {
103        return getReplyCode(line, TAGGED_PATTERN);
104    }
105
106    // Helper method to process both tagged and untagged replies.
107    private static int getReplyCode(final String line, final Pattern pattern) throws IOException {
108        if (isContinuation(line)) {
109            return CONT;
110        }
111        final Matcher m = pattern.matcher(line);
112        if (m.matches()) { // TODO would lookingAt() be more efficient? If so, then drop trailing .* from patterns
113            final String code = m.group(1);
114            switch (code) {
115            case IMAP_OK:
116                return OK;
117            case IMAP_BAD:
118                return BAD;
119            case IMAP_NO:
120                return NO;
121            default:
122                break;
123            }
124        }
125        throw new MalformedServerReplyException("Received unexpected IMAP protocol response from server: '" + line + "'.");
126    }
127
128    /**
129     * Gets the String reply code - OK, NO, BAD - in an untagged response as an integer.
130     *
131     * @param line the untagged line to be checked
132     * @return {@link #OK} or {@link #NO} or {@link #BAD} or {@link #CONT}
133     * @throws IOException if the input has an unexpected format
134     */
135    public static int getUntaggedReplyCode(final String line) throws IOException {
136        return getReplyCode(line, UNTAGGED_PATTERN);
137    }
138
139    /**
140     * Tests whether the reply line is a continuation, i.e. starts with "+"
141     *
142     * @param replyCode the code to be checked
143     * @return {@code true} if the response was a continuation
144     */
145    public static boolean isContinuation(final int replyCode) {
146        return replyCode == CONT;
147    }
148
149    /**
150     * Tests whether if the reply line is a continuation, i.e. starts with "+"
151     *
152     * @param line the line to be checked
153     * @return {@code true} if the line is a continuation
154     */
155    public static boolean isContinuation(final String line) {
156        return line.startsWith(IMAP_CONTINUATION_PREFIX);
157    }
158
159    /**
160     * Tests whether whether the reply code indicates success or not
161     *
162     * @param replyCode the code to check
163     * @return {@code true} if the code equals {@link #OK}
164     */
165    public static boolean isSuccess(final int replyCode) {
166        return replyCode == OK;
167    }
168
169    /**
170     * Tests whether if the reply line is untagged - e.g. "* OK ..."
171     *
172     * @param line to be checked
173     * @return {@code true} if the line is untagged
174     */
175    public static boolean isUntagged(final String line) {
176        return line.startsWith(IMAP_UNTAGGED_PREFIX);
177    }
178
179    /**
180     * Checks if the line introduces a literal, i.e. ends with {dd}
181     *
182     * @param line the line to check
183     * @return the literal count, or -1 if there was no literal.
184     */
185    public static int literalCount(final String line) {
186        final Matcher m = LITERAL_PATTERN.matcher(line);
187        if (m.find()) {
188            return Integer.parseInt(m.group(1)); // Should always parse because we matched \d+
189        }
190        return -1;
191    }
192
193    /** Cannot be instantiated. */
194    private IMAPReply() {
195    }
196
197}
198