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    
045    /**
046     * A Strategy used to convert between a Camel {@link Exchange} and {@link Message} to and
047     * from a Mail {@link MimeMessage}
048     *
049     * @version $Revision: 760920 $
050     */
051    public class MailBinding {
052    
053        private HeaderFilterStrategy headerFilterStrategy;
054    
055        public MailBinding() {
056            headerFilterStrategy = new DefaultHeaderFilterStrategy();
057        }
058    
059        public MailBinding(HeaderFilterStrategy headerFilterStrategy) {
060            this.headerFilterStrategy = headerFilterStrategy;
061        }
062    
063        public void populateMailMessage(MailEndpoint endpoint, MimeMessage mimeMessage, Exchange exchange)
064            throws MessagingException, IOException {
065    
066            // camel message headers takes presedence over endpoint configuration
067            if (hasRecipientHeaders(exchange.getIn())) {
068                setRecipientFromCamelMessage(mimeMessage, exchange, exchange.getIn());
069            } else {
070                // fallback to endpoint configuration
071                setRecipientFromEndpointConfiguration(mimeMessage, endpoint);
072            }
073    
074            // must have at least one recipients otherwise we do not know where to send the mail
075            if (mimeMessage.getAllRecipients() == null) {
076                throw new IllegalArgumentException("The mail message does not have any recipients set.");
077            }
078    
079            // append the rest of the headers (no recipients) that could be subject, reply-to etc.
080            appendHeadersFromCamelMessage(mimeMessage, exchange, exchange.getIn());
081    
082            if (empty(mimeMessage.getFrom())) {
083                // lets default the address to the endpoint destination
084                String from = endpoint.getConfiguration().getFrom();
085                mimeMessage.setFrom(new InternetAddress(from));
086            }
087    
088            // if there is an alternativebody provided, set up a mime multipart alternative message
089            if (hasAlternativeBody(endpoint.getConfiguration(), exchange.getIn())) {
090                createMultipartAlternativeMessage(mimeMessage, exchange.getIn(), endpoint.getConfiguration()); 
091            } else {
092                if (exchange.getIn().hasAttachments()) {
093                    appendAttachmentsFromCamel(mimeMessage, exchange.getIn(), endpoint.getConfiguration());
094                } else {
095                    if ("text/html".equals(endpoint.getConfiguration().getContentType())) {                  
096                        DataSource ds = new ByteArrayDataSource(exchange.getIn().getBody(String.class), "text/html");
097                        mimeMessage.setDataHandler(new DataHandler(ds));
098                    } else {
099                        // its just text/plain
100                        mimeMessage.setText(exchange.getIn().getBody(String.class));
101                    }
102                }
103            }
104        }
105    
106        /**
107         * Extracts the body from the Mail message
108         */
109        public Object extractBodyFromMail(MailExchange exchange, Message message) {
110            try {
111                return message.getContent();
112            } catch (Exception e) {
113                throw new RuntimeCamelException("Failed to extract body due to: " + e.getMessage()
114                    + ". Exchange: " + exchange + ". Message: " + message, e);
115            }
116        }
117    
118        /**
119         * Appends the Mail headers from the Camel {@link MailMessage}
120         */
121        protected void appendHeadersFromCamelMessage(MimeMessage mimeMessage, Exchange exchange,
122                                                     org.apache.camel.Message camelMessage)
123            throws MessagingException {
124    
125            for (Map.Entry<String, Object> entry : camelMessage.getHeaders().entrySet()) {
126                String headerName = entry.getKey();
127                Object headerValue = entry.getValue();
128                if (headerValue != null) {
129                    if (headerFilterStrategy != null
130                            && !headerFilterStrategy.applyFilterToCamelHeaders(headerName, headerValue)) {
131    
132                        if (isRecipientHeader(headerName)) {
133                            // skip any recipients as they are handled specially
134                            continue;
135                        }
136    
137                        // Mail messages can repeat the same header...
138                        if (ObjectConverter.isCollection(headerValue)) {
139                            Iterator iter = ObjectConverter.iterator(headerValue);
140                            while (iter.hasNext()) {
141                                Object value = iter.next();
142                                mimeMessage.addHeader(headerName, asString(exchange, value));
143                            }
144                        } else {
145                            mimeMessage.setHeader(headerName, asString(exchange, headerValue));
146                        }
147                    }
148                }
149            }
150        }
151    
152        private void setRecipientFromCamelMessage(MimeMessage mimeMessage, Exchange exchange,
153                                                    org.apache.camel.Message camelMessage)
154            throws MessagingException {
155    
156            for (Map.Entry<String, Object> entry : camelMessage.getHeaders().entrySet()) {
157                String headerName = entry.getKey();
158                Object headerValue = entry.getValue();
159                if (headerValue != null && isRecipientHeader(headerName)) {
160                    // special handling of recipients
161                    if (ObjectConverter.isCollection(headerValue)) {
162                        Iterator iter = ObjectConverter.iterator(headerValue);
163                        while (iter.hasNext()) {
164                            Object recipient = iter.next();
165                            appendRecipientToMimeMessage(mimeMessage, headerName, asString(exchange, recipient));
166                        }
167                    } else {
168                        appendRecipientToMimeMessage(mimeMessage, headerName, asString(exchange, headerValue));
169                    }
170                }
171            }
172        }
173    
174        /**
175         * Appends the Mail headers from the endpoint configuraiton.
176         */
177        protected void setRecipientFromEndpointConfiguration(MimeMessage mimeMessage, MailEndpoint endpoint)
178            throws MessagingException {
179    
180            Map<Message.RecipientType, String> recipients = endpoint.getConfiguration().getRecipients();
181            if (recipients.containsKey(Message.RecipientType.TO)) {
182                appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.TO.toString(), recipients.get(Message.RecipientType.TO));
183            }
184            if (recipients.containsKey(Message.RecipientType.CC)) {
185                appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.CC.toString(), recipients.get(Message.RecipientType.CC));
186            }
187            if (recipients.containsKey(Message.RecipientType.BCC)) {
188                appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.BCC.toString(), recipients.get(Message.RecipientType.BCC));
189            }
190    
191            // fallback to use destination if no TO provided at all
192            String destination = endpoint.getConfiguration().getDestination();
193            if (destination != null && mimeMessage.getRecipients(Message.RecipientType.TO) == null) {
194                appendRecipientToMimeMessage(mimeMessage, Message.RecipientType.TO.toString(), destination);
195            }
196        }
197    
198        /**
199         * Appends the Mail attachments from the Camel {@link MailMessage}
200         */
201        protected void appendAttachmentsFromCamel(MimeMessage mimeMessage, org.apache.camel.Message camelMessage,
202                                                  MailConfiguration configuration)
203            throws MessagingException {
204            
205            // Put parts in message
206            mimeMessage.setContent(createMixedMultipartAttachments(camelMessage, configuration));
207        }
208    
209        private MimeMultipart createMixedMultipartAttachments(org.apache.camel.Message camelMessage, MailConfiguration configuration) throws MessagingException {
210            // fill the body with text
211            MimeMultipart multipart = new MimeMultipart();
212            multipart.setSubType("mixed");
213            addBodyToMultipart(camelMessage, configuration, multipart);
214            String partDisposition = configuration.isUseInlineAttachments() ?  Part.INLINE : Part.ATTACHMENT;
215            addAttachmentsToMultipart(camelMessage, multipart, partDisposition);
216            return multipart;
217        }
218    
219        protected void addAttachmentsToMultipart(org.apache.camel.Message camelMessage, MimeMultipart multipart, String partDisposition) throws MessagingException {
220            for (Map.Entry<String, DataHandler> entry : camelMessage.getAttachments().entrySet()) {
221                String attachmentFilename = entry.getKey();
222                DataHandler handler = entry.getValue();
223                if (handler != null) {
224                    if (shouldOutputAttachment(camelMessage, attachmentFilename, handler)) {
225                        // Create another body part
226                        BodyPart messageBodyPart = new MimeBodyPart();
227                        // Set the data handler to the attachment
228                        messageBodyPart.setDataHandler(handler);
229                        
230                        if (attachmentFilename.toLowerCase().startsWith("cid:")) {
231                        // add a Content-ID header to the attachment
232                            messageBodyPart.addHeader("Content-ID", attachmentFilename.substring(4));
233                        }
234                        // Set the filename
235                        messageBodyPart.setFileName(attachmentFilename);
236                        // Set Disposition
237                        messageBodyPart.setDisposition(partDisposition);
238                        // Add part to multipart
239                        multipart.addBodyPart(messageBodyPart);
240                    }
241                }
242            }
243        }
244    
245        protected void createMultipartAlternativeMessage(MimeMessage mimeMessage, org.apache.camel.Message camelMessage, MailConfiguration configuration)
246            throws MessagingException { 
247    
248            MimeMultipart multipartAlternative = new MimeMultipart("alternative");
249            mimeMessage.setContent(multipartAlternative);
250    
251            BodyPart plainText = new MimeBodyPart();
252            plainText.setText(getAlternativeBody(configuration, camelMessage));
253            multipartAlternative.addBodyPart(plainText);
254    
255            // if there are no attachments, add the body to the same mulitpart message
256            if (!camelMessage.hasAttachments()) {
257                addBodyToMultipart(camelMessage, configuration, multipartAlternative);
258            } else {
259                // if there are attachments, but they aren't set to be inline, add them to
260                // treat them as normal. It will append a multipart-mixed with the attachments and the
261                // body text
262                if (!configuration.isUseInlineAttachments()) {
263                    BodyPart mixedAttachments = new MimeBodyPart();
264                    mixedAttachments.setContent(createMixedMultipartAttachments(camelMessage, configuration));
265                    multipartAlternative.addBodyPart(mixedAttachments);
266                    //appendAttachmentsFromCamel(mimeMessage, camelMessage, configuration);
267                } else { // if the attachments are set to be inline, attach them as inline attachments
268                    MimeMultipart multipartRelated = new MimeMultipart("related");
269                    BodyPart related = new MimeBodyPart();
270    
271                    related.setContent(multipartRelated);
272                    multipartAlternative.addBodyPart(related);
273    
274                    addBodyToMultipart(camelMessage, configuration, multipartRelated);
275    
276                    addAttachmentsToMultipart(camelMessage, multipartRelated, Part.INLINE);
277                }
278            }
279    
280        }
281    
282        protected void addBodyToMultipart(org.apache.camel.Message camelMessage, MailConfiguration configuration, MimeMultipart activeMultipart) throws MessagingException {
283            BodyPart bodyMessage = new MimeBodyPart();
284            bodyMessage.setContent(camelMessage.getBody(String.class), configuration.getContentType());
285            activeMultipart.addBodyPart(bodyMessage);
286        }
287    
288    
289        /**
290         * Strategy to allow filtering of attachments which are put on the Mail message
291         */
292        protected boolean shouldOutputAttachment(org.apache.camel.Message camelMessage, String attachmentFilename, DataHandler handler) {
293            return true;
294        }
295    
296        protected Map<String, Object> extractHeadersFromMail(Message mailMessage) throws MessagingException {
297            Map<String, Object> answer = new HashMap<String, Object>();
298            Enumeration names = mailMessage.getAllHeaders();
299    
300            while (names.hasMoreElements()) {
301                Header header = (Header)names.nextElement();
302                String[] value = mailMessage.getHeader(header.getName());
303                if (headerFilterStrategy != null
304                        && !headerFilterStrategy.applyFilterToExternalHeaders(header.getName(), value)) {
305                    // toLowerCase() for doing case insensitive search
306                    if (value.length == 1) {
307                        CollectionHelper.appendValue(answer, header.getName().toLowerCase(), value[0]);
308                    } else {
309                        CollectionHelper.appendValue(answer, header.getName().toLowerCase(), value);
310                    }
311                }
312            }
313    
314            return answer;
315        }
316    
317        private static void appendRecipientToMimeMessage(MimeMessage mimeMessage, String type, String recipient)
318            throws MessagingException {
319    
320            // we support that multi recipient can be given as a string seperated by comma or semi colon
321            String[] lines = recipient.split("[,|;]");
322            for (String line : lines) {
323                line = line.trim();
324                mimeMessage.addRecipients(asRecipientType(type), line);
325            }
326        }
327    
328        /**
329         * Does the given camel message contain any To, CC or BCC header names?
330         */
331        private static boolean hasRecipientHeaders(org.apache.camel.Message camelMessage) {
332            for (String key : camelMessage.getHeaders().keySet()) {
333                if (isRecipientHeader(key)) {
334                    return true;
335                }
336            }
337            return false;
338        }
339    
340        protected static boolean hasAlternativeBody(MailConfiguration configuration, org.apache.camel.Message camelMessage) {
341            return getAlternativeBody(configuration, camelMessage) != null;
342        }
343    
344        protected static String getAlternativeBody(MailConfiguration configuration, org.apache.camel.Message camelMessage) {
345            String alternativeBodyHeader = configuration.getAlternateBodyHeader();
346            return camelMessage.getHeader(alternativeBodyHeader, java.lang.String.class);
347        }
348    
349        /**
350         * Is the given key a mime message recipient header (To, CC or BCC)
351         */
352        private static boolean isRecipientHeader(String key) {
353            if (Message.RecipientType.TO.toString().equalsIgnoreCase(key)) {
354                return true;
355            } else if (Message.RecipientType.CC.toString().equalsIgnoreCase(key)) {
356                return true;
357            } else if (Message.RecipientType.BCC.toString().equalsIgnoreCase(key)) {
358                return true;
359            }
360            return false;
361        }
362    
363        /**
364         * Returns the RecipientType object.
365         */
366        private static Message.RecipientType asRecipientType(String type) {
367            if (Message.RecipientType.TO.toString().equalsIgnoreCase(type)) {
368                return Message.RecipientType.TO;
369            } else if (Message.RecipientType.CC.toString().equalsIgnoreCase(type)) {
370                return Message.RecipientType.CC;
371            } else if (Message.RecipientType.BCC.toString().equalsIgnoreCase(type)) {
372                return Message.RecipientType.BCC;
373            }
374            throw new IllegalArgumentException("Unknown recipient type: " + type);
375        }
376    
377    
378        private static boolean empty(Address[] addresses) {
379            return addresses == null || addresses.length == 0;
380        }
381    
382        private static String asString(Exchange exchange, Object value) {
383            return exchange.getContext().getTypeConverter().convertTo(String.class, exchange, value);
384        }
385    
386    }