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