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 }