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