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.net.MalformedURLException;
023import java.net.URL;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028
029import javax.activation.DataHandler;
030import javax.activation.DataSource;
031import javax.activation.FileDataSource;
032import javax.activation.URLDataSource;
033import javax.mail.BodyPart;
034import javax.mail.MessagingException;
035import javax.mail.internet.MimeBodyPart;
036import 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 */
086public 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}