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    }