001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. 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 package org.apache.camel.component.mail; 018 019 import java.io.IOException; 020 import java.io.UnsupportedEncodingException; 021 import java.nio.charset.Charset; 022 import java.nio.charset.IllegalCharsetNameException; 023 import java.util.Enumeration; 024 import java.util.HashMap; 025 import java.util.Iterator; 026 import java.util.Map; 027 028 import javax.activation.DataHandler; 029 import javax.activation.DataSource; 030 import javax.mail.Address; 031 import javax.mail.BodyPart; 032 import javax.mail.Header; 033 import javax.mail.Message; 034 import javax.mail.MessagingException; 035 import javax.mail.Multipart; 036 import javax.mail.Part; 037 import javax.mail.internet.InternetAddress; 038 import javax.mail.internet.MimeBodyPart; 039 import javax.mail.internet.MimeMessage; 040 import javax.mail.internet.MimeMultipart; 041 import javax.mail.util.ByteArrayDataSource; 042 043 import org.apache.camel.Exchange; 044 import org.apache.camel.RuntimeCamelException; 045 import org.apache.camel.converter.ObjectConverter; 046 import org.apache.camel.impl.DefaultHeaderFilterStrategy; 047 import org.apache.camel.spi.HeaderFilterStrategy; 048 import org.apache.camel.util.CollectionHelper; 049 import org.apache.camel.util.ObjectHelper; 050 import org.apache.commons.logging.Log; 051 import org.apache.commons.logging.LogFactory; 052 053 /** 054 * A Strategy used to convert between a Camel {@link Exchange} and {@link Message} to and 055 * from a Mail {@link MimeMessage} 056 * 057 * @version $Revision: 884750 $ 058 */ 059 public class MailBinding { 060 061 private static final transient Log LOG = LogFactory.getLog(MailBinding.class); 062 private HeaderFilterStrategy headerFilterStrategy; 063 private ContentTypeResolver contentTypeResolver; 064 065 public MailBinding() { 066 headerFilterStrategy = new DefaultHeaderFilterStrategy(); 067 } 068 069 public MailBinding(HeaderFilterStrategy headerFilterStrategy, ContentTypeResolver contentTypeResolver) { 070 this.headerFilterStrategy = headerFilterStrategy; 071 this.contentTypeResolver = contentTypeResolver; 072 } 073 074 public void populateMailMessage(MailEndpoint endpoint, MimeMessage mimeMessage, Exchange exchange) 075 throws MessagingException, IOException { 076 077 // camel message headers takes precedence over endpoint configuration 078 if (hasRecipientHeaders(exchange)) { 079 setRecipientFromCamelMessage(mimeMessage, exchange); 080 } else { 081 // fallback to endpoint configuration 082 setRecipientFromEndpointConfiguration(mimeMessage, endpoint); 083 } 084 085 // must have at least one recipients otherwise we do not know where to send the mail 086 if (mimeMessage.getAllRecipients() == null) { 087 throw new IllegalArgumentException("The mail message does not have any recipients set."); 088 } 089 090 // append the rest of the headers (no recipients) that could be subject, reply-to etc. 091 appendHeadersFromCamelMessage(mimeMessage, endpoint.getConfiguration(), exchange); 092 093 if (empty(mimeMessage.getFrom())) { 094 // lets default the address to the endpoint destination 095 String from = endpoint.getConfiguration().getFrom(); 096 mimeMessage.setFrom(new InternetAddress(from)); 097 } 098 099 // if there is an alternative body provided, set up a mime multipart alternative message 100 if (hasAlternativeBody(endpoint.getConfiguration(), exchange)) { 101 createMultipartAlternativeMessage(mimeMessage, endpoint.getConfiguration(), exchange); 102 } else { 103 if (exchange.getIn().hasAttachments()) { 104 appendAttachmentsFromCamel(mimeMessage, endpoint.getConfiguration(), exchange); 105 } else { 106 populateContentOnMimeMessage(mimeMessage, endpoint.getConfiguration(), exchange); 107 } 108 } 109 } 110 111 protected String determineContentType(MailConfiguration configuration, Exchange exchange) { 112 // see if we got any content type set 113 String contentType = configuration.getContentType(); 114 if (exchange.getIn().getHeader("contentType") != null) { 115 contentType = exchange.getIn().getHeader("contentType", String.class); 116 } else if (exchange.getIn().getHeader(Exchange.CONTENT_TYPE) != null) { 117 contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class); 118 } 119 120 // fix content type to include a space after semi colon if missing 121 if (contentType != null && contentType.contains(";")) { 122 String before = ObjectHelper.before(contentType, ";"); 123 String after = ObjectHelper.after(contentType, ";"); 124 125 // after is the charset lets see if its given and a valid charset 126 if (after != null) { 127 String charset = ObjectHelper.after(after, "="); 128 charset = determineCharSet(configuration, charset); 129 if (charset != null) { 130 after = "charset=" + charset; 131 } else { 132 after = null; 133 } 134 } 135 136 if (before != null && after == null) { 137 contentType = before.trim(); 138 } else if (before != null && after != null) { 139 contentType = before.trim() + "; " + after; 140 } 141 } 142 143 if (LOG.isTraceEnabled()) { 144 LOG.trace("Determined Content-Type: " + contentType); 145 } 146 147 return contentType; 148 } 149 150 protected String determineCharSet(MailConfiguration configuration, String charset) { 151 if (charset == null) { 152 return null; 153 } 154 155 boolean supported; 156 try { 157 supported = Charset.isSupported(charset); 158 } catch (IllegalCharsetNameException e) { 159 supported = false; 160 } 161 162 if (supported) { 163 return charset; 164 } else if (configuration.isIgnoreUnsupportedCharset()) { 165 LOG.warn("Charset: " + charset + " is not supported, will fallback to use platform default instead."); 166 return null; 167 } 168 169 return charset; 170 } 171 172 protected String populateContentOnMimeMessage(MimeMessage part, MailConfiguration configuration, Exchange exchange) 173 throws MessagingException, IOException { 174 175 String contentType = determineContentType(configuration, exchange); 176 177 if (LOG.isTraceEnabled()) { 178 LOG.trace("Using Content-Type " + contentType + " for MimeMessage: " + part); 179 } 180 181 // always store content in a byte array data store to avoid various content type and charset issues 182 DataSource ds = new ByteArrayDataSource(exchange.getIn().getBody(String.class), contentType); 183 part.setDataHandler(new DataHandler(ds)); 184 185 // set the content type header afterwards 186 part.setHeader("Content-Type", contentType); 187 188 return contentType; 189 } 190 191 protected String populateContentOnBodyPart(BodyPart part, MailConfiguration configuration, Exchange exchange) 192 throws MessagingException, IOException { 193 194 String contentType = determineContentType(configuration, exchange); 195 196 if (LOG.isTraceEnabled()) { 197 LOG.trace("Using Content-Type " + contentType + " for BodyPart: " + part); 198 } 199 200 // always store content in a byte array data store to avoid various content type and charset issues 201 DataSource ds = new ByteArrayDataSource(exchange.getIn().getBody(String.class), contentType); 202 part.setDataHandler(new DataHandler(ds)); 203 204 // set the content type header afterwards 205 part.setHeader("Content-Type", contentType); 206 207 return contentType; 208 } 209 210 /** 211 * Extracts the body from the Mail message 212 */ 213 public Object extractBodyFromMail(Exchange exchange, MailMessage mailMessage) { 214 Message message = mailMessage.getMessage(); 215 try { 216 return message.getContent(); 217 } catch (Exception e) { 218 // try to fix message in case it has an unsupported encoding in the Content-Type header 219 UnsupportedEncodingException uee = ObjectHelper.getException(UnsupportedEncodingException.class, e); 220 if (uee != null) { 221 LOG.debug("Unsupported encoding detected: " + uee.getMessage()); 222 try { 223 String contentType = message.getContentType(); 224 String type = ObjectHelper.before(contentType, "charset="); 225 if (type != null) { 226 // try again with fixed content type 227 LOG.debug("Trying to extract mail message again with fixed Content-Type: " + type); 228 // Since message is read-only, we need to use a copy 229 MimeMessage messageCopy = new MimeMessage((MimeMessage)message); 230 messageCopy.setHeader("Content-Type", type); 231 Object body = messageCopy.getContent(); 232 // If we got this far, our fix worked... 233 // Replace the MailMessage's Message with the copy 234 mailMessage.setMessage(messageCopy); 235 return body; 236 } 237 } catch (Exception e2) { 238 // fall through and let original exception be thrown 239 } 240 } 241 242 throw new RuntimeCamelException("Failed to extract body due to: " + e.getMessage() 243 + ". Exchange: " + exchange + ". Message: " + message, e); 244 } 245 } 246 247 /** 248 * Parses the attachments of the given mail message and adds them to the map 249 * 250 * @param message the mail message with attachments 251 * @param map the map to add found attachments (attachmentFilename is the key) 252 */ 253 public void extractAttachmentsFromMail(Message message, Map<String, DataHandler> map) 254 throws javax.mail.MessagingException, IOException { 255 256 LOG.trace("Extracting attachments +++ start +++"); 257 258 Object content = message.getContent(); 259 if (content instanceof Multipart) { 260 extractAttachmentsFromMultipart((Multipart)content, map); 261 } else if (content != null) { 262 LOG.trace("No attachments to extract as content is not Multipart: " + content.getClass().getName()); 263 } 264 265 LOG.trace("Extracting attachments +++ done +++"); 266 } 267 268 protected void extractAttachmentsFromMultipart(Multipart mp, Map<String, DataHandler> map) 269 throws javax.mail.MessagingException, IOException { 270 271 for (int i = 0; i < mp.getCount(); i++) { 272 Part part = mp.getBodyPart(i); 273 LOG.trace("Part #" + i + ": " + part); 274 275 if (part.isMimeType("multipart/*")) { 276 LOG.trace("Part #" + i + ": is mimetype: multipart/*"); 277 extractAttachmentsFromMultipart((Multipart)part.getContent(), map); 278 } else { 279 String disposition = part.getDisposition(); 280 if (LOG.isTraceEnabled()) { 281 LOG.trace("Part #" + i + ": Disposition: " + part.getDisposition()); 282 LOG.trace("Part #" + i + ": Description: " + part.getDescription()); 283 LOG.trace("Part #" + i + ": ContentType: " + part.getContentType()); 284 LOG.trace("Part #" + i + ": FileName: " + part.getFileName()); 285 LOG.trace("Part #" + i + ": Size: " + part.getSize()); 286 LOG.trace("Part #" + i + ": LineCount: " + part.getLineCount()); 287 } 288 289 if (disposition != null && (disposition.equalsIgnoreCase(Part.ATTACHMENT) || disposition.equalsIgnoreCase(Part.INLINE))) { 290 // only add named attachments 291 String fileName = part.getFileName(); 292 if (fileName != null) { 293 LOG.debug("Mail contains file attachment: " + fileName); 294 // Parts marked with a disposition of Part.ATTACHMENT are clearly attachments 295 CollectionHelper.appendValue(map, fileName, part.getDataHandler()); 296 } 297 } 298 } 299 } 300 } 301 302 /** 303 * Appends the Mail headers from the Camel {@link MailMessage} 304 */ 305 protected void appendHeadersFromCamelMessage(MimeMessage mimeMessage, MailConfiguration configuration, Exchange exchange) 306 throws MessagingException { 307 308 for (Map.Entry<String, Object> entry : exchange.getIn().getHeaders().entrySet()) { 309 String headerName = entry.getKey(); 310 Object headerValue = entry.getValue(); 311 if (headerValue != null) { 312 if (headerFilterStrategy != null 313 && !headerFilterStrategy.applyFilterToCamelHeaders(headerName, headerValue, exchange)) { 314 315 if (isRecipientHeader(headerName)) { 316 // skip any recipients as they are handled specially 317 continue; 318 } 319 320 // alternative body should also be skipped 321 if (headerName.equalsIgnoreCase(configuration.getAlternativeBodyHeader())) { 322 // skip alternative body 323 continue; 324 } 325 326 // Mail messages can repeat the same header... 327 if (ObjectConverter.isCollection(headerValue)) { 328 Iterator iter = ObjectHelper.createIterator(headerValue); 329 while (iter.hasNext()) { 330 Object value = iter.next(); 331 mimeMessage.addHeader(headerName, asString(exchange, value)); 332 } 333 } else { 334 mimeMessage.setHeader(headerName, asString(exchange, headerValue)); 335 } 336 } 337 } 338 } 339 } 340 341 private void setRecipientFromCamelMessage(MimeMessage mimeMessage, Exchange exchange) throws MessagingException { 342 for (Map.Entry<String, Object> entry : exchange.getIn().getHeaders().entrySet()) { 343 String headerName = entry.getKey(); 344 Object headerValue = entry.getValue(); 345 if (headerValue != null && isRecipientHeader(headerName)) { 346 // special handling of recipients 347 if (ObjectConverter.isCollection(headerValue)) { 348 Iterator iter = ObjectHelper.createIterator(headerValue); 349 while (iter.hasNext()) { 350 Object recipient = iter.next(); 351 appendRecipientToMimeMessage(mimeMessage, headerName, asString(exchange, recipient)); 352 } 353 } else { 354 appendRecipientToMimeMessage(mimeMessage, headerName, asString(exchange, headerValue)); 355 } 356 } 357 } 358 } 359 360 /** 361 * Appends the Mail headers from the endpoint configuration. 362 */ 363 protected void setRecipientFromEndpointConfiguration(MimeMessage mimeMessage, MailEndpoint endpoint) 364 throws MessagingException { 365 366 Map<Message.RecipientType, String> recipients = endpoint.getConfiguration().getRecipients(); 367 if (recipients.containsKey(Message.RecipientType.TO)) { 368 appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.TO.toString(), recipients.get(Message.RecipientType.TO)); 369 } 370 if (recipients.containsKey(Message.RecipientType.CC)) { 371 appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.CC.toString(), recipients.get(Message.RecipientType.CC)); 372 } 373 if (recipients.containsKey(Message.RecipientType.BCC)) { 374 appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.BCC.toString(), recipients.get(Message.RecipientType.BCC)); 375 } 376 } 377 378 /** 379 * Appends the Mail attachments from the Camel {@link MailMessage} 380 */ 381 protected void appendAttachmentsFromCamel(MimeMessage mimeMessage, MailConfiguration configuration, Exchange exchange) 382 throws MessagingException, IOException { 383 384 // Put parts in message 385 mimeMessage.setContent(createMixedMultipartAttachments(configuration, exchange)); 386 } 387 388 private MimeMultipart createMixedMultipartAttachments(MailConfiguration configuration, Exchange exchange) 389 throws MessagingException, IOException { 390 391 // fill the body with text 392 MimeMultipart multipart = new MimeMultipart(); 393 multipart.setSubType("mixed"); 394 addBodyToMultipart(configuration, multipart, exchange); 395 String partDisposition = configuration.isUseInlineAttachments() ? Part.INLINE : Part.ATTACHMENT; 396 if (exchange.getIn().hasAttachments()) { 397 addAttachmentsToMultipart(multipart, partDisposition, exchange); 398 } 399 return multipart; 400 } 401 402 protected void addAttachmentsToMultipart(MimeMultipart multipart, String partDisposition, Exchange exchange) throws MessagingException { 403 LOG.trace("Adding attachments +++ start +++"); 404 int i = 0; 405 for (Map.Entry<String, DataHandler> entry : exchange.getIn().getAttachments().entrySet()) { 406 String attachmentFilename = entry.getKey(); 407 DataHandler handler = entry.getValue(); 408 409 if (LOG.isTraceEnabled()) { 410 LOG.trace("Attachment #" + i + ": Disposition: " + partDisposition); 411 LOG.trace("Attachment #" + i + ": DataHandler: " + handler); 412 LOG.trace("Attachment #" + i + ": FileName: " + attachmentFilename); 413 } 414 if (handler != null) { 415 if (shouldAddAttachment(exchange, attachmentFilename, handler)) { 416 // Create another body part 417 BodyPart messageBodyPart = new MimeBodyPart(); 418 // Set the data handler to the attachment 419 messageBodyPart.setDataHandler(handler); 420 421 if (attachmentFilename.toLowerCase().startsWith("cid:")) { 422 // add a Content-ID header to the attachment 423 // must use angle brackets according to RFC: http://www.ietf.org/rfc/rfc2392.txt 424 messageBodyPart.addHeader("Content-ID", "<" + attachmentFilename.substring(4) + ">"); 425 // Set the filename without the cid 426 messageBodyPart.setFileName(attachmentFilename.substring(4)); 427 } else { 428 // Set the filename 429 messageBodyPart.setFileName(attachmentFilename); 430 } 431 432 LOG.trace("Attachment #" + i + ": ContentType: " + messageBodyPart.getContentType()); 433 434 if (contentTypeResolver != null) { 435 String contentType = contentTypeResolver.resolveContentType(attachmentFilename); 436 LOG.trace("Attachment #" + i + ": Using content type resolver: " + contentTypeResolver + " resolved content type as: " + contentType); 437 if (contentType != null) { 438 String value = contentType + "; name=" + attachmentFilename; 439 messageBodyPart.setHeader("Content-Type", value); 440 LOG.trace("Attachment #" + i + ": ContentType: " + messageBodyPart.getContentType()); 441 } 442 } 443 444 // Set Disposition 445 messageBodyPart.setDisposition(partDisposition); 446 // Add part to multipart 447 multipart.addBodyPart(messageBodyPart); 448 } else { 449 LOG.trace("shouldAddAttachment: false"); 450 } 451 } else { 452 LOG.warn("Cannot add attachment: " + attachmentFilename + " as DataHandler is null"); 453 } 454 i++; 455 } 456 LOG.trace("Adding attachments +++ done +++"); 457 } 458 459 protected void createMultipartAlternativeMessage(MimeMessage mimeMessage, MailConfiguration configuration, Exchange exchange) 460 throws MessagingException, IOException { 461 462 MimeMultipart multipartAlternative = new MimeMultipart("alternative"); 463 mimeMessage.setContent(multipartAlternative); 464 465 BodyPart plainText = new MimeBodyPart(); 466 plainText.setText(getAlternativeBody(configuration, exchange)); 467 // remove the header with the alternative mail now that we got it 468 // otherwise it might end up twice in the mail reader 469 exchange.getIn().removeHeader(configuration.getAlternativeBodyHeader()); 470 multipartAlternative.addBodyPart(plainText); 471 472 // if there are no attachments, add the body to the same mulitpart message 473 if (!exchange.getIn().hasAttachments()) { 474 addBodyToMultipart(configuration, multipartAlternative, exchange); 475 } else { 476 // if there are attachments, but they aren't set to be inline, add them to 477 // treat them as normal. It will append a multipart-mixed with the attachments and the body text 478 if (!configuration.isUseInlineAttachments()) { 479 BodyPart mixedAttachments = new MimeBodyPart(); 480 mixedAttachments.setContent(createMixedMultipartAttachments(configuration, exchange)); 481 multipartAlternative.addBodyPart(mixedAttachments); 482 } else { 483 // if the attachments are set to be inline, attach them as inline attachments 484 MimeMultipart multipartRelated = new MimeMultipart("related"); 485 BodyPart related = new MimeBodyPart(); 486 487 related.setContent(multipartRelated); 488 multipartAlternative.addBodyPart(related); 489 490 addBodyToMultipart(configuration, multipartRelated, exchange); 491 492 addAttachmentsToMultipart(multipartRelated, Part.INLINE, exchange); 493 } 494 } 495 } 496 497 protected void addBodyToMultipart(MailConfiguration configuration, MimeMultipart activeMultipart, Exchange exchange) 498 throws MessagingException, IOException { 499 500 BodyPart bodyMessage = new MimeBodyPart(); 501 populateContentOnBodyPart(bodyMessage, configuration, exchange); 502 activeMultipart.addBodyPart(bodyMessage); 503 } 504 505 /** 506 * Strategy to allow filtering of attachments which are added on the Mail message 507 */ 508 protected boolean shouldAddAttachment(Exchange exchange, String attachmentFilename, DataHandler handler) { 509 return true; 510 } 511 512 protected Map<String, Object> extractHeadersFromMail(Message mailMessage, Exchange exchange) throws MessagingException { 513 Map<String, Object> answer = new HashMap<String, Object>(); 514 Enumeration names = mailMessage.getAllHeaders(); 515 516 while (names.hasMoreElements()) { 517 Header header = (Header) names.nextElement(); 518 String value = header.getValue(); 519 if (headerFilterStrategy != null && !headerFilterStrategy.applyFilterToExternalHeaders(header.getName(), value, exchange)) { 520 CollectionHelper.appendValue(answer, header.getName(), value); 521 } 522 } 523 524 return answer; 525 } 526 527 private static void appendRecipientToMimeMessage(MimeMessage mimeMessage, String type, String recipient) 528 throws MessagingException { 529 530 // we support that multi recipient can be given as a string separated by comma or semicolon 531 String[] lines = recipient.split("[,;]"); 532 for (String line : lines) { 533 line = line.trim(); 534 mimeMessage.addRecipients(asRecipientType(type), line); 535 } 536 } 537 538 /** 539 * Does the given camel message contain any To, CC or BCC header names? 540 */ 541 private static boolean hasRecipientHeaders(Exchange exchange) { 542 for (String key : exchange.getIn().getHeaders().keySet()) { 543 if (isRecipientHeader(key)) { 544 return true; 545 } 546 } 547 return false; 548 } 549 550 protected static boolean hasAlternativeBody(MailConfiguration configuration, Exchange exchange) { 551 return getAlternativeBody(configuration, exchange) != null; 552 } 553 554 protected static String getAlternativeBody(MailConfiguration configuration, Exchange exchange) { 555 String alternativeBodyHeader = configuration.getAlternativeBodyHeader(); 556 return exchange.getIn().getHeader(alternativeBodyHeader, java.lang.String.class); 557 } 558 559 /** 560 * Is the given key a mime message recipient header (To, CC or BCC) 561 */ 562 private static boolean isRecipientHeader(String key) { 563 if (Message.RecipientType.TO.toString().equalsIgnoreCase(key)) { 564 return true; 565 } else if (Message.RecipientType.CC.toString().equalsIgnoreCase(key)) { 566 return true; 567 } else if (Message.RecipientType.BCC.toString().equalsIgnoreCase(key)) { 568 return true; 569 } 570 return false; 571 } 572 573 /** 574 * Returns the RecipientType object. 575 */ 576 private static Message.RecipientType asRecipientType(String type) { 577 if (Message.RecipientType.TO.toString().equalsIgnoreCase(type)) { 578 return Message.RecipientType.TO; 579 } else if (Message.RecipientType.CC.toString().equalsIgnoreCase(type)) { 580 return Message.RecipientType.CC; 581 } else if (Message.RecipientType.BCC.toString().equalsIgnoreCase(type)) { 582 return Message.RecipientType.BCC; 583 } 584 throw new IllegalArgumentException("Unknown recipient type: " + type); 585 } 586 587 588 private static boolean empty(Address[] addresses) { 589 return addresses == null || addresses.length == 0; 590 } 591 592 private static String asString(Exchange exchange, Object value) { 593 return exchange.getContext().getTypeConverter().convertTo(String.class, exchange, value); 594 } 595 596 }