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 578501 2007-09-22 21:18:49Z bspeakmon $
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                if (url.equals(urlDataSource.getURL()))
270                {
271                    return ii.getCid();
272                }
273                else
274                {
275                    throw new EmailException("embedded name '" + name
276                        + "' is already bound to URL " + urlDataSource.getURL()
277                        + "; existing names cannot be rebound");
278                }
279                // NOTE: Comparing URLs with URL.equals() is known to be
280                // inconsistent when dealing with virtual hosting over HTTP,
281                // but since these are almost always files on the local machine,
282                // using equals() should be sufficient.
283            }
284    
285            // verify that the URL is valid
286            InputStream is = null;
287            try
288            {
289                is = url.openStream();
290            }
291            catch (IOException e)
292            {
293                throw new EmailException("Invalid URL", e);
294            }
295            finally
296            {
297                try
298                {
299                    if (is != null)
300                    {
301                        is.close();
302                    }
303                }
304                catch (IOException ioe)
305                { /* sigh */ }
306            }
307    
308            return embed(new URLDataSource(url), name);
309        }
310    
311        /**
312         * Embeds a file in the HTML. This implementation delegates to
313         * {@link #embed(File, String)}.
314         *
315         * @param file The <code>File</code> object to embed
316         * @return A String with the Content-ID of the file.
317         * @throws EmailException when the supplied <code>File</code> cannot be
318         * used; also see {@link javax.mail.internet.MimeBodyPart} for definitions
319         *
320         * @see #embed(File, String)
321         * @since 1.1
322         */
323        public String embed(File file) throws EmailException
324        {
325            String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
326            return embed(file, cid);
327        }
328    
329        /**
330         * Embeds a file in the HTML.
331         *
332         * <p>This method embeds a file located by an URL into
333         * the mail body. It allows, for instance, to add inline images
334         * to the email.  Inline files may be referenced with a
335         * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
336         * returned by the embed function. Files are bound to their names, which is
337         * the value returned by {@link java.io.File#getName()}. If the same file
338         * is embedded multiple times, the same CID is guaranteed to be returned.
339         *
340         * <p>While functionally the same as passing <code>FileDataSource</code> to
341         * {@link #embed(DataSource, String, String)}, this method attempts
342         * to validate the file before embedding it in the message and will throw
343         * <code>EmailException</code> if the validation fails. In this case, the
344         * <code>HtmlEmail</code> object will not be changed.
345         *
346         * @param file The <code>File</code> to embed
347         * @param cid the Content-ID to use for the embedded <code>File</code>
348         * @return A String with the Content-ID of the file.
349         * @throws EmailException when the supplied <code>File</code> cannot be used
350         *  or if the file has already been embedded;
351         *  also see {@link javax.mail.internet.MimeBodyPart} for definitions
352         * @since 1.1
353         */
354        public String embed(File file, String cid) throws EmailException
355        {
356            if (EmailUtils.isEmpty(file.getName()))
357            {
358                throw new EmailException("file name cannot be null or empty");
359            }
360    
361            // verify that the File can provide a canonical path
362            String filePath = null;
363            try
364            {
365                filePath = file.getCanonicalPath();
366            }
367            catch (IOException ioe)
368            {
369                throw new EmailException("couldn't get canonical path for "
370                        + file.getName(), ioe);
371            }
372    
373            // check if a FileDataSource for this name has already been attached;
374            // if so, return the cached CID value.
375            if (inlineEmbeds.containsKey(file.getName()))
376            {
377                InlineImage ii = (InlineImage) inlineEmbeds.get(file.getName());
378                FileDataSource fileDataSource = (FileDataSource) ii.getDataSource();
379                // make sure the supplied file has the same canonical path
380                // as the one already associated with this name.
381                String existingFilePath = null;
382                try
383                {
384                    existingFilePath = fileDataSource.getFile().getCanonicalPath();
385                }
386                catch (IOException ioe)
387                {
388                    throw new EmailException("couldn't get canonical path for file "
389                            + fileDataSource.getFile().getName()
390                            + "which has already been embedded", ioe);
391                }
392                if (filePath.equals(existingFilePath))
393                {
394                    return ii.getCid();
395                }
396                else
397                {
398                    throw new EmailException("embedded name '" + file.getName()
399                        + "' is already bound to file " + existingFilePath
400                        + "; existing names cannot be rebound");
401                }
402            }
403    
404            // verify that the file is valid
405            if (!file.exists())
406            {
407                throw new EmailException("file " + filePath + " doesn't exist");
408            }
409            if (!file.isFile())
410            {
411                throw new EmailException("file " + filePath + " isn't a normal file");
412            }
413            if (!file.canRead())
414            {
415                throw new EmailException("file " + filePath + " isn't readable");
416            }
417    
418            return embed(new FileDataSource(file), file.getName());
419        }
420    
421        /**
422         * Embeds the specified <code>DataSource</code> in the HTML using a
423         * randomly generated Content-ID. Returns the generated Content-ID string.
424         *
425         * @param dataSource the <code>DataSource</code> to embed
426         * @param name the name that will be set in the filename header field
427         * @return the generated Content-ID for this <code>DataSource</code>
428         * @throws EmailException if the embedding fails or if <code>name</code> is
429         * null or empty
430         * @see #embed(DataSource, String, String)
431         * @since 1.1
432         */
433        public String embed(DataSource dataSource, String name) throws EmailException
434        {
435            // check if the DataSource has already been attached;
436            // if so, return the cached CID value.
437            if (inlineEmbeds.containsKey(name))
438            {
439                InlineImage ii = (InlineImage) inlineEmbeds.get(name);
440                // make sure the supplied URL points to the same thing
441                // as the one already associated with this name.
442                if (dataSource.equals(ii.getDataSource()))
443                {
444                    return ii.getCid();
445                }
446                else
447                {
448                    throw new EmailException("embedded DataSource '" + name
449                        + "' is already bound to name " + ii.getDataSource().toString()
450                        + "; existing names cannot be rebound");
451                }
452            }
453    
454            String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
455            return embed(dataSource, name, cid);
456        }
457    
458        /**
459         * Embeds the specified <code>DataSource</code> in the HTML using the
460         * specified Content-ID. Returns the specified Content-ID string.
461         *
462         * @param dataSource the <code>DataSource</code> to embed
463         * @param name the name that will be set in the filename header field
464         * @param cid the Content-ID to use for this <code>DataSource</code>
465         * @return the supplied Content-ID for this <code>DataSource</code>
466         * @throws EmailException if the embedding fails or if <code>name</code> is
467         * null or empty
468         * @since 1.1
469         */
470        public String embed(DataSource dataSource, String name, String cid)
471            throws EmailException
472        {
473            if (EmailUtils.isEmpty(name))
474            {
475                throw new EmailException("name cannot be null or empty");
476            }
477    
478            MimeBodyPart mbp = new MimeBodyPart();
479    
480            try
481            {
482                mbp.setDataHandler(new DataHandler(dataSource));
483                mbp.setFileName(name);
484                mbp.setDisposition("inline");
485                mbp.setContentID("<" + cid + ">");
486    
487                InlineImage ii = new InlineImage(cid, dataSource, mbp);
488                this.inlineEmbeds.put(name, ii);
489    
490                return cid;
491            }
492            catch (MessagingException me)
493            {
494                throw new EmailException(me);
495            }
496        }
497    
498        /**
499         * Does the work of actually building the email.
500         *
501         * @exception EmailException if there was an error.
502         * @since 1.0
503         */
504        public void buildMimeMessage() throws EmailException
505        {
506            try
507            {
508                build();
509            }
510            catch (MessagingException me)
511            {
512                throw new EmailException(me);
513            }
514            super.buildMimeMessage();
515        }
516    
517        /**
518         * @throws EmailException EmailException
519         * @throws MessagingException MessagingException
520         */
521        private void build() throws MessagingException, EmailException
522        {
523            MimeMultipart container = this.getContainer();
524            MimeMultipart subContainer = null;
525            BodyPart msgHtml = null;
526            BodyPart msgText = null;
527    
528            container.setSubType("related");
529            subContainer = new MimeMultipart("alternative");
530    
531            if (EmailUtils.isNotEmpty(this.text))
532            {
533                msgText = new MimeBodyPart();
534                if (this.inlineEmbeds.size() > 0)
535                {
536                    subContainer.addBodyPart(msgText);
537                }
538                else
539                {
540                    container.addBodyPart(msgText);
541                }
542    
543                // apply default charset if one has been set
544                if (EmailUtils.isNotEmpty(this.charset))
545                {
546                    msgText.setContent(
547                        this.text,
548                        Email.TEXT_PLAIN + "; charset=" + this.charset);
549                }
550                else
551                {
552                    msgText.setContent(this.text, Email.TEXT_PLAIN);
553                }
554            }
555    
556            if (EmailUtils.isNotEmpty(this.html))
557            {
558                msgHtml = new MimeBodyPart();
559                if (this.inlineEmbeds.size() > 0)
560                {
561                    subContainer.addBodyPart(msgHtml);
562                }
563                else
564                {
565                    container.addBodyPart(msgHtml);
566                }
567    
568                // apply default charset if one has been set
569                if (EmailUtils.isNotEmpty(this.charset))
570                {
571                    msgHtml.setContent(
572                        this.html,
573                        Email.TEXT_HTML + "; charset=" + this.charset);
574                }
575                else
576                {
577                    msgHtml.setContent(this.html, Email.TEXT_HTML);
578                }
579    
580                Iterator iter = this.inlineEmbeds.values().iterator();
581                while (iter.hasNext())
582                {
583                    InlineImage ii = (InlineImage) iter.next();
584                    container.addBodyPart(ii.getMbp());
585                }
586            }
587    
588            if (this.inlineEmbeds.size() > 0)
589            {
590                // add sub container to message
591                this.addPart(subContainer, 0);
592            }
593        }
594    
595        /**
596         * Private bean class that encapsulates data about URL contents
597         * that are embedded in the final email.
598         * @since 1.1
599         */
600        private static class InlineImage
601        {
602            /** content id */
603            private String cid;
604            /** <code>DataSource</code> for the content */
605            private DataSource dataSource;
606            /** the <code>MimeBodyPart</code> that contains the encoded data */
607            private MimeBodyPart mbp;
608    
609            /**
610             * Creates an InlineImage object to represent the
611             * specified content ID and <code>MimeBodyPart</code>.
612             * @param cid the generated content ID
613             * @param dataSource the <code>DataSource</code> that represents the content
614             * @param mbp the <code>MimeBodyPart</code> that contains the encoded
615             * data
616             */
617            public InlineImage(String cid, DataSource dataSource, MimeBodyPart mbp)
618            {
619                this.cid = cid;
620                this.dataSource = dataSource;
621                this.mbp = mbp;
622            }
623    
624            /**
625             * Returns the unique content ID of this InlineImage.
626             * @return the unique content ID of this InlineImage
627             */
628            public String getCid()
629            {
630                return cid;
631            }
632    
633            /**
634             * Returns the <code>DataSource</code> that represents the encoded content.
635             * @return the <code>DataSource</code> representing the encoded content
636             */
637            public DataSource getDataSource()
638            {
639                return dataSource;
640            }
641    
642            /**
643             * Returns the <code>MimeBodyPart</code> that contains the
644             * encoded InlineImage data.
645             * @return the <code>MimeBodyPart</code> containing the encoded
646             * InlineImage data
647             */
648            public MimeBodyPart getMbp()
649            {
650                return mbp;
651            }
652    
653            // equals()/hashCode() implementations, since this class
654            // is stored as a entry in a Map.
655            /**
656             * {@inheritDoc}
657             * @return true if the other object is also an InlineImage with the same cid.
658             */
659            public boolean equals(Object obj)
660            {
661                if (this == obj)
662                {
663                    return true;
664                }
665                if (!(obj instanceof InlineImage))
666                {
667                    return false;
668                }
669    
670                InlineImage that = (InlineImage) obj;
671    
672                return this.cid.equals(that.cid);
673            }
674    
675            /**
676             * {@inheritDoc}
677             * @return the cid hashCode.
678             */
679            public int hashCode()
680            {
681                return cid.hashCode();
682            }
683        }
684    }