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.commons.mail;
018    
019    import java.io.File;
020    import java.io.IOException;
021    import java.io.InputStream;
022    import java.net.MalformedURLException;
023    import java.net.URL;
024    import java.util.HashMap;
025    import java.util.Iterator;
026    import java.util.List;
027    import java.util.Map;
028    
029    import javax.activation.DataHandler;
030    import javax.activation.DataSource;
031    import javax.activation.FileDataSource;
032    import javax.activation.URLDataSource;
033    import javax.mail.BodyPart;
034    import javax.mail.MessagingException;
035    import javax.mail.internet.MimeBodyPart;
036    import javax.mail.internet.MimeMultipart;
037    
038    /**
039     * An HTML multipart email.
040     *
041     * <p>This class is used to send HTML formatted email.  A text message
042     * can also be set for HTML unaware email clients, such as text-based
043     * email clients.
044     *
045     * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to
046     * add attachments to the email.
047     *
048     * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then
049     * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods.
050     * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The
051     * alternative text content can be set with {@link #setTextMsg(String)}.
052     *
053     * <p>Either the text or HTML can be omitted, in which case the "main"
054     * part of the multipart becomes whichever is supplied rather than a
055     * <code>multipart/alternative</code>.
056     *
057     * <h3>Embedding Images and Media</h3>
058     *
059     * <p>It is also possible to embed URLs, files, or arbitrary
060     * <code>DataSource</code>s directly into the body of the mail:
061     * <pre><code>
062     * HtmlEmail he = new HtmlEmail();
063     * File img = new File("my/image.gif");
064     * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class
065     * StringBuffer msg = new StringBuffer();
066     * msg.append("&lt;html&gt;&lt;body&gt;");
067     * msg.append("&lt;img src=cid:").append(he.embed(img)).append("&gt;");
068     * msg.append("&lt;img src=cid:").append(he.embed(png)).append("&gt;");
069     * msg.append("&lt;/body&gt;&lt;/html&gt;");
070     * he.setHtmlMsg(msg.toString());
071     * // code to set the other email fields (not shown)
072     * </pre></code>
073     *
074     * <p>Embedded entities are tracked by their name, which for <code>File</code>s is
075     * the filename itself and for <code>URL</code>s is the canonical path. It is
076     * an error to bind the same name to more than one entity, and this class will
077     * attempt to validate that for <code>File</code>s and <code>URL</code>s. When
078     * embedding a <code>DataSource</code>, the code uses the <code>equals()</code>
079     * method defined on the <code>DataSource</code>s to make the determination.
080     *
081     * @since 1.0
082     * @author <a href="mailto:unknown">Regis Koenig</a>
083     * @author <a href="mailto:sean@informage.net">Sean Legassick</a>
084     * @version $Id: HtmlEmail.java 785383 2009-06-16 20:36:22Z sgoeschl $
085     */
086    public class HtmlEmail extends MultiPartEmail
087    {
088        /** Definition of the length of generated CID's */
089        public static final int CID_LENGTH = 10;
090    
091        /** prefix for default HTML mail */
092        private static final String HTML_MESSAGE_START = "<html><body><pre>";
093        /** suffix for default HTML mail */
094        private static final String HTML_MESSAGE_END = "</pre></body></html>";
095    
096    
097        /**
098         * Text part of the message.  This will be used as alternative text if
099         * the email client does not support HTML messages.
100         */
101        protected String text;
102    
103        /** Html part of the message */
104        protected String html;
105    
106        /**
107         * @deprecated As of commons-email 1.1, no longer used. Inline embedded
108         * objects are now stored in {@link #inlineEmbeds}.
109         */
110        protected List inlineImages;
111    
112        /**
113         * Embedded images Map<String, InlineImage> where the key is the
114         * user-defined image name.
115         */
116        protected Map inlineEmbeds = new HashMap();
117    
118        /**
119         * Set the text content.
120         *
121         * @param aText A String.
122         * @return An HtmlEmail.
123         * @throws EmailException see javax.mail.internet.MimeBodyPart
124         *  for definitions
125         * @since 1.0
126         */
127        public HtmlEmail setTextMsg(String aText) throws EmailException
128        {
129            if (EmailUtils.isEmpty(aText))
130            {
131                throw new EmailException("Invalid message supplied");
132            }
133    
134            this.text = aText;
135            return this;
136        }
137    
138        /**
139         * Set the HTML content.
140         *
141         * @param aHtml A String.
142         * @return An HtmlEmail.
143         * @throws EmailException see javax.mail.internet.MimeBodyPart
144         *  for definitions
145         * @since 1.0
146         */
147        public HtmlEmail setHtmlMsg(String aHtml) throws EmailException
148        {
149            if (EmailUtils.isEmpty(aHtml))
150            {
151                throw new EmailException("Invalid message supplied");
152            }
153    
154            this.html = aHtml;
155            return this;
156        }
157    
158        /**
159         * Set the message.
160         *
161         * <p>This method overrides {@link MultiPartEmail#setMsg(String)} in
162         * order to send an HTML message instead of a plain text message in
163         * the mail body. The message is formatted in HTML for the HTML
164         * part of the message; it is left as is in the alternate text
165         * part.
166         *
167         * @param msg the message text to use
168         * @return this <code>HtmlEmail</code>
169         * @throws EmailException if msg is null or empty;
170         * see javax.mail.internet.MimeBodyPart for definitions
171         * @since 1.0
172         */
173        public Email setMsg(String msg) throws EmailException
174        {
175            if (EmailUtils.isEmpty(msg))
176            {
177                throw new EmailException("Invalid message supplied");
178            }
179    
180            setTextMsg(msg);
181    
182            StringBuffer htmlMsgBuf = new StringBuffer(
183                msg.length()
184                + HTML_MESSAGE_START.length()
185                + HTML_MESSAGE_END.length()
186            );
187    
188            htmlMsgBuf.append(HTML_MESSAGE_START)
189                .append(msg)
190                .append(HTML_MESSAGE_END);
191    
192            setHtmlMsg(htmlMsgBuf.toString());
193    
194            return this;
195        }
196    
197        /**
198         * Attempts to parse the specified <code>String</code> as a URL that will
199         * then be embedded in the message.
200         *
201         * @param urlString String representation of the URL.
202         * @param name The name that will be set in the filename header field.
203         * @return A String with the Content-ID of the URL.
204         * @throws EmailException when URL supplied is invalid or if <code> is null
205         * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions
206         *
207         * @see #embed(URL, String)
208         * @since 1.1
209         */
210        public String embed(String urlString, String name) throws EmailException
211        {
212            try
213            {
214                return embed(new URL(urlString), name);
215            }
216            catch (MalformedURLException e)
217            {
218                throw new EmailException("Invalid URL", e);
219            }
220        }
221    
222        /**
223         * Embeds an URL in the HTML.
224         *
225         * <p>This method embeds a file located by an URL into
226         * the mail body. It allows, for instance, to add inline images
227         * to the email.  Inline files may be referenced with a
228         * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
229         * returned by the embed function. It is an error to bind the same name
230         * to more than one URL; if the same URL is embedded multiple times, the
231         * same Content-ID is guaranteed to be returned.
232         *
233         * <p>While functionally the same as passing <code>URLDataSource</code> to
234         * {@link #embed(DataSource, String, String)}, this method attempts
235         * to validate the URL before embedding it in the message and will throw
236         * <code>EmailException</code> if the validation fails. In this case, the
237         * <code>HtmlEmail</code> object will not be changed.
238         *
239         * <p>
240         * NOTE: Clients should take care to ensure that different URLs are bound to
241         * different names. This implementation tries to detect this and throw
242         * <code>EmailException</code>. However, it is not guaranteed to catch
243         * all cases, especially when the URL refers to a remote HTTP host that
244         * may be part of a virtual host cluster.
245         *
246         * @param url The URL of the file.
247         * @param name The name that will be set in the filename header
248         * field.
249         * @return A String with the Content-ID of the file.
250         * @throws EmailException when URL supplied is invalid or if <code> is null
251         * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions
252         * @since 1.0
253         */
254        public String embed(URL url, String name) throws EmailException
255        {
256            if (EmailUtils.isEmpty(name))
257            {
258                throw new EmailException("name cannot be null or empty");
259            }
260    
261            // check if a URLDataSource for this name has already been attached;
262            // if so, return the cached CID value.
263            if (inlineEmbeds.containsKey(name))
264            {
265                InlineImage ii = (InlineImage) inlineEmbeds.get(name);
266                URLDataSource urlDataSource = (URLDataSource) ii.getDataSource();
267                // make sure the supplied URL points to the same thing
268                // as the one already associated with this name.
269                // NOTE: Comparing URLs with URL.equals() is a blocking operation
270                // in the case of a network failure therefore we use
271                // url.toExternalForm().equals() here.
272                if (url.toExternalForm().equals(urlDataSource.getURL().toExternalForm()))
273                {
274                    return ii.getCid();
275                }
276                else
277                {
278                    throw new EmailException("embedded name '" + name
279                        + "' is already bound to URL " + urlDataSource.getURL()
280                        + "; existing names cannot be rebound");
281                }
282            }
283    
284            // verify that the URL is valid
285            InputStream is = null;
286            try
287            {
288                is = url.openStream();
289            }
290            catch (IOException e)
291            {
292                throw new EmailException("Invalid URL", e);
293            }
294            finally
295            {
296                try
297                {
298                    if (is != null)
299                    {
300                        is.close();
301                    }
302                }
303                catch (IOException ioe)
304                { /* sigh */ }
305            }
306    
307            return embed(new URLDataSource(url), name);
308        }
309    
310        /**
311         * Embeds a file in the HTML. This implementation delegates to
312         * {@link #embed(File, String)}.
313         *
314         * @param file The <code>File</code> object to embed
315         * @return A String with the Content-ID of the file.
316         * @throws EmailException when the supplied <code>File</code> cannot be
317         * used; also see {@link javax.mail.internet.MimeBodyPart} for definitions
318         *
319         * @see #embed(File, String)
320         * @since 1.1
321         */
322        public String embed(File file) throws EmailException
323        {
324            String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
325            return embed(file, cid);
326        }
327    
328        /**
329         * Embeds a file in the HTML.
330         *
331         * <p>This method embeds a file located by an URL into
332         * the mail body. It allows, for instance, to add inline images
333         * to the email.  Inline files may be referenced with a
334         * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
335         * returned by the embed function. Files are bound to their names, which is
336         * the value returned by {@link java.io.File#getName()}. If the same file
337         * is embedded multiple times, the same CID is guaranteed to be returned.
338         *
339         * <p>While functionally the same as passing <code>FileDataSource</code> to
340         * {@link #embed(DataSource, String, String)}, this method attempts
341         * to validate the file before embedding it in the message and will throw
342         * <code>EmailException</code> if the validation fails. In this case, the
343         * <code>HtmlEmail</code> object will not be changed.
344         *
345         * @param file The <code>File</code> to embed
346         * @param cid the Content-ID to use for the embedded <code>File</code>
347         * @return A String with the Content-ID of the file.
348         * @throws EmailException when the supplied <code>File</code> cannot be used
349         *  or if the file has already been embedded;
350         *  also see {@link javax.mail.internet.MimeBodyPart} for definitions
351         * @since 1.1
352         */
353        public String embed(File file, String cid) throws EmailException
354        {
355            if (EmailUtils.isEmpty(file.getName()))
356            {
357                throw new EmailException("file name cannot be null or empty");
358            }
359    
360            // verify that the File can provide a canonical path
361            String filePath = null;
362            try
363            {
364                filePath = file.getCanonicalPath();
365            }
366            catch (IOException ioe)
367            {
368                throw new EmailException("couldn't get canonical path for "
369                        + file.getName(), ioe);
370            }
371    
372            // check if a FileDataSource for this name has already been attached;
373            // if so, return the cached CID value.
374            if (inlineEmbeds.containsKey(file.getName()))
375            {
376                InlineImage ii = (InlineImage) inlineEmbeds.get(file.getName());
377                FileDataSource fileDataSource = (FileDataSource) ii.getDataSource();
378                // make sure the supplied file has the same canonical path
379                // as the one already associated with this name.
380                String existingFilePath = null;
381                try
382                {
383                    existingFilePath = fileDataSource.getFile().getCanonicalPath();
384                }
385                catch (IOException ioe)
386                {
387                    throw new EmailException("couldn't get canonical path for file "
388                            + fileDataSource.getFile().getName()
389                            + "which has already been embedded", ioe);
390                }
391                if (filePath.equals(existingFilePath))
392                {
393                    return ii.getCid();
394                }
395                else
396                {
397                    throw new EmailException("embedded name '" + file.getName()
398                        + "' is already bound to file " + existingFilePath
399                        + "; existing names cannot be rebound");
400                }
401            }
402    
403            // verify that the file is valid
404            if (!file.exists())
405            {
406                throw new EmailException("file " + filePath + " doesn't exist");
407            }
408            if (!file.isFile())
409            {
410                throw new EmailException("file " + filePath + " isn't a normal file");
411            }
412            if (!file.canRead())
413            {
414                throw new EmailException("file " + filePath + " isn't readable");
415            }
416    
417            return embed(new FileDataSource(file), file.getName());
418        }
419    
420        /**
421         * Embeds the specified <code>DataSource</code> in the HTML using a
422         * randomly generated Content-ID. Returns the generated Content-ID string.
423         *
424         * @param dataSource the <code>DataSource</code> to embed
425         * @param name the name that will be set in the filename header field
426         * @return the generated Content-ID for this <code>DataSource</code>
427         * @throws EmailException if the embedding fails or if <code>name</code> is
428         * null or empty
429         * @see #embed(DataSource, String, String)
430         * @since 1.1
431         */
432        public String embed(DataSource dataSource, String name) throws EmailException
433        {
434            // check if the DataSource has already been attached;
435            // if so, return the cached CID value.
436            if (inlineEmbeds.containsKey(name))
437            {
438                InlineImage ii = (InlineImage) inlineEmbeds.get(name);
439                // make sure the supplied URL points to the same thing
440                // as the one already associated with this name.
441                if (dataSource.equals(ii.getDataSource()))
442                {
443                    return ii.getCid();
444                }
445                else
446                {
447                    throw new EmailException("embedded DataSource '" + name
448                        + "' is already bound to name " + ii.getDataSource().toString()
449                        + "; existing names cannot be rebound");
450                }
451            }
452    
453            String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
454            return embed(dataSource, name, cid);
455        }
456    
457        /**
458         * Embeds the specified <code>DataSource</code> in the HTML using the
459         * specified Content-ID. Returns the specified Content-ID string.
460         *
461         * @param dataSource the <code>DataSource</code> to embed
462         * @param name the name that will be set in the filename header field
463         * @param cid the Content-ID to use for this <code>DataSource</code>
464         * @return the supplied Content-ID for this <code>DataSource</code>
465         * @throws EmailException if the embedding fails or if <code>name</code> is
466         * null or empty
467         * @since 1.1
468         */
469        public String embed(DataSource dataSource, String name, String cid)
470            throws EmailException
471        {
472            if (EmailUtils.isEmpty(name))
473            {
474                throw new EmailException("name cannot be null or empty");
475            }
476    
477            MimeBodyPart mbp = new MimeBodyPart();
478    
479            try
480            {
481                mbp.setDataHandler(new DataHandler(dataSource));
482                mbp.setFileName(name);
483                mbp.setDisposition("inline");
484                mbp.setContentID("<" + cid + ">");
485    
486                InlineImage ii = new InlineImage(cid, dataSource, mbp);
487                this.inlineEmbeds.put(name, ii);
488    
489                return cid;
490            }
491            catch (MessagingException me)
492            {
493                throw new EmailException(me);
494            }
495        }
496    
497        /**
498         * Does the work of actually building the email.
499         *
500         * @exception EmailException if there was an error.
501         * @since 1.0
502         */
503        public void buildMimeMessage() throws EmailException
504        {
505            try
506            {
507                build();
508            }
509            catch (MessagingException me)
510            {
511                throw new EmailException(me);
512            }
513            super.buildMimeMessage();
514        }
515    
516        /**
517         * @throws EmailException EmailException
518         * @throws MessagingException MessagingException
519         */
520        private void build() throws MessagingException, EmailException
521        {
522            MimeMultipart rootContainer = this.getContainer();
523            MimeMultipart bodyEmbedsContainer = rootContainer;
524            MimeMultipart bodyContainer = rootContainer;
525            BodyPart msgHtml = null;
526            BodyPart msgText = null;
527    
528            rootContainer.setSubType("mixed");
529    
530            // determine how to form multiparts of email
531    
532            if (EmailUtils.isNotEmpty(this.html) && this.inlineEmbeds.size() > 0)
533            {
534                //If HTML body and embeds are used, create a related container and add it to the root container
535                bodyEmbedsContainer = new MimeMultipart("related");
536                bodyContainer = bodyEmbedsContainer;
537                this.addPart(bodyEmbedsContainer, 0);
538    
539                //If TEXT body was specified, create a alternative container and add it to the embeds container
540                if (EmailUtils.isNotEmpty(this.text))
541                {
542                    bodyContainer = new MimeMultipart("alternative");
543                    BodyPart bodyPart = createBodyPart();
544                    try
545                    {
546                        bodyPart.setContent(bodyContainer);
547                        bodyEmbedsContainer.addBodyPart(bodyPart, 0);
548                    }
549                    catch (MessagingException me)
550                    {
551                        throw new EmailException(me);
552                    }
553                }
554            }
555            else if (EmailUtils.isNotEmpty(this.text) && EmailUtils.isNotEmpty(this.html))
556            {
557                //If both HTML and TEXT bodies are provided, create a alternative container and add it to the root container
558                bodyContainer = new MimeMultipart("alternative");
559                this.addPart(bodyContainer, 0);
560            }
561    
562            if (EmailUtils.isNotEmpty(this.html))
563            {
564                msgHtml = new MimeBodyPart();
565                bodyContainer.addBodyPart(msgHtml, 0);
566    
567                // apply default charset if one has been set
568                if (EmailUtils.isNotEmpty(this.charset))
569                {
570                    msgHtml.setContent(
571                        this.html,
572                        Email.TEXT_HTML + "; charset=" + this.charset);
573                }
574                else
575                {
576                    msgHtml.setContent(this.html, Email.TEXT_HTML);
577                }
578    
579                Iterator iter = this.inlineEmbeds.values().iterator();
580                while (iter.hasNext())
581                {
582                    InlineImage ii = (InlineImage) iter.next();
583                    bodyEmbedsContainer.addBodyPart(ii.getMbp());
584                }
585            }
586    
587            if (EmailUtils.isNotEmpty(this.text))
588            {
589                msgText = new MimeBodyPart();
590                bodyContainer.addBodyPart(msgText, 0);
591    
592                // apply default charset if one has been set
593                if (EmailUtils.isNotEmpty(this.charset))
594                {
595                    msgText.setContent(
596                        this.text,
597                        Email.TEXT_PLAIN + "; charset=" + this.charset);
598                }
599                else
600                {
601                    msgText.setContent(this.text, Email.TEXT_PLAIN);
602                }
603            }
604        }
605    
606        /**
607         * Private bean class that encapsulates data about URL contents
608         * that are embedded in the final email.
609         * @since 1.1
610         */
611        private static class InlineImage
612        {
613            /** content id */
614            private String cid;
615            /** <code>DataSource</code> for the content */
616            private DataSource dataSource;
617            /** the <code>MimeBodyPart</code> that contains the encoded data */
618            private MimeBodyPart mbp;
619    
620            /**
621             * Creates an InlineImage object to represent the
622             * specified content ID and <code>MimeBodyPart</code>.
623             * @param cid the generated content ID
624             * @param dataSource the <code>DataSource</code> that represents the content
625             * @param mbp the <code>MimeBodyPart</code> that contains the encoded
626             * data
627             */
628            public InlineImage(String cid, DataSource dataSource, MimeBodyPart mbp)
629            {
630                this.cid = cid;
631                this.dataSource = dataSource;
632                this.mbp = mbp;
633            }
634    
635            /**
636             * Returns the unique content ID of this InlineImage.
637             * @return the unique content ID of this InlineImage
638             */
639            public String getCid()
640            {
641                return cid;
642            }
643    
644            /**
645             * Returns the <code>DataSource</code> that represents the encoded content.
646             * @return the <code>DataSource</code> representing the encoded content
647             */
648            public DataSource getDataSource()
649            {
650                return dataSource;
651            }
652    
653            /**
654             * Returns the <code>MimeBodyPart</code> that contains the
655             * encoded InlineImage data.
656             * @return the <code>MimeBodyPart</code> containing the encoded
657             * InlineImage data
658             */
659            public MimeBodyPart getMbp()
660            {
661                return mbp;
662            }
663    
664            // equals()/hashCode() implementations, since this class
665            // is stored as a entry in a Map.
666            /**
667             * {@inheritDoc}
668             * @return true if the other object is also an InlineImage with the same cid.
669             */
670            public boolean equals(Object obj)
671            {
672                if (this == obj)
673                {
674                    return true;
675                }
676                if (!(obj instanceof InlineImage))
677                {
678                    return false;
679                }
680    
681                InlineImage that = (InlineImage) obj;
682    
683                return this.cid.equals(that.cid);
684            }
685    
686            /**
687             * {@inheritDoc}
688             * @return the cid hashCode.
689             */
690            public int hashCode()
691            {
692                return cid.hashCode();
693            }
694        }
695    }