001 /**
002 *
003 * Copyright 2003-2004 The Apache Software Foundation
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018 package javax.mail.internet;
019
020 import java.io.ByteArrayInputStream;
021 import java.io.ByteArrayOutputStream;
022 import java.io.IOException;
023 import java.io.InputStream;
024 import java.io.OutputStream;
025 import java.io.UnsupportedEncodingException;
026 import java.util.Enumeration;
027
028 import javax.activation.DataHandler;
029 import javax.mail.BodyPart;
030 import javax.mail.MessagingException;
031 import javax.mail.Multipart;
032 import javax.mail.Part;
033 import javax.mail.internet.HeaderTokenizer.Token;
034 import javax.swing.text.AbstractDocument.Content;
035
036 import org.apache.geronimo.mail.util.ASCIIUtil;
037 import org.apache.geronimo.mail.util.SessionUtil;
038
039 /**
040 * @version $Rev: 412720 $ $Date: 2006-06-08 03:52:48 -0700 (Thu, 08 Jun 2006) $
041 */
042 public class MimeBodyPart extends BodyPart implements MimePart {
043 // constants for accessed properties
044 private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
045 private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
046 private static final String MIME_SETDEFAULTTEXTCHARSET = "mail.mime.setdefaulttextcharset";
047 private static final String MIME_SETCONTENTTYPEFILENAME = "mail.mime.setcontenttypefilename";
048
049
050 /**
051 * The {@link DataHandler} for this Message's content.
052 */
053 protected DataHandler dh;
054 /**
055 * This message's content (unless sourced from a SharedInputStream).
056 */
057 protected byte content[];
058 /**
059 * If the data for this message was supplied by a {@link SharedInputStream}
060 * then this is another such stream representing the content of this message;
061 * if this field is non-null, then {@link #content} will be null.
062 */
063 protected InputStream contentStream;
064 /**
065 * This message's headers.
066 */
067 protected InternetHeaders headers;
068
069 public MimeBodyPart() {
070 headers = new InternetHeaders();
071 }
072
073 public MimeBodyPart(InputStream in) throws MessagingException {
074 headers = new InternetHeaders(in);
075 ByteArrayOutputStream baos = new ByteArrayOutputStream();
076 byte[] buffer = new byte[1024];
077 int count;
078 try {
079 while((count = in.read(buffer, 0, 1024)) != -1)
080 baos.write(buffer, 0, count);
081 } catch (IOException e) {
082 throw new MessagingException(e.toString(),e);
083 }
084 content = baos.toByteArray();
085 }
086
087 public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException {
088 this.headers = headers;
089 this.content = content;
090 }
091
092 /**
093 * Return the content size of this message. This is obtained
094 * either from the size of the content field (if available) or
095 * from the contentStream, IFF the contentStream returns a positive
096 * size. Returns -1 if the size is not available.
097 *
098 * @return Size of the content in bytes.
099 * @exception MessagingException
100 */
101 public int getSize() throws MessagingException {
102 if (content != null) {
103 return content.length;
104 }
105 if (contentStream != null) {
106 try {
107 int size = contentStream.available();
108 if (size > 0) {
109 return size;
110 }
111 } catch (IOException e) {
112 }
113 }
114 return -1;
115 }
116
117 public int getLineCount() throws MessagingException {
118 return -1;
119 }
120
121 public String getContentType() throws MessagingException {
122 String value = getSingleHeader("Content-Type");
123 if (value == null) {
124 value = "text/plain";
125 }
126 return value;
127 }
128
129 /**
130 * Tests to see if this message has a mime-type match with the
131 * given type name.
132 *
133 * @param type The tested type name.
134 *
135 * @return If this is a type match on the primary and secondare portion of the types.
136 * @exception MessagingException
137 */
138 public boolean isMimeType(String type) throws MessagingException {
139 return new ContentType(getContentType()).match(type);
140 }
141
142 /**
143 * Retrieve the message "Content-Disposition" header field.
144 * This value represents how the part should be represented to
145 * the user.
146 *
147 * @return The string value of the Content-Disposition field.
148 * @exception MessagingException
149 */
150 public String getDisposition() throws MessagingException {
151 String disp = getSingleHeader("Content-Disposition");
152 if (disp != null) {
153 return new ContentDisposition(disp).getDisposition();
154 }
155 return null;
156 }
157
158 /**
159 * Set a new dispostion value for the "Content-Disposition" field.
160 * If the new value is null, the header is removed.
161 *
162 * @param disposition
163 * The new disposition value.
164 *
165 * @exception MessagingException
166 */
167 public void setDisposition(String disposition) throws MessagingException {
168 if (disposition == null) {
169 removeHeader("Content-Disposition");
170 }
171 else {
172 // the disposition has parameters, which we'll attempt to preserve in any existing header.
173 String currentHeader = getSingleHeader("Content-Disposition");
174 if (currentHeader != null) {
175 ContentDisposition content = new ContentDisposition(currentHeader);
176 content.setDisposition(disposition);
177 setHeader("Content-Disposition", content.toString());
178 }
179 else {
180 // set using the raw string.
181 setHeader("Content-Disposition", disposition);
182 }
183 }
184 }
185
186 /**
187 * Retrieves the current value of the "Content-Transfer-Encoding"
188 * header. Returns null if the header does not exist.
189 *
190 * @return The current header value or null.
191 * @exception MessagingException
192 */
193 public String getEncoding() throws MessagingException {
194 // this might require some parsing to sort out.
195 String encoding = getSingleHeader("Content-Transfer-Encoding");
196 if (encoding != null) {
197 // we need to parse this into ATOMs and other constituent parts. We want the first
198 // ATOM token on the string.
199 HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME);
200
201 Token token = tokenizer.next();
202 while (token.getType() != Token.EOF) {
203 // if this is an ATOM type, return it.
204 if (token.getType() == Token.ATOM) {
205 return token.getValue();
206 }
207 }
208 // not ATOMs found, just return the entire header value....somebody might be able to make sense of
209 // this.
210 return encoding;
211 }
212 // no header, nothing to return.
213 return null;
214 }
215
216
217 /**
218 * Retrieve the value of the "Content-ID" header. Returns null
219 * if the header does not exist.
220 *
221 * @return The current header value or null.
222 * @exception MessagingException
223 */
224 public String getContentID() throws MessagingException {
225 return getSingleHeader("Content-ID");
226 }
227
228 public void setContentID(String cid) throws MessagingException {
229 setOrRemoveHeader("Content-ID", cid);
230 }
231
232 public String getContentMD5() throws MessagingException {
233 return getSingleHeader("Content-MD5");
234 }
235
236 public void setContentMD5(String md5) throws MessagingException {
237 setHeader("Content-MD5", md5);
238 }
239
240 public String[] getContentLanguage() throws MessagingException {
241 return getHeader("Content-Language");
242 }
243
244 public void setContentLanguage(String[] languages) throws MessagingException {
245 if (languages == null) {
246 removeHeader("Content-Language");
247 } else if (languages.length == 1) {
248 setHeader("Content-Language", languages[0]);
249 } else {
250 StringBuffer buf = new StringBuffer(languages.length * 20);
251 buf.append(languages[0]);
252 for (int i = 1; i < languages.length; i++) {
253 buf.append(',').append(languages[i]);
254 }
255 setHeader("Content-Language", buf.toString());
256 }
257 }
258
259 public String getDescription() throws MessagingException {
260 String description = getSingleHeader("Content-Description");
261 if (description != null) {
262 try {
263 // this could be both folded and encoded. Return this to usable form.
264 return MimeUtility.decodeText(ASCIIUtil.unfold(description));
265 } catch (UnsupportedEncodingException e) {
266 // ignore
267 }
268 }
269 // return the raw version for any errors.
270 return description;
271 }
272
273 public void setDescription(String description) throws MessagingException {
274 setDescription(description, null);
275 }
276
277 public void setDescription(String description, String charset) throws MessagingException {
278 if (description == null) {
279 removeHeader("Content-Description");
280 }
281 else {
282 try {
283 setHeader("Content-Description", ASCIIUtil.fold(21, MimeUtility.encodeText(description, charset, null)));
284 } catch (UnsupportedEncodingException e) {
285 throw new MessagingException(e.getMessage(), e);
286 }
287 }
288 }
289
290 public String getFileName() throws MessagingException {
291 // see if there is a disposition. If there is, parse off the filename parameter.
292 String disposition = getSingleHeader("Content-Disposition");
293 String filename = null;
294
295 if (disposition != null) {
296 filename = new ContentDisposition(disposition).getParameter("filename");
297 }
298
299 // if there's no filename on the disposition, there might be a name parameter on a
300 // Content-Type header.
301 if (filename == null) {
302 String type = getSingleHeader("Content-Type");
303 if (type != null) {
304 try {
305 filename = new ContentType(type).getParameter("name");
306 } catch (ParseException e) {
307 }
308 }
309 }
310 // if we have a name, we might need to decode this if an additional property is set.
311 if (filename != null && SessionUtil.getBooleanProperty(MIME_DECODEFILENAME, false)) {
312 try {
313 filename = MimeUtility.decodeText(filename);
314 } catch (UnsupportedEncodingException e) {
315 throw new MessagingException("Unable to decode filename", e);
316 }
317 }
318
319 return filename;
320 }
321
322
323 public void setFileName(String name) throws MessagingException {
324 // there's an optional session property that requests file name encoding...we need to process this before
325 // setting the value.
326 if (name != null && SessionUtil.getBooleanProperty(MIME_ENCODEFILENAME, false)) {
327 try {
328 name = MimeUtility.encodeText(name);
329 } catch (UnsupportedEncodingException e) {
330 throw new MessagingException("Unable to encode filename", e);
331 }
332 }
333
334 // get the disposition string.
335 String disposition = getDisposition();
336 // if not there, then this is an attachment.
337 if (disposition == null) {
338 disposition = Part.ATTACHMENT;
339 }
340
341 // now create a disposition object and set the parameter.
342 ContentDisposition contentDisposition = new ContentDisposition(disposition);
343 contentDisposition.setParameter("filename", name);
344
345 // serialize this back out and reset.
346 setDisposition(contentDisposition.toString());
347 setHeader("Content-Disposition", contentDisposition.toString());
348
349 // The Sun implementation appears to update the Content-type name parameter too, based on
350 // another system property
351 if (SessionUtil.getBooleanProperty(MIME_SETCONTENTTYPEFILENAME, true)) {
352 ContentType type = new ContentType(getContentType());
353 type.setParameter("name", name);
354 setHeader("Content-Type", type.toString());
355 }
356 }
357
358 public InputStream getInputStream() throws MessagingException, IOException {
359 return getDataHandler().getInputStream();
360 }
361
362 protected InputStream getContentStream() throws MessagingException {
363 if (contentStream != null) {
364 return contentStream;
365 }
366
367 if (content != null) {
368 return new ByteArrayInputStream(content);
369 } else {
370 throw new MessagingException("No content");
371 }
372 }
373
374 public InputStream getRawInputStream() throws MessagingException {
375 return getContentStream();
376 }
377
378 public synchronized DataHandler getDataHandler() throws MessagingException {
379 if (dh == null) {
380 dh = new DataHandler(new MimePartDataSource(this));
381 }
382 return dh;
383 }
384
385 public Object getContent() throws MessagingException, IOException {
386 return getDataHandler().getContent();
387 }
388
389 public void setDataHandler(DataHandler handler) throws MessagingException {
390 dh = handler;
391 // if we have a handler override, then we need to invalidate any content
392 // headers that define the types. This information will be derived from the
393 // data heander unless subsequently overridden.
394 removeHeader("Content-Type");
395 removeHeader("Content-Transfer-Encoding");
396
397 }
398
399 public void setContent(Object content, String type) throws MessagingException {
400 // Multipart content needs to be handled separately.
401 if (content instanceof Multipart) {
402 setContent((Multipart)content);
403 }
404 else {
405 setDataHandler(new DataHandler(content, type));
406 }
407 }
408
409 public void setText(String text) throws MessagingException {
410 setText(text, null);
411 }
412
413 public void setText(String text, String charset) throws MessagingException {
414 // we need to sort out the character set if one is not provided.
415 if (charset == null) {
416 // if we have non us-ascii characters here, we need to adjust this.
417 if (!ASCIIUtil.isAscii(text)) {
418 charset = MimeUtility.getDefaultMIMECharset();
419 }
420 else {
421 charset = "us-ascii";
422 }
423 }
424 setContent(text, "text/plain; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME));
425 }
426
427 public void setContent(Multipart part) throws MessagingException {
428 setDataHandler(new DataHandler(part, part.getContentType()));
429 part.setParent(this);
430 }
431
432 public void writeTo(OutputStream out) throws IOException, MessagingException {
433 headers.writeTo(out, null);
434 // add the separater between the headers and the data portion.
435 out.write('\r');
436 out.write('\n');
437 // we need to process this using the transfer encoding type
438 OutputStream encodingStream = MimeUtility.encode(out, getEncoding());
439 getDataHandler().writeTo(encodingStream);
440 encodingStream.flush();
441 }
442
443 public String[] getHeader(String name) throws MessagingException {
444 return headers.getHeader(name);
445 }
446
447 public String getHeader(String name, String delimiter) throws MessagingException {
448 return headers.getHeader(name, delimiter);
449 }
450
451 public void setHeader(String name, String value) throws MessagingException {
452 headers.setHeader(name, value);
453 }
454
455 /**
456 * Conditionally set or remove a named header. If the new value
457 * is null, the header is removed.
458 *
459 * @param name The header name.
460 * @param value The new header value. A null value causes the header to be
461 * removed.
462 *
463 * @exception MessagingException
464 */
465 private void setOrRemoveHeader(String name, String value) throws MessagingException {
466 if (value == null) {
467 headers.removeHeader(name);
468 }
469 else {
470 headers.setHeader(name, value);
471 }
472 }
473
474 public void addHeader(String name, String value) throws MessagingException {
475 headers.addHeader(name, value);
476 }
477
478 public void removeHeader(String name) throws MessagingException {
479 headers.removeHeader(name);
480 }
481
482 public Enumeration getAllHeaders() throws MessagingException {
483 return headers.getAllHeaders();
484 }
485
486 public Enumeration getMatchingHeaders(String[] name) throws MessagingException {
487 return headers.getMatchingHeaders(name);
488 }
489
490 public Enumeration getNonMatchingHeaders(String[] name) throws MessagingException {
491 return headers.getNonMatchingHeaders(name);
492 }
493
494 public void addHeaderLine(String line) throws MessagingException {
495 headers.addHeaderLine(line);
496 }
497
498 public Enumeration getAllHeaderLines() throws MessagingException {
499 return headers.getAllHeaderLines();
500 }
501
502 public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
503 return headers.getMatchingHeaderLines(names);
504 }
505
506 public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
507 return headers.getNonMatchingHeaderLines(names);
508 }
509
510 protected void updateHeaders() throws MessagingException {
511 DataHandler handler = getDataHandler();
512
513 try {
514 // figure out the content type. If not set, we'll need to figure this out.
515 String type = dh.getContentType();
516 // parse this content type out so we can do matches/compares.
517 ContentType content = new ContentType(type);
518 // is this a multipart content?
519 if (content.match("multipart/*")) {
520 // the content is suppose to be a MimeMultipart. Ping it to update it's headers as well.
521 try {
522 MimeMultipart part = (MimeMultipart)handler.getContent();
523 part.updateHeaders();
524 } catch (ClassCastException e) {
525 throw new MessagingException("Message content is not MimeMultipart", e);
526 }
527 }
528 else if (!content.match("message/rfc822")) {
529 // simple part, we need to update the header type information
530 // if no encoding is set yet, figure this out from the data handler.
531 if (getSingleHeader("Content-Transfer-Encoding") == null) {
532 setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
533 }
534
535 // is a content type header set? Check the property to see if we need to set this.
536 if (getHeader("Content-Type") == null) {
537 if (SessionUtil.getBooleanProperty(MIME_SETDEFAULTTEXTCHARSET, true)) {
538 // is this a text type? Figure out the encoding and make sure it is set.
539 if (content.match("text/*")) {
540 // the charset should be specified as a parameter on the MIME type. If not there,
541 // try to figure one out.
542 if (content.getParameter("charset") == null) {
543
544 String encoding = getEncoding();
545 // if we're sending this as 7-bit ASCII, our character set need to be
546 // compatible.
547 if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
548 content.setParameter("charset", "us-ascii");
549 }
550 else {
551 // get the global default.
552 content.setParameter("charset", MimeUtility.getDefaultMIMECharset());
553 }
554 }
555 }
556 }
557 }
558 }
559
560 // if we don't have a content type header, then create one.
561 if (getSingleHeader("Content-Type") == null) {
562 // get the disposition header, and if it is there, copy the filename parameter into the
563 // name parameter of the type.
564 String disp = getHeader("Content-Disposition", null);
565 if (disp != null) {
566 // parse up the string value of the disposition
567 ContentDisposition disposition = new ContentDisposition(disp);
568 // now check for a filename value
569 String filename = disposition.getParameter("filename");
570 // copy and rename the parameter, if it exists.
571 if (filename != null) {
572 content.setParameter("name", filename);
573 }
574 }
575 // set the header with the updated content type information.
576 setHeader("Content-Type", content.toString());
577 }
578
579 } catch (IOException e) {
580 throw new MessagingException("Error updating message headers", e);
581 }
582 }
583
584 private String getSingleHeader(String name) throws MessagingException {
585 String[] values = getHeader(name);
586 if (values == null || values.length == 0) {
587 return null;
588 } else {
589 return values[0];
590 }
591 }
592 }