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