View Javadoc
1   /*
2    * Copyright 2016 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.smtp;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.ChannelHandlerContext;
20  import io.netty.handler.codec.DecoderException;
21  import io.netty.handler.codec.LineBasedFrameDecoder;
22  import io.netty.handler.codec.TooLongFrameException;
23  import io.netty.util.CharsetUtil;
24  import io.netty.util.internal.ObjectUtil;
25  import io.netty.util.internal.UnstableApi;
26  
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.List;
30  
31  /**
32   * Decoder for SMTP responses.
33   */
34  @UnstableApi
35  public final class SmtpResponseDecoder extends LineBasedFrameDecoder {
36  
37      private static final int DEFAULT_MAX_RESPONSE_SIZE = 64 * 1024;
38  
39      // Charged per accumulated line so the limit bounds retained heap rather than payload bytes alone.
40      private static final int DETAIL_ENTRY_OVERHEAD = 48;
41  
42      private final int maxResponseSize;
43  
44      private List<CharSequence> details;
45      private long responseSize;
46  
47      /**
48       * Creates a new instance that enforces the given {@code maxLineLength} and a default limit of 64 KiB on the
49       * accumulated size of a multi-line response.
50       *
51       * @param maxLineLength the maximum length of a single response line, in bytes.
52       */
53      public SmtpResponseDecoder(int maxLineLength) {
54          this(maxLineLength, DEFAULT_MAX_RESPONSE_SIZE);
55      }
56  
57      /**
58       * Creates a new instance that enforces the given {@code maxLineLength} and {@code maxResponseSize}.
59       * <p>
60       * The lines of a multi-line response are buffered until its terminating line arrives.
61       * {@code maxResponseSize} bounds that buffering. Once it is exceeded, decoding fails with a
62       * {@link TooLongFrameException}. The limit is approximate and tracks retained memory rather than bytes
63       * received, as it charges each buffered line a fixed object overhead in addition to its detail bytes, and
64       * ignores the response code, the separator and the terminating line.
65       *
66       * @param maxLineLength   the maximum length of a single response line, in bytes.
67       * @param maxResponseSize the maximum accumulated size of a multi-line response, in bytes.
68       */
69      public SmtpResponseDecoder(int maxLineLength, int maxResponseSize) {
70          super(maxLineLength);
71          this.maxResponseSize = ObjectUtil.checkPositive(maxResponseSize, "maxResponseSize");
72      }
73  
74      @Override
75      protected SmtpResponse decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
76          ByteBuf frame = (ByteBuf) super.decode(ctx, buffer);
77          if (frame == null) {
78              // No full line received yet.
79              return null;
80          }
81          try {
82              final int readable = frame.readableBytes();
83              final int readerIndex = frame.readerIndex();
84              if (readable < 4) {
85                  reset();
86                  throw newDecoderException(buffer, readerIndex, readable);
87              }
88              final int code = parseCode(frame);
89              if (code < 0) {
90                  reset();
91                  throw newDecoderException(buffer, readerIndex, readable);
92              }
93              final int separator = frame.readByte();
94              final CharSequence detail = frame.isReadable() ? frame.toString(CharsetUtil.US_ASCII) : null;
95  
96              List<CharSequence> details = this.details;
97  
98              switch (separator) {
99              case ' ':
100                 // Marks the end of a response.
101                 reset();
102                 if (details != null) {
103                     if (detail != null) {
104                         details.add(detail);
105                     }
106                 } else {
107                     if (detail == null) {
108                         details = Collections.emptyList();
109                     } else {
110                         details = Collections.singletonList(detail);
111                     }
112                 }
113                 return new DefaultSmtpResponse(code, details);
114             case '-':
115                 // Multi-line response.
116                 if (detail != null) {
117                     responseSize += (long) detail.length() + DETAIL_ENTRY_OVERHEAD;
118                     if (responseSize > maxResponseSize) {
119                         reset();
120                         throw new TooLongFrameException("SMTP response exceeds " + maxResponseSize + " bytes");
121                     }
122                     if (details == null) {
123                         // Using initial capacity as it is very unlikely that we will receive a multi-line response
124                         // with more then 3 lines.
125                         this.details = details = new ArrayList<CharSequence>(4);
126                     }
127                     details.add(detail);
128                 }
129                 break;
130             default:
131                 reset();
132                 throw newDecoderException(buffer, readerIndex, readable);
133             }
134         } finally {
135             frame.release();
136         }
137         return null;
138     }
139 
140     private void reset() {
141         this.details = null;
142         this.responseSize = 0;
143     }
144 
145     private static DecoderException newDecoderException(ByteBuf buffer, int readerIndex, int readable) {
146         return new DecoderException(
147                 "Received invalid line: '" + buffer.toString(readerIndex, readable, CharsetUtil.US_ASCII) + '\'');
148     }
149 
150     /**
151      * Parses the io.netty.handler.codec.smtp code without any allocation, which is three digits.
152      *
153      * @return the code, or {@code -1} if one of the three bytes is not a digit.
154      */
155     private static int parseCode(ByteBuf buffer) {
156         final int first = parseNumber(buffer.readByte());
157         final int second = parseNumber(buffer.readByte());
158         final int third = parseNumber(buffer.readByte());
159         if (first < 0 || second < 0 || third < 0) {
160             return -1;
161         }
162         return first * 100 + second * 10 + third;
163     }
164 
165     private static int parseNumber(byte b) {
166         return Character.digit((char) b, 10);
167     }
168 }