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    }