1 /****************************************************************
2 * Licensed to the Apache Software Foundation (ASF) under one *
3 * or more contributor license agreements. See the NOTICE file *
4 * distributed with this work for additional information *
5 * regarding copyright ownership. The ASF licenses this file *
6 * to you under the Apache License, Version 2.0 (the *
7 * "License"); you may not use this file except in compliance *
8 * with the License. You may obtain a copy of the License at *
9 * *
10 * http://www.apache.org/licenses/LICENSE-2.0 *
11 * *
12 * Unless required by applicable law or agreed to in writing, *
13 * software distributed under the License is distributed on an *
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15 * KIND, either express or implied. See the License for the *
16 * specific language governing permissions and limitations *
17 * under the License. *
18 ****************************************************************/
19
20 package org.apache.james.util.stream;
21
22 import java.io.FilterOutputStream;
23 import java.io.IOException;
24 import java.io.OutputStream;
25
26 /**
27 * A Filter for use with SMTP or other protocols in which lines must end with
28 * CRLF. Converts every "isolated" occourency of \r or \n with \r\n
29 *
30 * RFC 2821 #2.3.7 mandates that line termination is CRLF, and that CR and LF
31 * must not be transmitted except in that pairing. If we get a naked LF, convert
32 * to CRLF.
33 *
34 */
35 public class CRLFOutputStream extends FilterOutputStream {
36
37 /**
38 * Counter for number of last (0A or 0D).
39 */
40 protected int statusLast;
41
42 protected final static int LAST_WAS_OTHER = 0;
43
44 protected final static int LAST_WAS_CR = 1;
45
46 protected final static int LAST_WAS_LF = 2;
47
48 protected boolean startOfLine = true;
49
50 /**
51 * Constructor that wraps an OutputStream.
52 *
53 * @param out
54 * the OutputStream to be wrapped
55 */
56 public CRLFOutputStream(OutputStream out) {
57 super(out);
58 statusLast = LAST_WAS_LF; // we already assume a CRLF at beginning
59 // (otherwise TOP would not work correctly
60 // !)
61 }
62
63 /**
64 * Writes a byte to the stream Fixes any naked CR or LF to the RFC 2821
65 * mandated CFLF pairing.
66 *
67 * @param b
68 * the byte to write
69 *
70 * @throws IOException
71 * if an error occurs writing the byte
72 */
73 public void write(int b) throws IOException {
74 switch (b) {
75 case '\r':
76 out.write('\r');
77 out.write('\n');
78 startOfLine = true;
79 statusLast = LAST_WAS_CR;
80 break;
81 case '\n':
82 if (statusLast != LAST_WAS_CR) {
83 out.write('\r');
84 out.write('\n');
85 startOfLine = true;
86 }
87 statusLast = LAST_WAS_LF;
88 break;
89 default:
90 // we're no longer at the start of a line
91 out.write(b);
92 startOfLine = false;
93 statusLast = LAST_WAS_OTHER;
94 break;
95 }
96 }
97
98 /**
99 * Provides an extension point for ExtraDotOutputStream to be able to add dots
100 * at the beginning of new lines.
101 *
102 * @see java.io.FilterOutputStream#write(byte[], int, int)
103 */
104 protected void writeChunk(byte buffer[], int offset, int length) throws IOException {
105 out.write(buffer, offset, length);
106 }
107
108 /**
109 * @see java.io.FilterOutputStream#write(byte[], int, int)
110 */
111 public synchronized void write(byte buffer[], int offset, int length)
112 throws IOException {
113 /* optimized */
114 int lineStart = offset;
115 for (int i = offset; i < length + offset; i++) {
116 switch(buffer[i]) {
117 case '\r':
118 // CR case. Write down the last line
119 // and position the new lineStart at the next char
120 writeChunk(buffer, lineStart, i - lineStart);
121 out.write('\r');
122 out.write('\n');
123 startOfLine = true;
124 lineStart = i + 1;
125 statusLast = LAST_WAS_CR;
126 break;
127 case '\n':
128 if (statusLast != LAST_WAS_CR) {
129 writeChunk(buffer, lineStart, i - lineStart);
130 out.write('\r');
131 out.write('\n');
132 startOfLine = true;
133 }
134 lineStart = i + 1;
135 statusLast = LAST_WAS_LF;
136 break;
137 default:
138 statusLast = LAST_WAS_OTHER;
139 }
140 }
141 if (length + offset > lineStart) {
142 writeChunk(buffer, lineStart, length + offset - lineStart);
143 startOfLine = false;
144 }
145 }
146
147
148 /**
149 * Ensure that the stream is CRLF terminated.
150 *
151 * @throws IOException
152 * if an error occurs writing the byte
153 */
154 public void checkCRLFTerminator() throws IOException {
155 if (statusLast == LAST_WAS_OTHER) {
156 out.write('\r');
157 out.write('\n');
158 statusLast = LAST_WAS_CR;
159 }
160 }
161 }