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