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