001    /****************************************************************
002     * Licensed to the Apache Software Foundation (ASF) under one   *
003     * or more contributor license agreements.  See the NOTICE file *
004     * distributed with this work for additional information        *
005     * regarding copyright ownership.  The ASF licenses this file   *
006     * to you under the Apache License, Version 2.0 (the            *
007     * "License"); you may not use this file except in compliance   *
008     * with the License.  You may obtain a copy of the License at   *
009     *                                                              *
010     *   http://www.apache.org/licenses/LICENSE-2.0                 *
011     *                                                              *
012     * Unless required by applicable law or agreed to in writing,   *
013     * software distributed under the License is distributed on an  *
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015     * KIND, either express or implied.  See the License for the    *
016     * specific language governing permissions and limitations      *
017     * under the License.                                           *
018     ****************************************************************/
019    
020    package org.apache.james.mime4j.message;
021    
022    import java.io.IOException;
023    import java.io.OutputStream;
024    
025    import org.apache.james.mime4j.codec.CodecUtil;
026    import org.apache.james.mime4j.dom.BinaryBody;
027    import org.apache.james.mime4j.dom.Body;
028    import org.apache.james.mime4j.dom.Entity;
029    import org.apache.james.mime4j.dom.Header;
030    import org.apache.james.mime4j.dom.Message;
031    import org.apache.james.mime4j.dom.MessageWriter;
032    import org.apache.james.mime4j.dom.Multipart;
033    import org.apache.james.mime4j.dom.SingleBody;
034    import org.apache.james.mime4j.dom.field.ContentTypeField;
035    import org.apache.james.mime4j.dom.field.FieldName;
036    import org.apache.james.mime4j.stream.Field;
037    import org.apache.james.mime4j.util.ByteArrayBuffer;
038    import org.apache.james.mime4j.util.ByteSequence;
039    import org.apache.james.mime4j.util.ContentUtil;
040    import org.apache.james.mime4j.util.MimeUtil;
041    
042    /**
043     * Default implementation of {@link MessageWriter}.
044     */
045    public class DefaultMessageWriter implements MessageWriter {
046    
047        private static final byte[] CRLF = { '\r', '\n' };
048        private static final byte[] DASHES = { '-', '-' };
049    
050        /**
051         * Protected constructor prevents direct instantiation.
052         */
053        public DefaultMessageWriter() {
054        }
055    
056        /**
057         * Write the specified <code>Body</code> to the specified
058         * <code>OutputStream</code>.
059         *
060         * @param body
061         *            the <code>Body</code> to write.
062         * @param out
063         *            the OutputStream to write to.
064         * @throws IOException
065         *             if an I/O error occurs.
066         */
067        public void writeBody(Body body, OutputStream out) throws IOException {
068            if (body instanceof Message) {
069                writeEntity((Message) body, out);
070            } else if (body instanceof Multipart) {
071                writeMultipart((Multipart) body, out);
072            } else if (body instanceof SingleBody) {
073                ((SingleBody) body).writeTo(out);
074            } else
075                throw new IllegalArgumentException("Unsupported body class");
076        }
077    
078        /**
079         * Write the specified <code>Entity</code> to the specified
080         * <code>OutputStream</code>.
081         *
082         * @param entity
083         *            the <code>Entity</code> to write.
084         * @param out
085         *            the OutputStream to write to.
086         * @throws IOException
087         *             if an I/O error occurs.
088         */
089        public void writeEntity(Entity entity, OutputStream out) throws IOException {
090            final Header header = entity.getHeader();
091            if (header == null)
092                throw new IllegalArgumentException("Missing header");
093    
094            writeHeader(header, out);
095    
096            final Body body = entity.getBody();
097            if (body == null)
098                throw new IllegalArgumentException("Missing body");
099    
100            boolean binaryBody = body instanceof BinaryBody;
101            OutputStream encOut = encodeStream(out, entity
102                    .getContentTransferEncoding(), binaryBody);
103    
104            writeBody(body, encOut);
105    
106            // close if wrapped (base64 or quoted-printable)
107            if (encOut != out)
108                encOut.close();
109        }
110    
111        /**
112         * Write the specified <code>Message</code> to the specified
113         * <code>OutputStream</code>.
114         *
115         * @param message
116         *            the <code>Message</code> to write.
117         * @param out
118         *            the OutputStream to write to.
119         * @throws IOException
120         *             if an I/O error occurs.
121         */
122        public void writeMessage(Message message, OutputStream out) throws IOException {
123            writeEntity(message, out);
124        }
125    
126        /**
127         * Write the specified <code>Multipart</code> to the specified
128         * <code>OutputStream</code>.
129         *
130         * @param multipart
131         *            the <code>Multipart</code> to write.
132         * @param out
133         *            the OutputStream to write to.
134         * @throws IOException
135         *             if an I/O error occurs.
136         */
137        public void writeMultipart(Multipart multipart, OutputStream out)
138                throws IOException {
139            ContentTypeField contentType = getContentType(multipart);
140    
141            ByteSequence boundary = getBoundary(contentType);
142    
143            ByteSequence preamble;
144            ByteSequence epilogue;
145            if (multipart instanceof MultipartImpl) {
146                preamble = ((MultipartImpl) multipart).getPreambleRaw();
147                epilogue = ((MultipartImpl) multipart).getEpilogueRaw();
148            } else {
149                preamble = multipart.getPreamble() != null ? ContentUtil.encode(multipart.getPreamble()) : null;
150                epilogue = multipart.getEpilogue() != null ? ContentUtil.encode(multipart.getEpilogue()) : null;
151            }
152            if (preamble != null) {
153                writeBytes(preamble, out);
154                out.write(CRLF);
155            }
156    
157            for (Entity bodyPart : multipart.getBodyParts()) {
158                out.write(DASHES);
159                writeBytes(boundary, out);
160                out.write(CRLF);
161    
162                writeEntity(bodyPart, out);
163                out.write(CRLF);
164            }
165    
166            out.write(DASHES);
167            writeBytes(boundary, out);
168            out.write(DASHES);
169            out.write(CRLF);
170            if (epilogue != null) {
171                writeBytes(epilogue, out);
172            }
173        }
174    
175        /**
176         * Write the specified <code>Field</code> to the specified
177         * <code>OutputStream</code>.
178         *
179         * @param field
180         *            the <code>Field</code> to write.
181         * @param out
182         *            the OutputStream to write to.
183         * @throws IOException
184         *             if an I/O error occurs.
185         */
186        public void writeField(Field field, OutputStream out) throws IOException {
187            ByteSequence raw = field.getRaw();
188            if (raw == null) {
189                StringBuilder buf = new StringBuilder();
190                buf.append(field.getName());
191                buf.append(": ");
192                String body = field.getBody();
193                if (body != null) {
194                    buf.append(body);
195                }
196                raw = ContentUtil.encode(MimeUtil.fold(buf.toString(), 0));
197            }
198            writeBytes(raw, out);
199            out.write(CRLF);
200        }
201    
202        /**
203         * Write the specified <code>Header</code> to the specified
204         * <code>OutputStream</code>.
205         *
206         * @param header
207         *            the <code>Header</code> to write.
208         * @param out
209         *            the OutputStream to write to.
210         * @throws IOException
211         *             if an I/O error occurs.
212         */
213        public void writeHeader(Header header, OutputStream out) throws IOException {
214            for (Field field : header) {
215                writeField(field, out);
216            }
217    
218            out.write(CRLF);
219        }
220    
221        protected OutputStream encodeStream(OutputStream out, String encoding,
222                boolean binaryBody) throws IOException {
223            if (MimeUtil.isBase64Encoding(encoding)) {
224                return CodecUtil.wrapBase64(out);
225            } else if (MimeUtil.isQuotedPrintableEncoded(encoding)) {
226                return CodecUtil.wrapQuotedPrintable(out, binaryBody);
227            } else {
228                return out;
229            }
230        }
231    
232        private ContentTypeField getContentType(Multipart multipart) {
233            Entity parent = multipart.getParent();
234            if (parent == null)
235                throw new IllegalArgumentException(
236                        "Missing parent entity in multipart");
237    
238            Header header = parent.getHeader();
239            if (header == null)
240                throw new IllegalArgumentException(
241                        "Missing header in parent entity");
242    
243            ContentTypeField contentType = (ContentTypeField) header
244                    .getField(FieldName.CONTENT_TYPE);
245            if (contentType == null)
246                throw new IllegalArgumentException(
247                        "Content-Type field not specified");
248    
249            return contentType;
250        }
251    
252        private ByteSequence getBoundary(ContentTypeField contentType) {
253            String boundary = contentType.getBoundary();
254            if (boundary == null)
255                throw new IllegalArgumentException(
256                        "Multipart boundary not specified. Mime-Type: "+contentType.getMimeType()+", Raw: "+contentType.toString());
257    
258            return ContentUtil.encode(boundary);
259        }
260    
261        private void writeBytes(ByteSequence byteSequence, OutputStream out)
262                throws IOException {
263            if (byteSequence instanceof ByteArrayBuffer) {
264                ByteArrayBuffer bab = (ByteArrayBuffer) byteSequence;
265                out.write(bab.buffer(), 0, bab.length());
266            } else {
267                out.write(byteSequence.toByteArray());
268            }
269        }
270    
271    }