View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.mail;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.UnsupportedEncodingException;
23  import java.net.MalformedURLException;
24  import java.net.URL;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Locale;
28  import java.util.Map;
29  
30  import javax.activation.DataHandler;
31  import javax.activation.DataSource;
32  import javax.activation.FileDataSource;
33  import javax.activation.URLDataSource;
34  import javax.mail.BodyPart;
35  import javax.mail.MessagingException;
36  import javax.mail.internet.MimeBodyPart;
37  import javax.mail.internet.MimeMultipart;
38  
39  /**
40   * An HTML multipart email.
41   *
42   * <p>This class is used to send HTML formatted email.  A text message
43   * can also be set for HTML unaware email clients, such as text-based
44   * email clients.
45   *
46   * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to
47   * add attachments to the email.
48   *
49   * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then
50   * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods.
51   * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The
52   * alternative text content can be set with {@link #setTextMsg(String)}.
53   *
54   * <p>Either the text or HTML can be omitted, in which case the "main"
55   * part of the multipart becomes whichever is supplied rather than a
56   * <code>multipart/alternative</code>.
57   *
58   * <h3>Embedding Images and Media</h3>
59   *
60   * <p>It is also possible to embed URLs, files, or arbitrary
61   * <code>DataSource</code>s directly into the body of the mail:
62   * <pre>
63   * HtmlEmail he = new HtmlEmail();
64   * File img = new File("my/image.gif");
65   * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class
66   * StringBuffer msg = new StringBuffer();
67   * msg.append("&lt;html&gt;&lt;body&gt;");
68   * msg.append("&lt;img src=cid:").append(he.embed(img)).append("&gt;");
69   * msg.append("&lt;img src=cid:").append(he.embed(png)).append("&gt;");
70   * msg.append("&lt;/body&gt;&lt;/html&gt;");
71   * he.setHtmlMsg(msg.toString());
72   * // code to set the other email fields (not shown)
73   * </pre>
74   *
75   * <p>Embedded entities are tracked by their name, which for <code>File</code>s is
76   * the filename itself and for <code>URL</code>s is the canonical path. It is
77   * an error to bind the same name to more than one entity, and this class will
78   * attempt to validate that for <code>File</code>s and <code>URL</code>s. When
79   * embedding a <code>DataSource</code>, the code uses the <code>equals()</code>
80   * method defined on the <code>DataSource</code>s to make the determination.
81   *
82   * @since 1.0
83   * @version $Id: HtmlEmail.java 1606709 2014-06-30 12:26:06Z ggregory $
84   */
85  public class HtmlEmail extends MultiPartEmail
86  {
87      /** Definition of the length of generated CID's. */
88      public static final int CID_LENGTH = 10;
89  
90      /** prefix for default HTML mail. */
91      private static final String HTML_MESSAGE_START = "<html><body><pre>";
92      /** suffix for default HTML mail. */
93      private static final String HTML_MESSAGE_END = "</pre></body></html>";
94  
95  
96      /**
97       * Text part of the message. This will be used as alternative text if
98       * the email client does not support HTML messages.
99       */
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&lt;String, InlineImage&gt; 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(final 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(final 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(final 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         final 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(final String urlString, final String name) throws EmailException
212     {
213         try
214         {
215             return embed(new URL(urlString), name);
216         }
217         catch (final 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(final URL url, final 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             final InlineImage ii = inlineEmbeds.get(name);
267             final 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 (final 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 (final 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(final File file) throws EmailException
324     {
325         final 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(final File file, final 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 (final 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             final InlineImage ii = inlineEmbeds.get(file.getName());
378             final 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 (final 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(final DataSource dataSource, final 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             final 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         final 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(final DataSource dataSource, final 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         final 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             final InlineImage ii = new InlineImage(cid, dataSource, mbp);
491             this.inlineEmbeds.put(name, ii);
492 
493             return cid;
494         }
495         catch (final MessagingException me)
496         {
497             throw new EmailException(me);
498         }
499         catch (final 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 (final 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         final 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                 final BodyPart bodyPart = createBodyPart();
556                 try
557                 {
558                     bodyPart.setContent(bodyContainer);
559                     bodyEmbedsContainer.addBodyPart(bodyPart, 0);
560                 }
561                 catch (final 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 (final 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 final String cid;
609         /** <code>DataSource</code> for the content. */
610         private final DataSource dataSource;
611         /** the <code>MimeBodyPart</code> that contains the encoded data. */
612         private final 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(final String cid, final DataSource dataSource, final 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(final Object obj)
666         {
667             if (this == obj)
668             {
669                 return true;
670             }
671             if (!(obj instanceof InlineImage))
672             {
673                 return false;
674             }
675 
676             final 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 }