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 */ 017package org.apache.commons.mail; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.UnsupportedEncodingException; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Locale; 028import java.util.Map; 029 030import javax.activation.DataHandler; 031import javax.activation.DataSource; 032import javax.activation.FileDataSource; 033import javax.activation.URLDataSource; 034import javax.mail.BodyPart; 035import javax.mail.MessagingException; 036import javax.mail.internet.MimeBodyPart; 037import javax.mail.internet.MimeMultipart; 038 039/** 040 * An HTML multipart email. 041 * 042 * <p>This class is used to send HTML formatted email. A text message 043 * can also be set for HTML unaware email clients, such as text-based 044 * email clients. 045 * 046 * <p>This class also inherits from {@link MultiPartEmail}, so it is easy to 047 * add attachments to the email. 048 * 049 * <p>To send an email in HTML, one should create a <code>HtmlEmail</code>, then 050 * use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods. 051 * The HTML content can be set with the {@link #setHtmlMsg(String)} method. The 052 * alternative text content can be set with {@link #setTextMsg(String)}. 053 * 054 * <p>Either the text or HTML can be omitted, in which case the "main" 055 * part of the multipart becomes whichever is supplied rather than a 056 * <code>multipart/alternative</code>. 057 * 058 * <h3>Embedding Images and Media</h3> 059 * 060 * <p>It is also possible to embed URLs, files, or arbitrary 061 * <code>DataSource</code>s directly into the body of the mail: 062 * <pre> 063 * HtmlEmail he = new HtmlEmail(); 064 * File img = new File("my/image.gif"); 065 * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class 066 * StringBuffer msg = new StringBuffer(); 067 * msg.append("<html><body>"); 068 * msg.append("<img src=cid:").append(he.embed(img)).append(">"); 069 * msg.append("<img src=cid:").append(he.embed(png)).append(">"); 070 * msg.append("</body></html>"); 071 * he.setHtmlMsg(msg.toString()); 072 * // code to set the other email fields (not shown) 073 * </pre> 074 * 075 * <p>Embedded entities are tracked by their name, which for <code>File</code>s is 076 * the filename itself and for <code>URL</code>s is the canonical path. It is 077 * an error to bind the same name to more than one entity, and this class will 078 * attempt to validate that for <code>File</code>s and <code>URL</code>s. When 079 * embedding a <code>DataSource</code>, the code uses the <code>equals()</code> 080 * method defined on the <code>DataSource</code>s to make the determination. 081 * 082 * @since 1.0 083 */ 084public class HtmlEmail extends MultiPartEmail 085{ 086 /** Definition of the length of generated CID's. */ 087 public static final int CID_LENGTH = 10; 088 089 /** prefix for default HTML mail. */ 090 private static final String HTML_MESSAGE_START = "<html><body><pre>"; 091 /** suffix for default HTML mail. */ 092 private static final String HTML_MESSAGE_END = "</pre></body></html>"; 093 094 095 /** 096 * Text part of the message. This will be used as alternative text if 097 * the email client does not support HTML messages. 098 */ 099 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<String, InlineImage> 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}