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 < 3) {
85                  reset();
86                  throw newDecoderException(buffer, readerIndex, readable);
87              }
88              final int code = parseCode(frame);
89              final int separator = frame.readByte();
90              final CharSequence detail = frame.isReadable() ? frame.toString(CharsetUtil.US_ASCII) : null;
91  
92              List<CharSequence> details = this.details;
93  
94              switch (separator) {
95              case ' ':
96                  // Marks the end of a response.
97                  reset();
98                  if (details != null) {
99                      if (detail != null) {
100                         details.add(detail);
101                     }
102                 } else {
103                     if (detail == null) {
104                         details = Collections.emptyList();
105                     } else {
106                         details = Collections.singletonList(detail);
107                     }
108                 }
109                 return new DefaultSmtpResponse(code, details);
110             case '-':
111                 // Multi-line response.
112                 if (detail != null) {
113                     responseSize += (long) detail.length() + DETAIL_ENTRY_OVERHEAD;
114                     if (responseSize > maxResponseSize) {
115                         reset();
116                         throw new TooLongFrameException("SMTP response exceeds " + maxResponseSize + " bytes");
117                     }
118                     if (details == null) {
119                         // Using initial capacity as it is very unlikely that we will receive a multi-line response
120                         // with more then 3 lines.
121                         this.details = details = new ArrayList<CharSequence>(4);
122                     }
123                     details.add(detail);
124                 }
125                 break;
126             default:
127                 reset();
128                 throw newDecoderException(buffer, readerIndex, readable);
129             }
130         } finally {
131             frame.release();
132         }
133         return null;
134     }
135 
136     private void reset() {
137         this.details = null;
138         this.responseSize = 0;
139     }
140 
141     private static DecoderException newDecoderException(ByteBuf buffer, int readerIndex, int readable) {
142         return new DecoderException(
143                 "Received invalid line: '" + buffer.toString(readerIndex, readable, CharsetUtil.US_ASCII) + '\'');
144     }
145 
146     /**
147      * Parses the io.netty.handler.codec.smtp code without any allocation, which is three digits.
148      */
149     private static int parseCode(ByteBuf buffer) {
150         final int first = parseNumber(buffer.readByte()) * 100;
151         final int second = parseNumber(buffer.readByte()) * 10;
152         final int third = parseNumber(buffer.readByte());
153         return first + second + third;
154     }
155 
156     private static int parseNumber(byte b) {
157         return Character.digit((char) b, 10);
158     }
159 }