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 * https://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.commons.net.imap;
19
20 import java.io.IOException;
21 import java.util.regex.Matcher;
22 import java.util.regex.Pattern;
23
24 import org.apache.commons.net.MalformedServerReplyException;
25
26 /**
27 * Stores IMAP reply code constants.
28 */
29 public final class IMAPReply {
30 /** The reply code indicating success of an operation. */
31 public static final int OK = 0;
32
33 /** The reply code indicating failure of an operation. */
34 public static final int NO = 1;
35
36 /** The reply code indicating command rejection. */
37 public static final int BAD = 2;
38
39 /** The reply code indicating command continuation. */
40 public static final int CONT = 3;
41
42 /**
43 * 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
44 * on return.
45 *
46 * @since 3.4
47 */
48 public static final int PARTIAL = 3;
49
50 /** The IMAP reply String indicating success of an operation. */
51 private static final String IMAP_OK = "OK";
52
53 /** The IMAP reply String indicating failure of an operation. */
54 private static final String IMAP_NO = "NO";
55
56 /** The IMAP reply String indicating command rejection. */
57 private static final String IMAP_BAD = "BAD";
58
59 // Start of line for untagged replies
60 private static final String IMAP_UNTAGGED_PREFIX = "* ";
61
62 // Start of line for continuation replies
63 private static final String IMAP_CONTINUATION_PREFIX = "+";
64
65 /**
66 * Guards against Polynomial regular expression used on uncontrolled data.
67 * <ol>
68 * <li>the start of a line.</li>
69 * <li>letters, up to 80.</li>
70 * <li>a space.</li>
71 * <li>non-whitespace characters, up to 80, for example {@code OK}.</li>
72 * <li>up to 500 extra characters.</li>
73 * </ol>
74 */
75 private static final String TAGGED_RESPONSE = "^\\w{1,80} (\\S{1,80}).{0,500}";
76
77 /**
78 * Tag cannot contain: + ( ) { SP CTL % * " \ ]
79 */
80 private static final Pattern TAGGED_PATTERN = Pattern.compile(TAGGED_RESPONSE);
81
82 /**
83 * Guards against Polynomial regular expression used on uncontrolled data.
84 * <ol>
85 * <li>the start of a line, then a star, then a space.</li>
86 * <li>non-whitespace characters, up to 80, for example {@code OK}.</li>
87 * <li>up to 500 extra characters.</li>
88 * </ol>
89 */
90 private static final String UNTAGGED_RESPONSE = "^\\* (\\S{1,80}).{0,500}";
91
92 private static final Pattern UNTAGGED_PATTERN = Pattern.compile(UNTAGGED_RESPONSE);
93 private static final Pattern LITERAL_PATTERN = Pattern.compile("\\{(\\d+)\\}$"); // {dd}
94
95 /**
96 * Gets the String reply code - OK, NO, BAD - in a tagged response as an integer.
97 *
98 * @param line the tagged line to be checked
99 * @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