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
020package org.apache.james.mime4j.message;
021
022import java.io.IOException;
023import java.io.OutputStream;
024
025import org.apache.james.mime4j.codec.CodecUtil;
026import org.apache.james.mime4j.dom.BinaryBody;
027import org.apache.james.mime4j.dom.Body;
028import org.apache.james.mime4j.dom.Entity;
029import org.apache.james.mime4j.dom.Header;
030import org.apache.james.mime4j.dom.Message;
031import org.apache.james.mime4j.dom.MessageWriter;
032import org.apache.james.mime4j.dom.Multipart;
033import org.apache.james.mime4j.dom.SingleBody;
034import org.apache.james.mime4j.dom.field.ContentTypeField;
035import org.apache.james.mime4j.dom.field.FieldName;
036import org.apache.james.mime4j.stream.Field;
037import org.apache.james.mime4j.util.ByteArrayBuffer;
038import org.apache.james.mime4j.util.ByteSequence;
039import org.apache.james.mime4j.util.ContentUtil;
040import org.apache.james.mime4j.util.MimeUtil;
041
042/**
043 * Default implementation of {@link MessageWriter}.
044 */
045public 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}