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.field.ContentTypeField;
027    import org.apache.james.mime4j.field.FieldName;
028    import org.apache.james.mime4j.parser.Field;
029    import org.apache.james.mime4j.util.ByteArrayBuffer;
030    import org.apache.james.mime4j.util.ByteSequence;
031    import org.apache.james.mime4j.util.ContentUtil;
032    import org.apache.james.mime4j.util.MimeUtil;
033    
034    /**
035     * Writes a message (or a part of a message) to an output stream.
036     * <p>
037     * This class cannot be instantiated; instead the static instance
038     * {@link #DEFAULT} implements the default strategy for writing a message.
039     * <p>
040     * This class may be subclassed to implement custom strategies for writing
041     * messages.
042     */
043    public class MessageWriter {
044    
045        private static final byte[] CRLF = { '\r', '\n' };
046        private static final byte[] DASHES = { '-', '-' };
047    
048        /**
049         * The default message writer.
050         */
051        public static final MessageWriter DEFAULT = new MessageWriter();
052    
053        /**
054         * Protected constructor prevents direct instantiation.
055         */
056        protected MessageWriter() {
057        }
058    
059        /**
060         * Write the specified <code>Body</code> to the specified
061         * <code>OutputStream</code>.
062         * 
063         * @param body
064         *            the <code>Body</code> to write.
065         * @param out
066         *            the OutputStream to write to.
067         * @throws IOException
068         *             if an I/O error occurs.
069         */
070        public void writeBody(Body body, OutputStream out) throws IOException {
071            if (body instanceof Message) {
072                writeEntity((Message) body, out);
073            } else if (body instanceof Multipart) {
074                writeMultipart((Multipart) body, out);
075            } else if (body instanceof SingleBody) {
076                ((SingleBody) body).writeTo(out);
077            } else
078                throw new IllegalArgumentException("Unsupported body class");
079        }
080    
081        /**
082         * Write the specified <code>Entity</code> to the specified
083         * <code>OutputStream</code>.
084         * 
085         * @param entity
086         *            the <code>Entity</code> to write.
087         * @param out
088         *            the OutputStream to write to.
089         * @throws IOException
090         *             if an I/O error occurs.
091         */
092        public void writeEntity(Entity entity, OutputStream out) throws IOException {
093            final Header header = entity.getHeader();
094            if (header == null)
095                throw new IllegalArgumentException("Missing header");
096    
097            writeHeader(header, out);
098    
099            final Body body = entity.getBody();
100            if (body == null)
101                throw new IllegalArgumentException("Missing body");
102    
103            boolean binaryBody = body instanceof BinaryBody;
104            OutputStream encOut = encodeStream(out, entity
105                    .getContentTransferEncoding(), binaryBody);
106    
107            writeBody(body, encOut);
108    
109            // close if wrapped (base64 or quoted-printable)
110            if (encOut != out)
111                encOut.close();
112        }
113    
114        /**
115         * Write the specified <code>Multipart</code> to the specified
116         * <code>OutputStream</code>.
117         * 
118         * @param multipart
119         *            the <code>Multipart</code> to write.
120         * @param out
121         *            the OutputStream to write to.
122         * @throws IOException
123         *             if an I/O error occurs.
124         */
125        public void writeMultipart(Multipart multipart, OutputStream out)
126                throws IOException {
127            ContentTypeField contentType = getContentType(multipart);
128    
129            ByteSequence boundary = getBoundary(contentType);
130    
131            writeBytes(multipart.getPreambleRaw(), out);
132            out.write(CRLF);
133    
134            for (BodyPart bodyPart : multipart.getBodyParts()) {
135                out.write(DASHES);
136                writeBytes(boundary, out);
137                out.write(CRLF);
138    
139                writeEntity(bodyPart, out);
140                out.write(CRLF);
141            }
142    
143            out.write(DASHES);
144            writeBytes(boundary, out);
145            out.write(DASHES);
146            out.write(CRLF);
147    
148            writeBytes(multipart.getEpilogueRaw(), out);
149        }
150    
151        /**
152         * Write the specified <code>Header</code> to the specified
153         * <code>OutputStream</code>.
154         * 
155         * @param header
156         *            the <code>Header</code> to write.
157         * @param out
158         *            the OutputStream to write to.
159         * @throws IOException
160         *             if an I/O error occurs.
161         */
162        public void writeHeader(Header header, OutputStream out) throws IOException {
163            for (Field field : header) {
164                writeBytes(field.getRaw(), out);
165                out.write(CRLF);
166            }
167    
168            out.write(CRLF);
169        }
170    
171        protected OutputStream encodeStream(OutputStream out, String encoding,
172                boolean binaryBody) throws IOException {
173            if (MimeUtil.isBase64Encoding(encoding)) {
174                return CodecUtil.wrapBase64(out);
175            } else if (MimeUtil.isQuotedPrintableEncoded(encoding)) {
176                return CodecUtil.wrapQuotedPrintable(out, binaryBody);
177            } else {
178                return out;
179            }
180        }
181    
182        private ContentTypeField getContentType(Multipart multipart) {
183            Entity parent = multipart.getParent();
184            if (parent == null)
185                throw new IllegalArgumentException(
186                        "Missing parent entity in multipart");
187    
188            Header header = parent.getHeader();
189            if (header == null)
190                throw new IllegalArgumentException(
191                        "Missing header in parent entity");
192    
193            ContentTypeField contentType = (ContentTypeField) header
194                    .getField(FieldName.CONTENT_TYPE);
195            if (contentType == null)
196                throw new IllegalArgumentException(
197                        "Content-Type field not specified");
198    
199            return contentType;
200        }
201    
202        private ByteSequence getBoundary(ContentTypeField contentType) {
203            String boundary = contentType.getBoundary();
204            if (boundary == null)
205                throw new IllegalArgumentException(
206                        "Multipart boundary not specified");
207    
208            return ContentUtil.encode(boundary);
209        }
210    
211        private void writeBytes(ByteSequence byteSequence, OutputStream out)
212                throws IOException {
213            if (byteSequence instanceof ByteArrayBuffer) {
214                ByteArrayBuffer bab = (ByteArrayBuffer) byteSequence;
215                out.write(bab.buffer(), 0, bab.length());
216            } else {
217                out.write(byteSequence.toByteArray());
218            }
219        }
220    
221    }