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}