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 */
017package org.apache.commons.mail;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.UnsupportedEncodingException;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029
030import javax.activation.DataHandler;
031import javax.activation.DataSource;
032import javax.activation.FileDataSource;
033import javax.activation.URLDataSource;
034import javax.mail.BodyPart;
035import javax.mail.MessagingException;
036import javax.mail.internet.MimeBodyPart;
037import javax.mail.internet.MimeMultipart;
038
039/**
040 * An HTML multipart email.
041 *
042 * <p>This class is used to send HTML formatted email.  A text message
043 * can also be set for HTML unaware email clients, such as text-based
044 * email clients.
045 *
046 * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to
047 * add attachments to the email.
048 *
049 * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then
050 * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods.
051 * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The
052 * alternative text content can be set with {@link #setTextMsg(String)}.
053 *
054 * <p>Either the text or HTML can be omitted, in which case the "main"
055 * part of the multipart becomes whichever is supplied rather than a
056 * <code>multipart/alternative</code>.
057 *
058 * <h3>Embedding Images and Media</h3>
059 *
060 * <p>It is also possible to embed URLs, files, or arbitrary
061 * <code>DataSource</code>s directly into the body of the mail:
062 * <pre><code>
063 * HtmlEmail he = new HtmlEmail();
064 * File img = new File("my/image.gif");
065 * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class
066 * StringBuffer msg = new StringBuffer();
067 * msg.append("&lt;html&gt;&lt;body&gt;");
068 * msg.append("&lt;img src=cid:").append(he.embed(img)).append("&gt;");
069 * msg.append("&lt;img src=cid:").append(he.embed(png)).append("&gt;");
070 * msg.append("&lt;/body&gt;&lt;/html&gt;");
071 * he.setHtmlMsg(msg.toString());
072 * // code to set the other email fields (not shown)
073 * </pre></code>
074 *
075 * <p>Embedded entities are tracked by their name, which for <code>File</code>s is
076 * the filename itself and for <code>URL</code>s is the canonical path. It is
077 * an error to bind the same name to more than one entity, and this class will
078 * attempt to validate that for <code>File</code>s and <code>URL</code>s. When
079 * embedding a <code>DataSource</code>, the code uses the <code>equals()</code>
080 * method defined on the <code>DataSource</code>s to make the determination.
081 *
082 * @since 1.0
083 * @version $Id: HtmlEmail.java 1532531 2013-10-15 21:13:11Z tn $
084 */
085public class HtmlEmail extends MultiPartEmail
086{
087    /** Definition of the length of generated CID's. */
088    public static final int CID_LENGTH = 10;
089
090    /** prefix for default HTML mail. */
091    private static final String HTML_MESSAGE_START = "<html><body><pre>";
092    /** suffix for default HTML mail. */
093    private static final String HTML_MESSAGE_END = "</pre></body></html>";
094
095
096    /**
097     * Text part of the message. This will be used as alternative text if
098     * the email client does not support HTML messages.
099     */
100    protected String text;
101
102    /** Html part of the message. */
103    protected String html;
104
105    /**
106     * @deprecated As of commons-email 1.1, no longer used. Inline embedded
107     * objects are now stored in {@link #inlineEmbeds}.
108     */
109    @Deprecated
110    protected List<InlineImage> inlineImages;
111
112    /**
113     * Embedded images Map<String, InlineImage> where the key is the
114     * user-defined image name.
115     */
116    protected Map<String, InlineImage> inlineEmbeds = new HashMap<String, InlineImage>();
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    @Override
174    public Email setMsg(String msg) throws EmailException
175    {
176        if (EmailUtils.isEmpty(msg))
177        {
178            throw new EmailException("Invalid message supplied");
179        }
180
181        setTextMsg(msg);
182
183        StringBuffer htmlMsgBuf = new StringBuffer(
184            msg.length()
185            + HTML_MESSAGE_START.length()
186            + HTML_MESSAGE_END.length()
187        );
188
189        htmlMsgBuf.append(HTML_MESSAGE_START)
190            .append(msg)
191            .append(HTML_MESSAGE_END);
192
193        setHtmlMsg(htmlMsgBuf.toString());
194
195        return this;
196    }
197
198    /**
199     * Attempts to parse the specified <code>String</code> as a URL that will
200     * then be embedded in the message.
201     *
202     * @param urlString String representation of the URL.
203     * @param name The name that will be set in the filename header field.
204     * @return A String with the Content-ID of the URL.
205     * @throws EmailException when URL supplied is invalid or if {@code name} is null
206     * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions
207     *
208     * @see #embed(URL, String)
209     * @since 1.1
210     */
211    public String embed(String urlString, String name) throws EmailException
212    {
213        try
214        {
215            return embed(new URL(urlString), name);
216        }
217        catch (MalformedURLException e)
218        {
219            throw new EmailException("Invalid URL", e);
220        }
221    }
222
223    /**
224     * Embeds an URL in the HTML.
225     *
226     * <p>This method embeds a file located by an URL into
227     * the mail body. It allows, for instance, to add inline images
228     * to the email.  Inline files may be referenced with a
229     * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
230     * returned by the embed function. It is an error to bind the same name
231     * to more than one URL; if the same URL is embedded multiple times, the
232     * same Content-ID is guaranteed to be returned.
233     *
234     * <p>While functionally the same as passing <code>URLDataSource</code> to
235     * {@link #embed(DataSource, String, String)}, this method attempts
236     * to validate the URL before embedding it in the message and will throw
237     * <code>EmailException</code> if the validation fails. In this case, the
238     * <code>HtmlEmail</code> object will not be changed.
239     *
240     * <p>
241     * NOTE: Clients should take care to ensure that different URLs are bound to
242     * different names. This implementation tries to detect this and throw
243     * <code>EmailException</code>. However, it is not guaranteed to catch
244     * all cases, especially when the URL refers to a remote HTTP host that
245     * may be part of a virtual host cluster.
246     *
247     * @param url The URL of the file.
248     * @param name The name that will be set in the filename header
249     * field.
250     * @return A String with the Content-ID of the file.
251     * @throws EmailException when URL supplied is invalid or if {@code name} is null
252     * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions
253     * @since 1.0
254     */
255    public String embed(URL url, String name) throws EmailException
256    {
257        if (EmailUtils.isEmpty(name))
258        {
259            throw new EmailException("name cannot be null or empty");
260        }
261
262        // check if a URLDataSource for this name has already been attached;
263        // if so, return the cached CID value.
264        if (inlineEmbeds.containsKey(name))
265        {
266            InlineImage ii = inlineEmbeds.get(name);
267            URLDataSource urlDataSource = (URLDataSource) ii.getDataSource();
268            // make sure the supplied URL points to the same thing
269            // as the one already associated with this name.
270            // NOTE: Comparing URLs with URL.equals() is a blocking operation
271            // in the case of a network failure therefore we use
272            // url.toExternalForm().equals() here.
273            if (url.toExternalForm().equals(urlDataSource.getURL().toExternalForm()))
274            {
275                return ii.getCid();
276            }
277            else
278            {
279                throw new EmailException("embedded name '" + name
280                    + "' is already bound to URL " + urlDataSource.getURL()
281                    + "; existing names cannot be rebound");
282            }
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) // NOPMD
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(Locale.ENGLISH);
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 = 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(), cid);
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 = 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 URL encoded 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            // url encode the cid according to rfc 2392
483            cid = EmailUtils.encodeUrl(cid);
484
485            mbp.setDataHandler(new DataHandler(dataSource));
486            mbp.setFileName(name);
487            mbp.setDisposition(EmailAttachment.INLINE);
488            mbp.setContentID("<" + cid + ">");
489
490            InlineImage ii = new InlineImage(cid, dataSource, mbp);
491            this.inlineEmbeds.put(name, ii);
492
493            return cid;
494        }
495        catch (MessagingException me)
496        {
497            throw new EmailException(me);
498        }
499        catch (UnsupportedEncodingException uee)
500        {
501            throw new EmailException(uee);
502        }
503    }
504
505    /**
506     * Does the work of actually building the MimeMessage. Please note that
507     * a user rarely calls this method directly and only if he/she is
508     * interested in the sending the underlying MimeMessage without
509     * commons-email.
510     *
511     * @exception EmailException if there was an error.
512     * @since 1.0
513     */
514    @Override
515    public void buildMimeMessage() throws EmailException
516    {
517        try
518        {
519            build();
520        }
521        catch (MessagingException me)
522        {
523            throw new EmailException(me);
524        }
525        super.buildMimeMessage();
526    }
527
528    /**
529     * @throws EmailException EmailException
530     * @throws MessagingException MessagingException
531     */
532    private void build() throws MessagingException, EmailException
533    {
534        MimeMultipart rootContainer = this.getContainer();
535        MimeMultipart bodyEmbedsContainer = rootContainer;
536        MimeMultipart bodyContainer = rootContainer;
537        MimeBodyPart msgHtml = null;
538        MimeBodyPart msgText = null;
539
540        rootContainer.setSubType("mixed");
541
542        // determine how to form multiparts of email
543
544        if (EmailUtils.isNotEmpty(this.html) && this.inlineEmbeds.size() > 0)
545        {
546            //If HTML body and embeds are used, create a related container and add it to the root container
547            bodyEmbedsContainer = new MimeMultipart("related");
548            bodyContainer = bodyEmbedsContainer;
549            this.addPart(bodyEmbedsContainer, 0);
550
551            //If TEXT body was specified, create a alternative container and add it to the embeds container
552            if (EmailUtils.isNotEmpty(this.text))
553            {
554                bodyContainer = new MimeMultipart("alternative");
555                BodyPart bodyPart = createBodyPart();
556                try
557                {
558                    bodyPart.setContent(bodyContainer);
559                    bodyEmbedsContainer.addBodyPart(bodyPart, 0);
560                }
561                catch (MessagingException me)
562                {
563                    throw new EmailException(me);
564                }
565            }
566        }
567        else if (EmailUtils.isNotEmpty(this.text) && EmailUtils.isNotEmpty(this.html))
568        {
569            //If both HTML and TEXT bodies are provided, create a alternative container and add it to the root container
570            bodyContainer = new MimeMultipart("alternative");
571            this.addPart(bodyContainer, 0);
572        }
573
574        if (EmailUtils.isNotEmpty(this.html))
575        {
576            msgHtml = new MimeBodyPart();
577            bodyContainer.addBodyPart(msgHtml, 0);
578
579            // EMAIL-104: call explicitly setText to use default mime charset
580            //            (property "mail.mime.charset") in case none has been set
581            msgHtml.setText(this.html, this.charset, EmailConstants.TEXT_SUBTYPE_HTML);
582
583            for (InlineImage image : this.inlineEmbeds.values())
584            {
585                bodyEmbedsContainer.addBodyPart(image.getMbp());
586            }
587        }
588
589        if (EmailUtils.isNotEmpty(this.text))
590        {
591            msgText = new MimeBodyPart();
592            bodyContainer.addBodyPart(msgText, 0);
593
594            // EMAIL-104: call explicitly setText to use default mime charset
595            //            (property "mail.mime.charset") in case none has been set
596            msgText.setText(this.text, this.charset);
597        }
598    }
599
600    /**
601     * Private bean class that encapsulates data about URL contents
602     * that are embedded in the final email.
603     * @since 1.1
604     */
605    private static class InlineImage
606    {
607        /** content id. */
608        private String cid;
609        /** <code>DataSource</code> for the content. */
610        private DataSource dataSource;
611        /** the <code>MimeBodyPart</code> that contains the encoded data. */
612        private MimeBodyPart mbp;
613
614        /**
615         * Creates an InlineImage object to represent the
616         * specified content ID and <code>MimeBodyPart</code>.
617         * @param cid the generated content ID
618         * @param dataSource the <code>DataSource</code> that represents the content
619         * @param mbp the <code>MimeBodyPart</code> that contains the encoded
620         * data
621         */
622        public InlineImage(String cid, DataSource dataSource, MimeBodyPart mbp)
623        {
624            this.cid = cid;
625            this.dataSource = dataSource;
626            this.mbp = mbp;
627        }
628
629        /**
630         * Returns the unique content ID of this InlineImage.
631         * @return the unique content ID of this InlineImage
632         */
633        public String getCid()
634        {
635            return cid;
636        }
637
638        /**
639         * Returns the <code>DataSource</code> that represents the encoded content.
640         * @return the <code>DataSource</code> representing the encoded content
641         */
642        public DataSource getDataSource()
643        {
644            return dataSource;
645        }
646
647        /**
648         * Returns the <code>MimeBodyPart</code> that contains the
649         * encoded InlineImage data.
650         * @return the <code>MimeBodyPart</code> containing the encoded
651         * InlineImage data
652         */
653        public MimeBodyPart getMbp()
654        {
655            return mbp;
656        }
657
658        // equals()/hashCode() implementations, since this class
659        // is stored as a entry in a Map.
660        /**
661         * {@inheritDoc}
662         * @return true if the other object is also an InlineImage with the same cid.
663         */
664        @Override
665        public boolean equals(Object obj)
666        {
667            if (this == obj)
668            {
669                return true;
670            }
671            if (!(obj instanceof InlineImage))
672            {
673                return false;
674            }
675
676            InlineImage that = (InlineImage) obj;
677
678            return this.cid.equals(that.cid);
679        }
680
681        /**
682         * {@inheritDoc}
683         * @return the cid hashCode.
684         */
685        @Override
686        public int hashCode()
687        {
688            return cid.hashCode();
689        }
690    }
691}