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.net.MalformedURLException;
23  import java.net.URL;
24  import java.util.HashMap;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.Map;
28  
29  import javax.activation.DataHandler;
30  import javax.activation.DataSource;
31  import javax.activation.FileDataSource;
32  import javax.activation.URLDataSource;
33  import javax.mail.BodyPart;
34  import javax.mail.MessagingException;
35  import javax.mail.internet.MimeBodyPart;
36  import javax.mail.internet.MimeMultipart;
37  
38  /**
39   * An HTML multipart email.
40   *
41   * <p>This class is used to send HTML formatted email.  A text message
42   * can also be set for HTML unaware email clients, such as text-based
43   * email clients.
44   *
45   * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to
46   * add attachments to the email.
47   *
48   * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then
49   * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods.
50   * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The
51   * alternative text content can be set with {@link #setTextMsg(String)}.
52   *
53   * <p>Either the text or HTML can be omitted, in which case the "main"
54   * part of the multipart becomes whichever is supplied rather than a
55   * <code>multipart/alternative</code>.
56   *
57   * <h3>Embedding Images and Media</h3>
58   *
59   * <p>It is also possible to embed URLs, files, or arbitrary
60   * <code>DataSource</code>s directly into the body of the mail:
61   * <pre><code>
62   * HtmlEmail he = new HtmlEmail();
63   * File img = new File("my/image.gif");
64   * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class
65   * StringBuffer msg = new StringBuffer();
66   * msg.append("&lt;html&gt;&lt;body&gt;");
67   * msg.append("&lt;img src=cid:").append(he.embed(img)).append("&gt;");
68   * msg.append("&lt;img src=cid:").append(he.embed(png)).append("&gt;");
69   * msg.append("&lt;/body&gt;&lt;/html&gt;");
70   * he.setHtmlMsg(msg.toString());
71   * // code to set the other email fields (not shown)
72   * </pre></code>
73   *
74   * <p>Embedded entities are tracked by their name, which for <code>File</code>s is
75   * the filename itself and for <code>URL</code>s is the canonical path. It is
76   * an error to bind the same name to more than one entity, and this class will
77   * attempt to validate that for <code>File</code>s and <code>URL</code>s. When
78   * embedding a <code>DataSource</code>, the code uses the <code>equals()</code>
79   * method defined on the <code>DataSource</code>s to make the determination.
80   *
81   * @since 1.0
82   * @author <a href="mailto:unknown">Regis Koenig</a>
83   * @author <a href="mailto:sean@informage.net">Sean Legassick</a>
84   * @version $Id: HtmlEmail.java 578501 2007-09-22 21:18:49Z bspeakmon $
85   */
86  public class HtmlEmail extends MultiPartEmail
87  {
88      /** Definition of the length of generated CID's */
89      public static final int CID_LENGTH = 10;
90  
91      /** prefix for default HTML mail */
92      private static final String HTML_MESSAGE_START = "<html><body><pre>";
93      /** suffix for default HTML mail */
94      private static final String HTML_MESSAGE_END = "</pre></body></html>";
95  
96  
97      /**
98       * Text part of the message.  This will be used as alternative text if
99       * 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             if (url.equals(urlDataSource.getURL()))
270             {
271                 return ii.getCid();
272             }
273             else
274             {
275                 throw new EmailException("embedded name '" + name
276                     + "' is already bound to URL " + urlDataSource.getURL()
277                     + "; existing names cannot be rebound");
278             }
279             // NOTE: Comparing URLs with URL.equals() is known to be
280             // inconsistent when dealing with virtual hosting over HTTP,
281             // but since these are almost always files on the local machine,
282             // using equals() should be sufficient.
283         }
284 
285         // verify that the URL is valid
286         InputStream is = null;
287         try
288         {
289             is = url.openStream();
290         }
291         catch (IOException e)
292         {
293             throw new EmailException("Invalid URL", e);
294         }
295         finally
296         {
297             try
298             {
299                 if (is != null)
300                 {
301                     is.close();
302                 }
303             }
304             catch (IOException ioe)
305             { /* sigh */ }
306         }
307 
308         return embed(new URLDataSource(url), name);
309     }
310 
311     /**
312      * Embeds a file in the HTML. This implementation delegates to
313      * {@link #embed(File, String)}.
314      *
315      * @param file The <code>File</code> object to embed
316      * @return A String with the Content-ID of the file.
317      * @throws EmailException when the supplied <code>File</code> cannot be
318      * used; also see {@link javax.mail.internet.MimeBodyPart} for definitions
319      *
320      * @see #embed(File, String)
321      * @since 1.1
322      */
323     public String embed(File file) throws EmailException
324     {
325         String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
326         return embed(file, cid);
327     }
328 
329     /**
330      * Embeds a file in the HTML.
331      *
332      * <p>This method embeds a file located by an URL into
333      * the mail body. It allows, for instance, to add inline images
334      * to the email.  Inline files may be referenced with a
335      * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
336      * returned by the embed function. Files are bound to their names, which is
337      * the value returned by {@link java.io.File#getName()}. If the same file
338      * is embedded multiple times, the same CID is guaranteed to be returned.
339      *
340      * <p>While functionally the same as passing <code>FileDataSource</code> to
341      * {@link #embed(DataSource, String, String)}, this method attempts
342      * to validate the file before embedding it in the message and will throw
343      * <code>EmailException</code> if the validation fails. In this case, the
344      * <code>HtmlEmail</code> object will not be changed.
345      *
346      * @param file The <code>File</code> to embed
347      * @param cid the Content-ID to use for the embedded <code>File</code>
348      * @return A String with the Content-ID of the file.
349      * @throws EmailException when the supplied <code>File</code> cannot be used
350      *  or if the file has already been embedded;
351      *  also see {@link javax.mail.internet.MimeBodyPart} for definitions
352      * @since 1.1
353      */
354     public String embed(File file, String cid) throws EmailException
355     {
356         if (EmailUtils.isEmpty(file.getName()))
357         {
358             throw new EmailException("file name cannot be null or empty");
359         }
360 
361         // verify that the File can provide a canonical path
362         String filePath = null;
363         try
364         {
365             filePath = file.getCanonicalPath();
366         }
367         catch (IOException ioe)
368         {
369             throw new EmailException("couldn't get canonical path for "
370                     + file.getName(), ioe);
371         }
372 
373         // check if a FileDataSource for this name has already been attached;
374         // if so, return the cached CID value.
375         if (inlineEmbeds.containsKey(file.getName()))
376         {
377             InlineImage ii = (InlineImage) inlineEmbeds.get(file.getName());
378             FileDataSource fileDataSource = (FileDataSource) ii.getDataSource();
379             // make sure the supplied file has the same canonical path
380             // as the one already associated with this name.
381             String existingFilePath = null;
382             try
383             {
384                 existingFilePath = fileDataSource.getFile().getCanonicalPath();
385             }
386             catch (IOException ioe)
387             {
388                 throw new EmailException("couldn't get canonical path for file "
389                         + fileDataSource.getFile().getName()
390                         + "which has already been embedded", ioe);
391             }
392             if (filePath.equals(existingFilePath))
393             {
394                 return ii.getCid();
395             }
396             else
397             {
398                 throw new EmailException("embedded name '" + file.getName()
399                     + "' is already bound to file " + existingFilePath
400                     + "; existing names cannot be rebound");
401             }
402         }
403 
404         // verify that the file is valid
405         if (!file.exists())
406         {
407             throw new EmailException("file " + filePath + " doesn't exist");
408         }
409         if (!file.isFile())
410         {
411             throw new EmailException("file " + filePath + " isn't a normal file");
412         }
413         if (!file.canRead())
414         {
415             throw new EmailException("file " + filePath + " isn't readable");
416         }
417 
418         return embed(new FileDataSource(file), file.getName());
419     }
420 
421     /**
422      * Embeds the specified <code>DataSource</code> in the HTML using a
423      * randomly generated Content-ID. Returns the generated Content-ID string.
424      *
425      * @param dataSource the <code>DataSource</code> to embed
426      * @param name the name that will be set in the filename header field
427      * @return the generated Content-ID for this <code>DataSource</code>
428      * @throws EmailException if the embedding fails or if <code>name</code> is
429      * null or empty
430      * @see #embed(DataSource, String, String)
431      * @since 1.1
432      */
433     public String embed(DataSource dataSource, String name) throws EmailException
434     {
435         // check if the DataSource has already been attached;
436         // if so, return the cached CID value.
437         if (inlineEmbeds.containsKey(name))
438         {
439             InlineImage ii = (InlineImage) inlineEmbeds.get(name);
440             // make sure the supplied URL points to the same thing
441             // as the one already associated with this name.
442             if (dataSource.equals(ii.getDataSource()))
443             {
444                 return ii.getCid();
445             }
446             else
447             {
448                 throw new EmailException("embedded DataSource '" + name
449                     + "' is already bound to name " + ii.getDataSource().toString()
450                     + "; existing names cannot be rebound");
451             }
452         }
453 
454         String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
455         return embed(dataSource, name, cid);
456     }
457 
458     /**
459      * Embeds the specified <code>DataSource</code> in the HTML using the
460      * specified Content-ID. Returns the specified Content-ID string.
461      *
462      * @param dataSource the <code>DataSource</code> to embed
463      * @param name the name that will be set in the filename header field
464      * @param cid the Content-ID to use for this <code>DataSource</code>
465      * @return the supplied Content-ID for this <code>DataSource</code>
466      * @throws EmailException if the embedding fails or if <code>name</code> is
467      * null or empty
468      * @since 1.1
469      */
470     public String embed(DataSource dataSource, String name, String cid)
471         throws EmailException
472     {
473         if (EmailUtils.isEmpty(name))
474         {
475             throw new EmailException("name cannot be null or empty");
476         }
477 
478         MimeBodyPart mbp = new MimeBodyPart();
479 
480         try
481         {
482             mbp.setDataHandler(new DataHandler(dataSource));
483             mbp.setFileName(name);
484             mbp.setDisposition("inline");
485             mbp.setContentID("<" + cid + ">");
486 
487             InlineImage ii = new InlineImage(cid, dataSource, mbp);
488             this.inlineEmbeds.put(name, ii);
489 
490             return cid;
491         }
492         catch (MessagingException me)
493         {
494             throw new EmailException(me);
495         }
496     }
497 
498     /**
499      * Does the work of actually building the email.
500      *
501      * @exception EmailException if there was an error.
502      * @since 1.0
503      */
504     public void buildMimeMessage() throws EmailException
505     {
506         try
507         {
508             build();
509         }
510         catch (MessagingException me)
511         {
512             throw new EmailException(me);
513         }
514         super.buildMimeMessage();
515     }
516 
517     /**
518      * @throws EmailException EmailException
519      * @throws MessagingException MessagingException
520      */
521     private void build() throws MessagingException, EmailException
522     {
523         MimeMultipart container = this.getContainer();
524         MimeMultipart subContainer = null;
525         BodyPart msgHtml = null;
526         BodyPart msgText = null;
527 
528         container.setSubType("related");
529         subContainer = new MimeMultipart("alternative");
530 
531         if (EmailUtils.isNotEmpty(this.text))
532         {
533             msgText = new MimeBodyPart();
534             if (this.inlineEmbeds.size() > 0)
535             {
536                 subContainer.addBodyPart(msgText);
537             }
538             else
539             {
540                 container.addBodyPart(msgText);
541             }
542 
543             // apply default charset if one has been set
544             if (EmailUtils.isNotEmpty(this.charset))
545             {
546                 msgText.setContent(
547                     this.text,
548                     Email.TEXT_PLAIN + "; charset=" + this.charset);
549             }
550             else
551             {
552                 msgText.setContent(this.text, Email.TEXT_PLAIN);
553             }
554         }
555 
556         if (EmailUtils.isNotEmpty(this.html))
557         {
558             msgHtml = new MimeBodyPart();
559             if (this.inlineEmbeds.size() > 0)
560             {
561                 subContainer.addBodyPart(msgHtml);
562             }
563             else
564             {
565                 container.addBodyPart(msgHtml);
566             }
567 
568             // apply default charset if one has been set
569             if (EmailUtils.isNotEmpty(this.charset))
570             {
571                 msgHtml.setContent(
572                     this.html,
573                     Email.TEXT_HTML + "; charset=" + this.charset);
574             }
575             else
576             {
577                 msgHtml.setContent(this.html, Email.TEXT_HTML);
578             }
579 
580             Iterator iter = this.inlineEmbeds.values().iterator();
581             while (iter.hasNext())
582             {
583                 InlineImage ii = (InlineImage) iter.next();
584                 container.addBodyPart(ii.getMbp());
585             }
586         }
587 
588         if (this.inlineEmbeds.size() > 0)
589         {
590             // add sub container to message
591             this.addPart(subContainer, 0);
592         }
593     }
594 
595     /**
596      * Private bean class that encapsulates data about URL contents
597      * that are embedded in the final email.
598      * @since 1.1
599      */
600     private static class InlineImage
601     {
602         /** content id */
603         private String cid;
604         /** <code>DataSource</code> for the content */
605         private DataSource dataSource;
606         /** the <code>MimeBodyPart</code> that contains the encoded data */
607         private MimeBodyPart mbp;
608 
609         /**
610          * Creates an InlineImage object to represent the
611          * specified content ID and <code>MimeBodyPart</code>.
612          * @param cid the generated content ID
613          * @param dataSource the <code>DataSource</code> that represents the content
614          * @param mbp the <code>MimeBodyPart</code> that contains the encoded
615          * data
616          */
617         public InlineImage(String cid, DataSource dataSource, MimeBodyPart mbp)
618         {
619             this.cid = cid;
620             this.dataSource = dataSource;
621             this.mbp = mbp;
622         }
623 
624         /**
625          * Returns the unique content ID of this InlineImage.
626          * @return the unique content ID of this InlineImage
627          */
628         public String getCid()
629         {
630             return cid;
631         }
632 
633         /**
634          * Returns the <code>DataSource</code> that represents the encoded content.
635          * @return the <code>DataSource</code> representing the encoded content
636          */
637         public DataSource getDataSource()
638         {
639             return dataSource;
640         }
641 
642         /**
643          * Returns the <code>MimeBodyPart</code> that contains the
644          * encoded InlineImage data.
645          * @return the <code>MimeBodyPart</code> containing the encoded
646          * InlineImage data
647          */
648         public MimeBodyPart getMbp()
649         {
650             return mbp;
651         }
652 
653         // equals()/hashCode() implementations, since this class
654         // is stored as a entry in a Map.
655         /**
656          * {@inheritDoc}
657          * @return true if the other object is also an InlineImage with the same cid.
658          */
659         public boolean equals(Object obj)
660         {
661             if (this == obj)
662             {
663                 return true;
664             }
665             if (!(obj instanceof InlineImage))
666             {
667                 return false;
668             }
669 
670             InlineImage that = (InlineImage) obj;
671 
672             return this.cid.equals(that.cid);
673         }
674 
675         /**
676          * {@inheritDoc}
677          * @return the cid hashCode.
678          */
679         public int hashCode()
680         {
681             return cid.hashCode();
682         }
683     }
684 }