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.Iterator;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  
31  import javax.activation.DataHandler;
32  import javax.activation.DataSource;
33  import javax.activation.FileDataSource;
34  import javax.activation.URLDataSource;
35  import javax.mail.BodyPart;
36  import javax.mail.MessagingException;
37  import javax.mail.internet.MimeBodyPart;
38  import javax.mail.internet.MimeMultipart;
39  
40  /**
41   * An HTML multipart email.
42   *
43   * <p>This class is used to send HTML formatted email.  A text message
44   * can also be set for HTML unaware email clients, such as text-based
45   * email clients.
46   *
47   * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to
48   * add attachments to the email.
49   *
50   * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then
51   * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods.
52   * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The
53   * alternative text content can be set with {@link #setTextMsg(String)}.
54   *
55   * <p>Either the text or HTML can be omitted, in which case the "main"
56   * part of the multipart becomes whichever is supplied rather than a
57   * <code>multipart/alternative</code>.
58   *
59   * <h3>Embedding Images and Media</h3>
60   *
61   * <p>It is also possible to embed URLs, files, or arbitrary
62   * <code>DataSource</code>s directly into the body of the mail:
63   * <pre><code>
64   * HtmlEmail he = new HtmlEmail();
65   * File img = new File("my/image.gif");
66   * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class
67   * StringBuffer msg = new StringBuffer();
68   * msg.append("&lt;html&gt;&lt;body&gt;");
69   * msg.append("&lt;img src=cid:").append(he.embed(img)).append("&gt;");
70   * msg.append("&lt;img src=cid:").append(he.embed(png)).append("&gt;");
71   * msg.append("&lt;/body&gt;&lt;/html&gt;");
72   * he.setHtmlMsg(msg.toString());
73   * // code to set the other email fields (not shown)
74   * </pre></code>
75   *
76   * <p>Embedded entities are tracked by their name, which for <code>File</code>s is
77   * the filename itself and for <code>URL</code>s is the canonical path. It is
78   * an error to bind the same name to more than one entity, and this class will
79   * attempt to validate that for <code>File</code>s and <code>URL</code>s. When
80   * embedding a <code>DataSource</code>, the code uses the <code>equals()</code>
81   * method defined on the <code>DataSource</code>s to make the determination.
82   *
83   * @since 1.0
84   * @author <a href="mailto:unknown">Regis Koenig</a>
85   * @author <a href="mailto:sean@informage.net">Sean Legassick</a>
86   * @version $Id: HtmlEmail.java 1421492 2012-12-13 20:25:53Z tn $
87   */
88  public class HtmlEmail extends MultiPartEmail
89  {
90      /** Definition of the length of generated CID's. */
91      public static final int CID_LENGTH = 10;
92  
93      /** prefix for default HTML mail. */
94      private static final String HTML_MESSAGE_START = "<html><body><pre>";
95      /** suffix for default HTML mail. */
96      private static final String HTML_MESSAGE_END = "</pre></body></html>";
97  
98  
99      /**
100      * Text part of the message. This will be used as alternative text if
101      * the email client does not support HTML messages.
102      */
103     protected String text;
104 
105     /** Html part of the message. */
106     protected String html;
107 
108     /**
109      * @deprecated As of commons-email 1.1, no longer used. Inline embedded
110      * objects are now stored in {@link #inlineEmbeds}.
111      */
112     @Deprecated
113     protected List<InlineImage> inlineImages;
114 
115     /**
116      * Embedded images Map<String, InlineImage> where the key is the
117      * user-defined image name.
118      */
119     protected Map<String, InlineImage> inlineEmbeds = new HashMap<String, InlineImage>();
120 
121     /**
122      * Set the text content.
123      *
124      * @param aText A String.
125      * @return An HtmlEmail.
126      * @throws EmailException see javax.mail.internet.MimeBodyPart
127      *  for definitions
128      * @since 1.0
129      */
130     public HtmlEmail setTextMsg(String aText) throws EmailException
131     {
132         if (EmailUtils.isEmpty(aText))
133         {
134             throw new EmailException("Invalid message supplied");
135         }
136 
137         this.text = aText;
138         return this;
139     }
140 
141     /**
142      * Set the HTML content.
143      *
144      * @param aHtml A String.
145      * @return An HtmlEmail.
146      * @throws EmailException see javax.mail.internet.MimeBodyPart
147      *  for definitions
148      * @since 1.0
149      */
150     public HtmlEmail setHtmlMsg(String aHtml) throws EmailException
151     {
152         if (EmailUtils.isEmpty(aHtml))
153         {
154             throw new EmailException("Invalid message supplied");
155         }
156 
157         this.html = aHtml;
158         return this;
159     }
160 
161     /**
162      * Set the message.
163      *
164      * <p>This method overrides {@link MultiPartEmail#setMsg(String)} in
165      * order to send an HTML message instead of a plain text message in
166      * the mail body. The message is formatted in HTML for the HTML
167      * part of the message; it is left as is in the alternate text
168      * part.
169      *
170      * @param msg the message text to use
171      * @return this <code>HtmlEmail</code>
172      * @throws EmailException if msg is null or empty;
173      * see javax.mail.internet.MimeBodyPart for definitions
174      * @since 1.0
175      */
176     @Override
177     public Email setMsg(String msg) throws EmailException
178     {
179         if (EmailUtils.isEmpty(msg))
180         {
181             throw new EmailException("Invalid message supplied");
182         }
183 
184         setTextMsg(msg);
185 
186         StringBuffer htmlMsgBuf = new StringBuffer(
187             msg.length()
188             + HTML_MESSAGE_START.length()
189             + HTML_MESSAGE_END.length()
190         );
191 
192         htmlMsgBuf.append(HTML_MESSAGE_START)
193             .append(msg)
194             .append(HTML_MESSAGE_END);
195 
196         setHtmlMsg(htmlMsgBuf.toString());
197 
198         return this;
199     }
200 
201     /**
202      * Attempts to parse the specified <code>String</code> as a URL that will
203      * then be embedded in the message.
204      *
205      * @param urlString String representation of the URL.
206      * @param name The name that will be set in the filename header field.
207      * @return A String with the Content-ID of the URL.
208      * @throws EmailException when URL supplied is invalid or if {@code name} is null
209      * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions
210      *
211      * @see #embed(URL, String)
212      * @since 1.1
213      */
214     public String embed(String urlString, String name) throws EmailException
215     {
216         try
217         {
218             return embed(new URL(urlString), name);
219         }
220         catch (MalformedURLException e)
221         {
222             throw new EmailException("Invalid URL", e);
223         }
224     }
225 
226     /**
227      * Embeds an URL in the HTML.
228      *
229      * <p>This method embeds a file located by an URL into
230      * the mail body. It allows, for instance, to add inline images
231      * to the email.  Inline files may be referenced with a
232      * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
233      * returned by the embed function. It is an error to bind the same name
234      * to more than one URL; if the same URL is embedded multiple times, the
235      * same Content-ID is guaranteed to be returned.
236      *
237      * <p>While functionally the same as passing <code>URLDataSource</code> to
238      * {@link #embed(DataSource, String, String)}, this method attempts
239      * to validate the URL before embedding it in the message and will throw
240      * <code>EmailException</code> if the validation fails. In this case, the
241      * <code>HtmlEmail</code> object will not be changed.
242      *
243      * <p>
244      * NOTE: Clients should take care to ensure that different URLs are bound to
245      * different names. This implementation tries to detect this and throw
246      * <code>EmailException</code>. However, it is not guaranteed to catch
247      * all cases, especially when the URL refers to a remote HTTP host that
248      * may be part of a virtual host cluster.
249      *
250      * @param url The URL of the file.
251      * @param name The name that will be set in the filename header
252      * field.
253      * @return A String with the Content-ID of the file.
254      * @throws EmailException when URL supplied is invalid or if {@code name} is null
255      * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions
256      * @since 1.0
257      */
258     public String embed(URL url, String name) throws EmailException
259     {
260         if (EmailUtils.isEmpty(name))
261         {
262             throw new EmailException("name cannot be null or empty");
263         }
264 
265         // check if a URLDataSource for this name has already been attached;
266         // if so, return the cached CID value.
267         if (inlineEmbeds.containsKey(name))
268         {
269             InlineImage ii = inlineEmbeds.get(name);
270             URLDataSource urlDataSource = (URLDataSource) ii.getDataSource();
271             // make sure the supplied URL points to the same thing
272             // as the one already associated with this name.
273             // NOTE: Comparing URLs with URL.equals() is a blocking operation
274             // in the case of a network failure therefore we use
275             // url.toExternalForm().equals() here.
276             if (url.toExternalForm().equals(urlDataSource.getURL().toExternalForm()))
277             {
278                 return ii.getCid();
279             }
280             else
281             {
282                 throw new EmailException("embedded name '" + name
283                     + "' is already bound to URL " + urlDataSource.getURL()
284                     + "; existing names cannot be rebound");
285             }
286         }
287 
288         // verify that the URL is valid
289         InputStream is = null;
290         try
291         {
292             is = url.openStream();
293         }
294         catch (IOException e)
295         {
296             throw new EmailException("Invalid URL", e);
297         }
298         finally
299         {
300             try
301             {
302                 if (is != null)
303                 {
304                     is.close();
305                 }
306             }
307             catch (IOException ioe) // NOPMD
308             { /* sigh */ }
309         }
310 
311         return embed(new URLDataSource(url), name);
312     }
313 
314     /**
315      * Embeds a file in the HTML. This implementation delegates to
316      * {@link #embed(File, String)}.
317      *
318      * @param file The <code>File</code> object to embed
319      * @return A String with the Content-ID of the file.
320      * @throws EmailException when the supplied <code>File</code> cannot be
321      * used; also see {@link javax.mail.internet.MimeBodyPart} for definitions
322      *
323      * @see #embed(File, String)
324      * @since 1.1
325      */
326     public String embed(File file) throws EmailException
327     {
328         String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase(Locale.ENGLISH);
329         return embed(file, cid);
330     }
331 
332     /**
333      * Embeds a file in the HTML.
334      *
335      * <p>This method embeds a file located by an URL into
336      * the mail body. It allows, for instance, to add inline images
337      * to the email.  Inline files may be referenced with a
338      * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
339      * returned by the embed function. Files are bound to their names, which is
340      * the value returned by {@link java.io.File#getName()}. If the same file
341      * is embedded multiple times, the same CID is guaranteed to be returned.
342      *
343      * <p>While functionally the same as passing <code>FileDataSource</code> to
344      * {@link #embed(DataSource, String, String)}, this method attempts
345      * to validate the file before embedding it in the message and will throw
346      * <code>EmailException</code> if the validation fails. In this case, the
347      * <code>HtmlEmail</code> object will not be changed.
348      *
349      * @param file The <code>File</code> to embed
350      * @param cid the Content-ID to use for the embedded <code>File</code>
351      * @return A String with the Content-ID of the file.
352      * @throws EmailException when the supplied <code>File</code> cannot be used
353      *  or if the file has already been embedded;
354      *  also see {@link javax.mail.internet.MimeBodyPart} for definitions
355      * @since 1.1
356      */
357     public String embed(File file, String cid) throws EmailException
358     {
359         if (EmailUtils.isEmpty(file.getName()))
360         {
361             throw new EmailException("file name cannot be null or empty");
362         }
363 
364         // verify that the File can provide a canonical path
365         String filePath = null;
366         try
367         {
368             filePath = file.getCanonicalPath();
369         }
370         catch (IOException ioe)
371         {
372             throw new EmailException("couldn't get canonical path for "
373                     + file.getName(), ioe);
374         }
375 
376         // check if a FileDataSource for this name has already been attached;
377         // if so, return the cached CID value.
378         if (inlineEmbeds.containsKey(file.getName()))
379         {
380             InlineImage ii = inlineEmbeds.get(file.getName());
381             FileDataSource fileDataSource = (FileDataSource) ii.getDataSource();
382             // make sure the supplied file has the same canonical path
383             // as the one already associated with this name.
384             String existingFilePath = null;
385             try
386             {
387                 existingFilePath = fileDataSource.getFile().getCanonicalPath();
388             }
389             catch (IOException ioe)
390             {
391                 throw new EmailException("couldn't get canonical path for file "
392                         + fileDataSource.getFile().getName()
393                         + "which has already been embedded", ioe);
394             }
395             if (filePath.equals(existingFilePath))
396             {
397                 return ii.getCid();
398             }
399             else
400             {
401                 throw new EmailException("embedded name '" + file.getName()
402                     + "' is already bound to file " + existingFilePath
403                     + "; existing names cannot be rebound");
404             }
405         }
406 
407         // verify that the file is valid
408         if (!file.exists())
409         {
410             throw new EmailException("file " + filePath + " doesn't exist");
411         }
412         if (!file.isFile())
413         {
414             throw new EmailException("file " + filePath + " isn't a normal file");
415         }
416         if (!file.canRead())
417         {
418             throw new EmailException("file " + filePath + " isn't readable");
419         }
420 
421         return embed(new FileDataSource(file), file.getName(), cid);
422     }
423 
424     /**
425      * Embeds the specified <code>DataSource</code> in the HTML using a
426      * randomly generated Content-ID. Returns the generated Content-ID string.
427      *
428      * @param dataSource the <code>DataSource</code> to embed
429      * @param name the name that will be set in the filename header field
430      * @return the generated Content-ID for this <code>DataSource</code>
431      * @throws EmailException if the embedding fails or if <code>name</code> is
432      * null or empty
433      * @see #embed(DataSource, String, String)
434      * @since 1.1
435      */
436     public String embed(DataSource dataSource, String name) throws EmailException
437     {
438         // check if the DataSource has already been attached;
439         // if so, return the cached CID value.
440         if (inlineEmbeds.containsKey(name))
441         {
442             InlineImage ii = inlineEmbeds.get(name);
443             // make sure the supplied URL points to the same thing
444             // as the one already associated with this name.
445             if (dataSource.equals(ii.getDataSource()))
446             {
447                 return ii.getCid();
448             }
449             else
450             {
451                 throw new EmailException("embedded DataSource '" + name
452                     + "' is already bound to name " + ii.getDataSource().toString()
453                     + "; existing names cannot be rebound");
454             }
455         }
456 
457         String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
458         return embed(dataSource, name, cid);
459     }
460 
461     /**
462      * Embeds the specified <code>DataSource</code> in the HTML using the
463      * specified Content-ID. Returns the specified Content-ID string.
464      *
465      * @param dataSource the <code>DataSource</code> to embed
466      * @param name the name that will be set in the filename header field
467      * @param cid the Content-ID to use for this <code>DataSource</code>
468      * @return the URL encoded Content-ID for this <code>DataSource</code>
469      * @throws EmailException if the embedding fails or if <code>name</code> is
470      * null or empty
471      * @since 1.1
472      */
473     public String embed(DataSource dataSource, String name, String cid)
474         throws EmailException
475     {
476         if (EmailUtils.isEmpty(name))
477         {
478             throw new EmailException("name cannot be null or empty");
479         }
480 
481         MimeBodyPart mbp = new MimeBodyPart();
482 
483         try
484         {
485             // url encode the cid according to rfc 2392
486             cid = EmailUtils.encodeUrl(cid);
487 
488             mbp.setDataHandler(new DataHandler(dataSource));
489             mbp.setFileName(name);
490             mbp.setDisposition(EmailAttachment.INLINE);
491             mbp.setContentID("<" + cid + ">");
492 
493             InlineImage ii = new InlineImage(cid, dataSource, mbp);
494             this.inlineEmbeds.put(name, ii);
495 
496             return cid;
497         }
498         catch (MessagingException me)
499         {
500             throw new EmailException(me);
501         }
502         catch (UnsupportedEncodingException uee)
503         {
504             throw new EmailException(uee);
505         }
506     }
507 
508     /**
509      * Does the work of actually building the MimeMessage. Please note that
510      * a user rarely calls this method directly and only if he/she is
511      * interested in the sending the underlying MimeMessage without
512      * commons-email.
513      *
514      * @exception EmailException if there was an error.
515      * @since 1.0
516      */
517     @Override
518     public void buildMimeMessage() throws EmailException
519     {
520         try
521         {
522             build();
523         }
524         catch (MessagingException me)
525         {
526             throw new EmailException(me);
527         }
528         super.buildMimeMessage();
529     }
530 
531     /**
532      * @throws EmailException EmailException
533      * @throws MessagingException MessagingException
534      */
535     private void build() throws MessagingException, EmailException
536     {
537         MimeMultipart rootContainer = this.getContainer();
538         MimeMultipart bodyEmbedsContainer = rootContainer;
539         MimeMultipart bodyContainer = rootContainer;
540         BodyPart msgHtml = null;
541         BodyPart msgText = null;
542 
543         rootContainer.setSubType("mixed");
544 
545         // determine how to form multiparts of email
546 
547         if (EmailUtils.isNotEmpty(this.html) && this.inlineEmbeds.size() > 0)
548         {
549             //If HTML body and embeds are used, create a related container and add it to the root container
550             bodyEmbedsContainer = new MimeMultipart("related");
551             bodyContainer = bodyEmbedsContainer;
552             this.addPart(bodyEmbedsContainer, 0);
553 
554             //If TEXT body was specified, create a alternative container and add it to the embeds container
555             if (EmailUtils.isNotEmpty(this.text))
556             {
557                 bodyContainer = new MimeMultipart("alternative");
558                 BodyPart bodyPart = createBodyPart();
559                 try
560                 {
561                     bodyPart.setContent(bodyContainer);
562                     bodyEmbedsContainer.addBodyPart(bodyPart, 0);
563                 }
564                 catch (MessagingException me)
565                 {
566                     throw new EmailException(me);
567                 }
568             }
569         }
570         else if (EmailUtils.isNotEmpty(this.text) && EmailUtils.isNotEmpty(this.html))
571         {
572             //If both HTML and TEXT bodies are provided, create a alternative container and add it to the root container
573             bodyContainer = new MimeMultipart("alternative");
574             this.addPart(bodyContainer, 0);
575         }
576 
577         if (EmailUtils.isNotEmpty(this.html))
578         {
579             msgHtml = new MimeBodyPart();
580             bodyContainer.addBodyPart(msgHtml, 0);
581 
582             // apply default charset if one has been set
583             if (EmailUtils.isNotEmpty(this.charset))
584             {
585                 msgHtml.setContent(
586                     this.html,
587                     EmailConstants.TEXT_HTML + "; charset=" + this.charset);
588             }
589             else
590             {
591                 msgHtml.setContent(this.html, EmailConstants.TEXT_HTML);
592             }
593 
594             Iterator<InlineImage> iter = this.inlineEmbeds.values().iterator();
595             while (iter.hasNext())
596             {
597                 InlineImage ii = iter.next();
598                 bodyEmbedsContainer.addBodyPart(ii.getMbp());
599             }
600         }
601 
602         if (EmailUtils.isNotEmpty(this.text))
603         {
604             msgText = new MimeBodyPart();
605             bodyContainer.addBodyPart(msgText, 0);
606 
607             // apply default charset if one has been set
608             if (EmailUtils.isNotEmpty(this.charset))
609             {
610                 msgText.setContent(
611                     this.text,
612                     EmailConstants.TEXT_PLAIN + "; charset=" + this.charset);
613             }
614             else
615             {
616                 msgText.setContent(this.text, EmailConstants.TEXT_PLAIN);
617             }
618         }
619     }
620 
621     /**
622      * Private bean class that encapsulates data about URL contents
623      * that are embedded in the final email.
624      * @since 1.1
625      */
626     private static class InlineImage
627     {
628         /** content id. */
629         private String cid;
630         /** <code>DataSource</code> for the content. */
631         private DataSource dataSource;
632         /** the <code>MimeBodyPart</code> that contains the encoded data. */
633         private MimeBodyPart mbp;
634 
635         /**
636          * Creates an InlineImage object to represent the
637          * specified content ID and <code>MimeBodyPart</code>.
638          * @param cid the generated content ID
639          * @param dataSource the <code>DataSource</code> that represents the content
640          * @param mbp the <code>MimeBodyPart</code> that contains the encoded
641          * data
642          */
643         public InlineImage(String cid, DataSource dataSource, MimeBodyPart mbp)
644         {
645             this.cid = cid;
646             this.dataSource = dataSource;
647             this.mbp = mbp;
648         }
649 
650         /**
651          * Returns the unique content ID of this InlineImage.
652          * @return the unique content ID of this InlineImage
653          */
654         public String getCid()
655         {
656             return cid;
657         }
658 
659         /**
660          * Returns the <code>DataSource</code> that represents the encoded content.
661          * @return the <code>DataSource</code> representing the encoded content
662          */
663         public DataSource getDataSource()
664         {
665             return dataSource;
666         }
667 
668         /**
669          * Returns the <code>MimeBodyPart</code> that contains the
670          * encoded InlineImage data.
671          * @return the <code>MimeBodyPart</code> containing the encoded
672          * InlineImage data
673          */
674         public MimeBodyPart getMbp()
675         {
676             return mbp;
677         }
678 
679         // equals()/hashCode() implementations, since this class
680         // is stored as a entry in a Map.
681         /**
682          * {@inheritDoc}
683          * @return true if the other object is also an InlineImage with the same cid.
684          */
685         @Override
686         public boolean equals(Object obj)
687         {
688             if (this == obj)
689             {
690                 return true;
691             }
692             if (!(obj instanceof InlineImage))
693             {
694                 return false;
695             }
696 
697             InlineImage that = (InlineImage) obj;
698 
699             return this.cid.equals(that.cid);
700         }
701 
702         /**
703          * {@inheritDoc}
704          * @return the cid hashCode.
705          */
706         @Override
707         public int hashCode()
708         {
709             return cid.hashCode();
710         }
711     }
712 }