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   */
84  public class HtmlEmail extends MultiPartEmail
85  {
86      /** Definition of the length of generated CID's. */
87      public static final int CID_LENGTH = 10;
88  
89      /** prefix for default HTML mail. */
90      private static final String HTML_MESSAGE_START = "<html><body><pre>";
91      /** suffix for default HTML mail. */
92      private static final String HTML_MESSAGE_END = "</pre></body></html>";
93  
94  
95      /**
96       * Text part of the message. This will be used as alternative text if
97       * the email client does not support HTML messages.
98       */
99      protected String text;
100 
101     /** Html part of the message. */
102     protected String html;
103 
104     /**
105      * @deprecated As of commons-email 1.1, no longer used. Inline embedded
106      * objects are now stored in {@link #inlineEmbeds}.
107      */
108     @Deprecated
109     protected List<InlineImage> inlineImages;
110 
111     /**
112      * Embedded images Map&lt;String, InlineImage&gt; where the key is the
113      * user-defined image name.
114      */
115     protected Map<String, InlineImage> inlineEmbeds = new HashMap<String, InlineImage>();
116 
117     /**
118      * Set the text content.
119      *
120      * @param aText A String.
121      * @return An HtmlEmail.
122      * @throws EmailException see javax.mail.internet.MimeBodyPart
123      *  for definitions
124      * @since 1.0
125      */
126     public HtmlEmail setTextMsg(final String aText) throws EmailException
127     {
128         if (EmailUtils.isEmpty(aText))
129         {
130             throw new EmailException("Invalid message supplied");
131         }
132 
133         this.text = aText;
134         return this;
135     }
136 
137     /**
138      * Set the HTML content.
139      *
140      * @param aHtml A String.
141      * @return An HtmlEmail.
142      * @throws EmailException see javax.mail.internet.MimeBodyPart
143      *  for definitions
144      * @since 1.0
145      */
146     public HtmlEmail setHtmlMsg(final String aHtml) throws EmailException
147     {
148         if (EmailUtils.isEmpty(aHtml))
149         {
150             throw new EmailException("Invalid message supplied");
151         }
152 
153         this.html = aHtml;
154         return this;
155     }
156 
157     /**
158      * Set the message.
159      *
160      * <p>This method overrides {@link MultiPartEmail#setMsg(String)} in
161      * order to send an HTML message instead of a plain text message in
162      * the mail body. The message is formatted in HTML for the HTML
163      * part of the message; it is left as is in the alternate text
164      * part.
165      *
166      * @param msg the message text to use
167      * @return this <code>HtmlEmail</code>
168      * @throws EmailException if msg is null or empty;
169      * see javax.mail.internet.MimeBodyPart for definitions
170      * @since 1.0
171      */
172     @Override
173     public Email setMsg(final 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         final 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 name} 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(final String urlString, final String name) throws EmailException
211     {
212         try
213         {
214             return embed(new URL(urlString), name);
215         }
216         catch (final 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 name} is null
251      * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions
252      * @since 1.0
253      */
254     public String embed(final URL url, final 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             final InlineImage ii = inlineEmbeds.get(name);
266             final 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             throw new EmailException("embedded name '" + name
277                 + "' is already bound to URL " + urlDataSource.getURL()
278                 + "; existing names cannot be rebound");
279         }
280 
281         // verify that the URL is valid
282         InputStream is = null;
283         try
284         {
285             is = url.openStream();
286         }
287         catch (final IOException e)
288         {
289             throw new EmailException("Invalid URL", e);
290         }
291         finally
292         {
293             try
294             {
295                 if (is != null)
296                 {
297                     is.close();
298                 }
299             }
300             catch (final IOException ioe) // NOPMD
301             { /* sigh */ }
302         }
303 
304         return embed(new URLDataSource(url), name);
305     }
306 
307     /**
308      * Embeds a file in the HTML. This implementation delegates to
309      * {@link #embed(File, String)}.
310      *
311      * @param file The <code>File</code> object to embed
312      * @return A String with the Content-ID of the file.
313      * @throws EmailException when the supplied <code>File</code> cannot be
314      * used; also see {@link javax.mail.internet.MimeBodyPart} for definitions
315      *
316      * @see #embed(File, String)
317      * @since 1.1
318      */
319     public String embed(final File file) throws EmailException
320     {
321         final String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase(Locale.ENGLISH);
322         return embed(file, cid);
323     }
324 
325     /**
326      * Embeds a file in the HTML.
327      *
328      * <p>This method embeds a file located by an URL into
329      * the mail body. It allows, for instance, to add inline images
330      * to the email.  Inline files may be referenced with a
331      * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
332      * returned by the embed function. Files are bound to their names, which is
333      * the value returned by {@link java.io.File#getName()}. If the same file
334      * is embedded multiple times, the same CID is guaranteed to be returned.
335      *
336      * <p>While functionally the same as passing <code>FileDataSource</code> to
337      * {@link #embed(DataSource, String, String)}, this method attempts
338      * to validate the file before embedding it in the message and will throw
339      * <code>EmailException</code> if the validation fails. In this case, the
340      * <code>HtmlEmail</code> object will not be changed.
341      *
342      * @param file The <code>File</code> to embed
343      * @param cid the Content-ID to use for the embedded <code>File</code>
344      * @return A String with the Content-ID of the file.
345      * @throws EmailException when the supplied <code>File</code> cannot be used
346      *  or if the file has already been embedded;
347      *  also see {@link javax.mail.internet.MimeBodyPart} for definitions
348      * @since 1.1
349      */
350     public String embed(final File file, final String cid) throws EmailException
351     {
352         if (EmailUtils.isEmpty(file.getName()))
353         {
354             throw new EmailException("file name cannot be null or empty");
355         }
356 
357         // verify that the File can provide a canonical path
358         String filePath = null;
359         try
360         {
361             filePath = file.getCanonicalPath();
362         }
363         catch (final IOException ioe)
364         {
365             throw new EmailException("couldn't get canonical path for "
366                     + file.getName(), ioe);
367         }
368 
369         // check if a FileDataSource for this name has already been attached;
370         // if so, return the cached CID value.
371         if (inlineEmbeds.containsKey(file.getName()))
372         {
373             final InlineImage ii = inlineEmbeds.get(file.getName());
374             final FileDataSource fileDataSource = (FileDataSource) ii.getDataSource();
375             // make sure the supplied file has the same canonical path
376             // as the one already associated with this name.
377             String existingFilePath = null;
378             try
379             {
380                 existingFilePath = fileDataSource.getFile().getCanonicalPath();
381             }
382             catch (final IOException ioe)
383             {
384                 throw new EmailException("couldn't get canonical path for file "
385                         + fileDataSource.getFile().getName()
386                         + "which has already been embedded", ioe);
387             }
388             if (filePath.equals(existingFilePath))
389             {
390                 return ii.getCid();
391             }
392             throw new EmailException("embedded name '" + file.getName()
393                 + "' is already bound to file " + existingFilePath
394                 + "; existing names cannot be rebound");
395         }
396 
397         // verify that the file is valid
398         if (!file.exists())
399         {
400             throw new EmailException("file " + filePath + " doesn't exist");
401         }
402         if (!file.isFile())
403         {
404             throw new EmailException("file " + filePath + " isn't a normal file");
405         }
406         if (!file.canRead())
407         {
408             throw new EmailException("file " + filePath + " isn't readable");
409         }
410 
411         return embed(new FileDataSource(file), file.getName(), cid);
412     }
413 
414     /**
415      * Embeds the specified <code>DataSource</code> in the HTML using a
416      * randomly generated Content-ID. Returns the generated Content-ID string.
417      *
418      * @param dataSource the <code>DataSource</code> to embed
419      * @param name the name that will be set in the filename header field
420      * @return the generated Content-ID for this <code>DataSource</code>
421      * @throws EmailException if the embedding fails or if <code>name</code> is
422      * null or empty
423      * @see #embed(DataSource, String, String)
424      * @since 1.1
425      */
426     public String embed(final DataSource dataSource, final String name) throws EmailException
427     {
428         // check if the DataSource has already been attached;
429         // if so, return the cached CID value.
430         if (inlineEmbeds.containsKey(name))
431         {
432             final InlineImage ii = inlineEmbeds.get(name);
433             // make sure the supplied URL points to the same thing
434             // as the one already associated with this name.
435             if (dataSource.equals(ii.getDataSource()))
436             {
437                 return ii.getCid();
438             }
439             throw new EmailException("embedded DataSource '" + name
440                 + "' is already bound to name " + ii.getDataSource().toString()
441                 + "; existing names cannot be rebound");
442         }
443 
444         final String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
445         return embed(dataSource, name, cid);
446     }
447 
448     /**
449      * Embeds the specified <code>DataSource</code> in the HTML using the
450      * specified Content-ID. Returns the specified Content-ID string.
451      *
452      * @param dataSource the <code>DataSource</code> to embed
453      * @param name the name that will be set in the filename header field
454      * @param cid the Content-ID to use for this <code>DataSource</code>
455      * @return the URL encoded Content-ID for this <code>DataSource</code>
456      * @throws EmailException if the embedding fails or if <code>name</code> is
457      * null or empty
458      * @since 1.1
459      */
460     public String embed(final DataSource dataSource, final String name, final String cid)
461         throws EmailException
462     {
463         if (EmailUtils.isEmpty(name))
464         {
465             throw new EmailException("name cannot be null or empty");
466         }
467 
468         final MimeBodyPart mbp = new MimeBodyPart();
469 
470         try
471         {
472             // URL encode the cid according to RFC 2392
473             final String encodedCid = EmailUtils.encodeUrl(cid);
474 
475             mbp.setDataHandler(new DataHandler(dataSource));
476             mbp.setFileName(name);
477             mbp.setDisposition(EmailAttachment.INLINE);
478             mbp.setContentID("<" + encodedCid + ">");
479 
480             final InlineImage ii = new InlineImage(encodedCid, dataSource, mbp);
481             this.inlineEmbeds.put(name, ii);
482 
483             return encodedCid;
484         }
485         catch (final MessagingException me)
486         {
487             throw new EmailException(me);
488         }
489         catch (final UnsupportedEncodingException uee)
490         {
491             throw new EmailException(uee);
492         }
493     }
494 
495     /**
496      * Does the work of actually building the MimeMessage. Please note that
497      * a user rarely calls this method directly and only if he/she is
498      * interested in the sending the underlying MimeMessage without
499      * commons-email.
500      *
501      * @throws EmailException if there was an error.
502      * @since 1.0
503      */
504     @Override
505     public void buildMimeMessage() throws EmailException
506     {
507         try
508         {
509             build();
510         }
511         catch (final MessagingException me)
512         {
513             throw new EmailException(me);
514         }
515         super.buildMimeMessage();
516     }
517 
518     /**
519      * @throws EmailException EmailException
520      * @throws MessagingException MessagingException
521      */
522     private void build() throws MessagingException, EmailException
523     {
524         final MimeMultipart rootContainer = this.getContainer();
525         MimeMultipart bodyEmbedsContainer = rootContainer;
526         MimeMultipart bodyContainer = rootContainer;
527         MimeBodyPart msgHtml = null;
528         MimeBodyPart msgText = null;
529 
530         rootContainer.setSubType("mixed");
531 
532         // determine how to form multiparts of email
533 
534         if (EmailUtils.isNotEmpty(this.html) && this.inlineEmbeds.size() > 0)
535         {
536             //If HTML body and embeds are used, create a related container and add it to the root container
537             bodyEmbedsContainer = new MimeMultipart("related");
538             bodyContainer = bodyEmbedsContainer;
539             this.addPart(bodyEmbedsContainer, 0);
540 
541             // If TEXT body was specified, create a alternative container and add it to the embeds container
542             if (EmailUtils.isNotEmpty(this.text))
543             {
544                 bodyContainer = new MimeMultipart("alternative");
545                 final BodyPart bodyPart = createBodyPart();
546                 try
547                 {
548                     bodyPart.setContent(bodyContainer);
549                     bodyEmbedsContainer.addBodyPart(bodyPart, 0);
550                 }
551                 catch (final MessagingException me)
552                 {
553                     throw new EmailException(me);
554                 }
555             }
556         }
557         else if (EmailUtils.isNotEmpty(this.text) && EmailUtils.isNotEmpty(this.html))
558         {
559             // EMAIL-142: if we have both an HTML and TEXT body, but no attachments or
560             //            inline images, the root container should have mimetype
561             //            "multipart/alternative".
562             // reference: http://tools.ietf.org/html/rfc2046#section-5.1.4
563             if (this.inlineEmbeds.size() > 0 || isBoolHasAttachments())
564             {
565                 // If both HTML and TEXT bodies are provided, create an alternative
566                 // container and add it to the root container
567                 bodyContainer = new MimeMultipart("alternative");
568                 this.addPart(bodyContainer, 0);
569             }
570             else
571             {
572                 // no attachments or embedded images present, change the mimetype
573                 // of the root container (= body container)
574                 rootContainer.setSubType("alternative");
575             }
576         }
577 
578         if (EmailUtils.isNotEmpty(this.html))
579         {
580             msgHtml = new MimeBodyPart();
581             bodyContainer.addBodyPart(msgHtml, 0);
582 
583             // EMAIL-104: call explicitly setText to use default mime charset
584             //            (property "mail.mime.charset") in case none has been set
585             msgHtml.setText(this.html, this.charset, EmailConstants.TEXT_SUBTYPE_HTML);
586 
587             // EMAIL-147: work-around for buggy JavaMail implementations;
588             //            in case setText(...) does not set the correct content type,
589             //            use the setContent() method instead.
590             final String contentType = msgHtml.getContentType();
591             if (contentType == null || !contentType.equals(EmailConstants.TEXT_HTML))
592             {
593                 // apply default charset if one has been set
594                 if (EmailUtils.isNotEmpty(this.charset))
595                 {
596                     msgHtml.setContent(this.html, EmailConstants.TEXT_HTML + "; charset=" + this.charset);
597                 }
598                 else
599                 {
600                     // unfortunately, MimeUtility.getDefaultMIMECharset() is package private
601                     // and thus can not be used to set the default system charset in case
602                     // no charset has been provided by the user
603                     msgHtml.setContent(this.html, EmailConstants.TEXT_HTML);
604                 }
605             }
606 
607             for (final InlineImage image : this.inlineEmbeds.values())
608             {
609                 bodyEmbedsContainer.addBodyPart(image.getMbp());
610             }
611         }
612 
613         if (EmailUtils.isNotEmpty(this.text))
614         {
615             msgText = new MimeBodyPart();
616             bodyContainer.addBodyPart(msgText, 0);
617 
618             // EMAIL-104: call explicitly setText to use default mime charset
619             //            (property "mail.mime.charset") in case none has been set
620             msgText.setText(this.text, this.charset);
621         }
622     }
623 
624     /**
625      * Private bean class that encapsulates data about URL contents
626      * that are embedded in the final email.
627      * @since 1.1
628      */
629     private static class InlineImage
630     {
631         /** content id. */
632         private final String cid;
633         /** <code>DataSource</code> for the content. */
634         private final DataSource dataSource;
635         /** the <code>MimeBodyPart</code> that contains the encoded data. */
636         private final MimeBodyPart mbp;
637 
638         /**
639          * Creates an InlineImage object to represent the
640          * specified content ID and <code>MimeBodyPart</code>.
641          * @param cid the generated content ID
642          * @param dataSource the <code>DataSource</code> that represents the content
643          * @param mbp the <code>MimeBodyPart</code> that contains the encoded
644          * data
645          */
646         public InlineImage(final String cid, final DataSource dataSource, final MimeBodyPart mbp)
647         {
648             this.cid = cid;
649             this.dataSource = dataSource;
650             this.mbp = mbp;
651         }
652 
653         /**
654          * Returns the unique content ID of this InlineImage.
655          * @return the unique content ID of this InlineImage
656          */
657         public String getCid()
658         {
659             return cid;
660         }
661 
662         /**
663          * Returns the <code>DataSource</code> that represents the encoded content.
664          * @return the <code>DataSource</code> representing the encoded content
665          */
666         public DataSource getDataSource()
667         {
668             return dataSource;
669         }
670 
671         /**
672          * Returns the <code>MimeBodyPart</code> that contains the
673          * encoded InlineImage data.
674          * @return the <code>MimeBodyPart</code> containing the encoded
675          * InlineImage data
676          */
677         public MimeBodyPart getMbp()
678         {
679             return mbp;
680         }
681 
682         // equals()/hashCode() implementations, since this class
683         // is stored as a entry in a Map.
684         /**
685          * {@inheritDoc}
686          * @return true if the other object is also an InlineImage with the same cid.
687          */
688         @Override
689         public boolean equals(final Object obj)
690         {
691             if (this == obj)
692             {
693                 return true;
694             }
695             if (!(obj instanceof InlineImage))
696             {
697                 return false;
698             }
699 
700             final InlineImage that = (InlineImage) obj;
701 
702             return this.cid.equals(that.cid);
703         }
704 
705         /**
706          * {@inheritDoc}
707          * @return the cid hashCode.
708          */
709         @Override
710         public int hashCode()
711         {
712             return cid.hashCode();
713         }
714     }
715 }