001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.commons.mail;
018
019 import java.io.File;
020 import java.io.IOException;
021 import java.io.InputStream;
022 import java.net.MalformedURLException;
023 import java.net.URL;
024 import java.util.HashMap;
025 import java.util.Iterator;
026 import java.util.List;
027 import java.util.Map;
028
029 import javax.activation.DataHandler;
030 import javax.activation.DataSource;
031 import javax.activation.FileDataSource;
032 import javax.activation.URLDataSource;
033 import javax.mail.BodyPart;
034 import javax.mail.MessagingException;
035 import javax.mail.internet.MimeBodyPart;
036 import javax.mail.internet.MimeMultipart;
037
038 /**
039 * An HTML multipart email.
040 *
041 * <p>This class is used to send HTML formatted email. A text message
042 * can also be set for HTML unaware email clients, such as text-based
043 * email clients.
044 *
045 * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to
046 * add attachments to the email.
047 *
048 * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then
049 * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods.
050 * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The
051 * alternative text content can be set with {@link #setTextMsg(String)}.
052 *
053 * <p>Either the text or HTML can be omitted, in which case the "main"
054 * part of the multipart becomes whichever is supplied rather than a
055 * <code>multipart/alternative</code>.
056 *
057 * <h3>Embedding Images and Media</h3>
058 *
059 * <p>It is also possible to embed URLs, files, or arbitrary
060 * <code>DataSource</code>s directly into the body of the mail:
061 * <pre><code>
062 * HtmlEmail he = new HtmlEmail();
063 * File img = new File("my/image.gif");
064 * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class
065 * StringBuffer msg = new StringBuffer();
066 * msg.append("<html><body>");
067 * msg.append("<img src=cid:").append(he.embed(img)).append(">");
068 * msg.append("<img src=cid:").append(he.embed(png)).append(">");
069 * msg.append("</body></html>");
070 * he.setHtmlMsg(msg.toString());
071 * // code to set the other email fields (not shown)
072 * </pre></code>
073 *
074 * <p>Embedded entities are tracked by their name, which for <code>File</code>s is
075 * the filename itself and for <code>URL</code>s is the canonical path. It is
076 * an error to bind the same name to more than one entity, and this class will
077 * attempt to validate that for <code>File</code>s and <code>URL</code>s. When
078 * embedding a <code>DataSource</code>, the code uses the <code>equals()</code>
079 * method defined on the <code>DataSource</code>s to make the determination.
080 *
081 * @since 1.0
082 * @author <a href="mailto:unknown">Regis Koenig</a>
083 * @author <a href="mailto:sean@informage.net">Sean Legassick</a>
084 * @version $Id: HtmlEmail.java 785383 2009-06-16 20:36:22Z sgoeschl $
085 */
086 public class HtmlEmail extends MultiPartEmail
087 {
088 /** Definition of the length of generated CID's */
089 public static final int CID_LENGTH = 10;
090
091 /** prefix for default HTML mail */
092 private static final String HTML_MESSAGE_START = "<html><body><pre>";
093 /** suffix for default HTML mail */
094 private static final String HTML_MESSAGE_END = "</pre></body></html>";
095
096
097 /**
098 * Text part of the message. This will be used as alternative text if
099 * the email client does not support HTML messages.
100 */
101 protected String text;
102
103 /** Html part of the message */
104 protected String html;
105
106 /**
107 * @deprecated As of commons-email 1.1, no longer used. Inline embedded
108 * objects are now stored in {@link #inlineEmbeds}.
109 */
110 protected List inlineImages;
111
112 /**
113 * Embedded images Map<String, InlineImage> where the key is the
114 * user-defined image name.
115 */
116 protected Map inlineEmbeds = new HashMap();
117
118 /**
119 * Set the text content.
120 *
121 * @param aText A String.
122 * @return An HtmlEmail.
123 * @throws EmailException see javax.mail.internet.MimeBodyPart
124 * for definitions
125 * @since 1.0
126 */
127 public HtmlEmail setTextMsg(String aText) throws EmailException
128 {
129 if (EmailUtils.isEmpty(aText))
130 {
131 throw new EmailException("Invalid message supplied");
132 }
133
134 this.text = aText;
135 return this;
136 }
137
138 /**
139 * Set the HTML content.
140 *
141 * @param aHtml A String.
142 * @return An HtmlEmail.
143 * @throws EmailException see javax.mail.internet.MimeBodyPart
144 * for definitions
145 * @since 1.0
146 */
147 public HtmlEmail setHtmlMsg(String aHtml) throws EmailException
148 {
149 if (EmailUtils.isEmpty(aHtml))
150 {
151 throw new EmailException("Invalid message supplied");
152 }
153
154 this.html = aHtml;
155 return this;
156 }
157
158 /**
159 * Set the message.
160 *
161 * <p>This method overrides {@link MultiPartEmail#setMsg(String)} in
162 * order to send an HTML message instead of a plain text message in
163 * the mail body. The message is formatted in HTML for the HTML
164 * part of the message; it is left as is in the alternate text
165 * part.
166 *
167 * @param msg the message text to use
168 * @return this <code>HtmlEmail</code>
169 * @throws EmailException if msg is null or empty;
170 * see javax.mail.internet.MimeBodyPart for definitions
171 * @since 1.0
172 */
173 public Email setMsg(String msg) throws EmailException
174 {
175 if (EmailUtils.isEmpty(msg))
176 {
177 throw new EmailException("Invalid message supplied");
178 }
179
180 setTextMsg(msg);
181
182 StringBuffer htmlMsgBuf = new StringBuffer(
183 msg.length()
184 + HTML_MESSAGE_START.length()
185 + HTML_MESSAGE_END.length()
186 );
187
188 htmlMsgBuf.append(HTML_MESSAGE_START)
189 .append(msg)
190 .append(HTML_MESSAGE_END);
191
192 setHtmlMsg(htmlMsgBuf.toString());
193
194 return this;
195 }
196
197 /**
198 * Attempts to parse the specified <code>String</code> as a URL that will
199 * then be embedded in the message.
200 *
201 * @param urlString String representation of the URL.
202 * @param name The name that will be set in the filename header field.
203 * @return A String with the Content-ID of the URL.
204 * @throws EmailException when URL supplied is invalid or if <code> is null
205 * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions
206 *
207 * @see #embed(URL, String)
208 * @since 1.1
209 */
210 public String embed(String urlString, String name) throws EmailException
211 {
212 try
213 {
214 return embed(new URL(urlString), name);
215 }
216 catch (MalformedURLException e)
217 {
218 throw new EmailException("Invalid URL", e);
219 }
220 }
221
222 /**
223 * Embeds an URL in the HTML.
224 *
225 * <p>This method embeds a file located by an URL into
226 * the mail body. It allows, for instance, to add inline images
227 * to the email. Inline files may be referenced with a
228 * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
229 * returned by the embed function. It is an error to bind the same name
230 * to more than one URL; if the same URL is embedded multiple times, the
231 * same Content-ID is guaranteed to be returned.
232 *
233 * <p>While functionally the same as passing <code>URLDataSource</code> to
234 * {@link #embed(DataSource, String, String)}, this method attempts
235 * to validate the URL before embedding it in the message and will throw
236 * <code>EmailException</code> if the validation fails. In this case, the
237 * <code>HtmlEmail</code> object will not be changed.
238 *
239 * <p>
240 * NOTE: Clients should take care to ensure that different URLs are bound to
241 * different names. This implementation tries to detect this and throw
242 * <code>EmailException</code>. However, it is not guaranteed to catch
243 * all cases, especially when the URL refers to a remote HTTP host that
244 * may be part of a virtual host cluster.
245 *
246 * @param url The URL of the file.
247 * @param name The name that will be set in the filename header
248 * field.
249 * @return A String with the Content-ID of the file.
250 * @throws EmailException when URL supplied is invalid or if <code> is null
251 * or empty; also see {@link javax.mail.internet.MimeBodyPart} for definitions
252 * @since 1.0
253 */
254 public String embed(URL url, String name) throws EmailException
255 {
256 if (EmailUtils.isEmpty(name))
257 {
258 throw new EmailException("name cannot be null or empty");
259 }
260
261 // check if a URLDataSource for this name has already been attached;
262 // if so, return the cached CID value.
263 if (inlineEmbeds.containsKey(name))
264 {
265 InlineImage ii = (InlineImage) inlineEmbeds.get(name);
266 URLDataSource urlDataSource = (URLDataSource) ii.getDataSource();
267 // make sure the supplied URL points to the same thing
268 // as the one already associated with this name.
269 // NOTE: Comparing URLs with URL.equals() is a blocking operation
270 // in the case of a network failure therefore we use
271 // url.toExternalForm().equals() here.
272 if (url.toExternalForm().equals(urlDataSource.getURL().toExternalForm()))
273 {
274 return ii.getCid();
275 }
276 else
277 {
278 throw new EmailException("embedded name '" + name
279 + "' is already bound to URL " + urlDataSource.getURL()
280 + "; existing names cannot be rebound");
281 }
282 }
283
284 // verify that the URL is valid
285 InputStream is = null;
286 try
287 {
288 is = url.openStream();
289 }
290 catch (IOException e)
291 {
292 throw new EmailException("Invalid URL", e);
293 }
294 finally
295 {
296 try
297 {
298 if (is != null)
299 {
300 is.close();
301 }
302 }
303 catch (IOException ioe)
304 { /* sigh */ }
305 }
306
307 return embed(new URLDataSource(url), name);
308 }
309
310 /**
311 * Embeds a file in the HTML. This implementation delegates to
312 * {@link #embed(File, String)}.
313 *
314 * @param file The <code>File</code> object to embed
315 * @return A String with the Content-ID of the file.
316 * @throws EmailException when the supplied <code>File</code> cannot be
317 * used; also see {@link javax.mail.internet.MimeBodyPart} for definitions
318 *
319 * @see #embed(File, String)
320 * @since 1.1
321 */
322 public String embed(File file) throws EmailException
323 {
324 String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
325 return embed(file, cid);
326 }
327
328 /**
329 * Embeds a file in the HTML.
330 *
331 * <p>This method embeds a file located by an URL into
332 * the mail body. It allows, for instance, to add inline images
333 * to the email. Inline files may be referenced with a
334 * <code>cid:xxxxxx</code> URL, where xxxxxx is the Content-ID
335 * returned by the embed function. Files are bound to their names, which is
336 * the value returned by {@link java.io.File#getName()}. If the same file
337 * is embedded multiple times, the same CID is guaranteed to be returned.
338 *
339 * <p>While functionally the same as passing <code>FileDataSource</code> to
340 * {@link #embed(DataSource, String, String)}, this method attempts
341 * to validate the file before embedding it in the message and will throw
342 * <code>EmailException</code> if the validation fails. In this case, the
343 * <code>HtmlEmail</code> object will not be changed.
344 *
345 * @param file The <code>File</code> to embed
346 * @param cid the Content-ID to use for the embedded <code>File</code>
347 * @return A String with the Content-ID of the file.
348 * @throws EmailException when the supplied <code>File</code> cannot be used
349 * or if the file has already been embedded;
350 * also see {@link javax.mail.internet.MimeBodyPart} for definitions
351 * @since 1.1
352 */
353 public String embed(File file, String cid) throws EmailException
354 {
355 if (EmailUtils.isEmpty(file.getName()))
356 {
357 throw new EmailException("file name cannot be null or empty");
358 }
359
360 // verify that the File can provide a canonical path
361 String filePath = null;
362 try
363 {
364 filePath = file.getCanonicalPath();
365 }
366 catch (IOException ioe)
367 {
368 throw new EmailException("couldn't get canonical path for "
369 + file.getName(), ioe);
370 }
371
372 // check if a FileDataSource for this name has already been attached;
373 // if so, return the cached CID value.
374 if (inlineEmbeds.containsKey(file.getName()))
375 {
376 InlineImage ii = (InlineImage) inlineEmbeds.get(file.getName());
377 FileDataSource fileDataSource = (FileDataSource) ii.getDataSource();
378 // make sure the supplied file has the same canonical path
379 // as the one already associated with this name.
380 String existingFilePath = null;
381 try
382 {
383 existingFilePath = fileDataSource.getFile().getCanonicalPath();
384 }
385 catch (IOException ioe)
386 {
387 throw new EmailException("couldn't get canonical path for file "
388 + fileDataSource.getFile().getName()
389 + "which has already been embedded", ioe);
390 }
391 if (filePath.equals(existingFilePath))
392 {
393 return ii.getCid();
394 }
395 else
396 {
397 throw new EmailException("embedded name '" + file.getName()
398 + "' is already bound to file " + existingFilePath
399 + "; existing names cannot be rebound");
400 }
401 }
402
403 // verify that the file is valid
404 if (!file.exists())
405 {
406 throw new EmailException("file " + filePath + " doesn't exist");
407 }
408 if (!file.isFile())
409 {
410 throw new EmailException("file " + filePath + " isn't a normal file");
411 }
412 if (!file.canRead())
413 {
414 throw new EmailException("file " + filePath + " isn't readable");
415 }
416
417 return embed(new FileDataSource(file), file.getName());
418 }
419
420 /**
421 * Embeds the specified <code>DataSource</code> in the HTML using a
422 * randomly generated Content-ID. Returns the generated Content-ID string.
423 *
424 * @param dataSource the <code>DataSource</code> to embed
425 * @param name the name that will be set in the filename header field
426 * @return the generated Content-ID for this <code>DataSource</code>
427 * @throws EmailException if the embedding fails or if <code>name</code> is
428 * null or empty
429 * @see #embed(DataSource, String, String)
430 * @since 1.1
431 */
432 public String embed(DataSource dataSource, String name) throws EmailException
433 {
434 // check if the DataSource has already been attached;
435 // if so, return the cached CID value.
436 if (inlineEmbeds.containsKey(name))
437 {
438 InlineImage ii = (InlineImage) inlineEmbeds.get(name);
439 // make sure the supplied URL points to the same thing
440 // as the one already associated with this name.
441 if (dataSource.equals(ii.getDataSource()))
442 {
443 return ii.getCid();
444 }
445 else
446 {
447 throw new EmailException("embedded DataSource '" + name
448 + "' is already bound to name " + ii.getDataSource().toString()
449 + "; existing names cannot be rebound");
450 }
451 }
452
453 String cid = EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH).toLowerCase();
454 return embed(dataSource, name, cid);
455 }
456
457 /**
458 * Embeds the specified <code>DataSource</code> in the HTML using the
459 * specified Content-ID. Returns the specified Content-ID string.
460 *
461 * @param dataSource the <code>DataSource</code> to embed
462 * @param name the name that will be set in the filename header field
463 * @param cid the Content-ID to use for this <code>DataSource</code>
464 * @return the supplied Content-ID for this <code>DataSource</code>
465 * @throws EmailException if the embedding fails or if <code>name</code> is
466 * null or empty
467 * @since 1.1
468 */
469 public String embed(DataSource dataSource, String name, String cid)
470 throws EmailException
471 {
472 if (EmailUtils.isEmpty(name))
473 {
474 throw new EmailException("name cannot be null or empty");
475 }
476
477 MimeBodyPart mbp = new MimeBodyPart();
478
479 try
480 {
481 mbp.setDataHandler(new DataHandler(dataSource));
482 mbp.setFileName(name);
483 mbp.setDisposition("inline");
484 mbp.setContentID("<" + cid + ">");
485
486 InlineImage ii = new InlineImage(cid, dataSource, mbp);
487 this.inlineEmbeds.put(name, ii);
488
489 return cid;
490 }
491 catch (MessagingException me)
492 {
493 throw new EmailException(me);
494 }
495 }
496
497 /**
498 * Does the work of actually building the email.
499 *
500 * @exception EmailException if there was an error.
501 * @since 1.0
502 */
503 public void buildMimeMessage() throws EmailException
504 {
505 try
506 {
507 build();
508 }
509 catch (MessagingException me)
510 {
511 throw new EmailException(me);
512 }
513 super.buildMimeMessage();
514 }
515
516 /**
517 * @throws EmailException EmailException
518 * @throws MessagingException MessagingException
519 */
520 private void build() throws MessagingException, EmailException
521 {
522 MimeMultipart rootContainer = this.getContainer();
523 MimeMultipart bodyEmbedsContainer = rootContainer;
524 MimeMultipart bodyContainer = rootContainer;
525 BodyPart msgHtml = null;
526 BodyPart msgText = null;
527
528 rootContainer.setSubType("mixed");
529
530 // determine how to form multiparts of email
531
532 if (EmailUtils.isNotEmpty(this.html) && this.inlineEmbeds.size() > 0)
533 {
534 //If HTML body and embeds are used, create a related container and add it to the root container
535 bodyEmbedsContainer = new MimeMultipart("related");
536 bodyContainer = bodyEmbedsContainer;
537 this.addPart(bodyEmbedsContainer, 0);
538
539 //If TEXT body was specified, create a alternative container and add it to the embeds container
540 if (EmailUtils.isNotEmpty(this.text))
541 {
542 bodyContainer = new MimeMultipart("alternative");
543 BodyPart bodyPart = createBodyPart();
544 try
545 {
546 bodyPart.setContent(bodyContainer);
547 bodyEmbedsContainer.addBodyPart(bodyPart, 0);
548 }
549 catch (MessagingException me)
550 {
551 throw new EmailException(me);
552 }
553 }
554 }
555 else if (EmailUtils.isNotEmpty(this.text) && EmailUtils.isNotEmpty(this.html))
556 {
557 //If both HTML and TEXT bodies are provided, create a alternative container and add it to the root container
558 bodyContainer = new MimeMultipart("alternative");
559 this.addPart(bodyContainer, 0);
560 }
561
562 if (EmailUtils.isNotEmpty(this.html))
563 {
564 msgHtml = new MimeBodyPart();
565 bodyContainer.addBodyPart(msgHtml, 0);
566
567 // apply default charset if one has been set
568 if (EmailUtils.isNotEmpty(this.charset))
569 {
570 msgHtml.setContent(
571 this.html,
572 Email.TEXT_HTML + "; charset=" + this.charset);
573 }
574 else
575 {
576 msgHtml.setContent(this.html, Email.TEXT_HTML);
577 }
578
579 Iterator iter = this.inlineEmbeds.values().iterator();
580 while (iter.hasNext())
581 {
582 InlineImage ii = (InlineImage) iter.next();
583 bodyEmbedsContainer.addBodyPart(ii.getMbp());
584 }
585 }
586
587 if (EmailUtils.isNotEmpty(this.text))
588 {
589 msgText = new MimeBodyPart();
590 bodyContainer.addBodyPart(msgText, 0);
591
592 // apply default charset if one has been set
593 if (EmailUtils.isNotEmpty(this.charset))
594 {
595 msgText.setContent(
596 this.text,
597 Email.TEXT_PLAIN + "; charset=" + this.charset);
598 }
599 else
600 {
601 msgText.setContent(this.text, Email.TEXT_PLAIN);
602 }
603 }
604 }
605
606 /**
607 * Private bean class that encapsulates data about URL contents
608 * that are embedded in the final email.
609 * @since 1.1
610 */
611 private static class InlineImage
612 {
613 /** content id */
614 private String cid;
615 /** <code>DataSource</code> for the content */
616 private DataSource dataSource;
617 /** the <code>MimeBodyPart</code> that contains the encoded data */
618 private MimeBodyPart mbp;
619
620 /**
621 * Creates an InlineImage object to represent the
622 * specified content ID and <code>MimeBodyPart</code>.
623 * @param cid the generated content ID
624 * @param dataSource the <code>DataSource</code> that represents the content
625 * @param mbp the <code>MimeBodyPart</code> that contains the encoded
626 * data
627 */
628 public InlineImage(String cid, DataSource dataSource, MimeBodyPart mbp)
629 {
630 this.cid = cid;
631 this.dataSource = dataSource;
632 this.mbp = mbp;
633 }
634
635 /**
636 * Returns the unique content ID of this InlineImage.
637 * @return the unique content ID of this InlineImage
638 */
639 public String getCid()
640 {
641 return cid;
642 }
643
644 /**
645 * Returns the <code>DataSource</code> that represents the encoded content.
646 * @return the <code>DataSource</code> representing the encoded content
647 */
648 public DataSource getDataSource()
649 {
650 return dataSource;
651 }
652
653 /**
654 * Returns the <code>MimeBodyPart</code> that contains the
655 * encoded InlineImage data.
656 * @return the <code>MimeBodyPart</code> containing the encoded
657 * InlineImage data
658 */
659 public MimeBodyPart getMbp()
660 {
661 return mbp;
662 }
663
664 // equals()/hashCode() implementations, since this class
665 // is stored as a entry in a Map.
666 /**
667 * {@inheritDoc}
668 * @return true if the other object is also an InlineImage with the same cid.
669 */
670 public boolean equals(Object obj)
671 {
672 if (this == obj)
673 {
674 return true;
675 }
676 if (!(obj instanceof InlineImage))
677 {
678 return false;
679 }
680
681 InlineImage that = (InlineImage) obj;
682
683 return this.cid.equals(that.cid);
684 }
685
686 /**
687 * {@inheritDoc}
688 * @return the cid hashCode.
689 */
690 public int hashCode()
691 {
692 return cid.hashCode();
693 }
694 }
695 }