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.net.MalformedURLException;
023import java.net.URL;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Objects;
028
029import javax.activation.DataHandler;
030import javax.activation.DataSource;
031import javax.activation.FileDataSource;
032import javax.activation.URLDataSource;
033import javax.mail.BodyPart;
034import javax.mail.MessagingException;
035import javax.mail.internet.MimeBodyPart;
036import javax.mail.internet.MimeMultipart;
037
038/**
039 * An HTML multipart email.
040 * <p>
041 * This class is used to send HTML formatted email. A text message can also be set for HTML unaware email clients, such as text-based email clients.
042 * </p>
043 * <p>
044 * This class also inherits from {@link MultiPartEmail}, so it is easy to add attachments to the email.
045 * </p>
046 * <p>
047 * To send an email in HTML, one should create a {@code HtmlEmail}, then use the {@link #setFrom(String)}, {@link #addTo(String)} etc. methods. The HTML content
048 * can be set with the {@link #setHtmlMsg(String)} method. The alternative text content can be set with {@link #setTextMsg(String)}.
049 * </p>
050 * <p>
051 * Either the text or HTML can be omitted, in which case the "main" part of the multipart becomes whichever is supplied rather than a
052 * {@code multipart/alternative}.
053 * </p>
054 * <h2>Embedding Images and Media</h2>
055 * <p>
056 * It is also possible to embed URLs, files, or arbitrary {@code DataSource}s directly into the body of the mail:
057 * </p>
058 *
059 * <pre>
060 * HtmlEmail he = new HtmlEmail();
061 * File img = new File("my/image.gif");
062 * PNGDataSource png = new PNGDataSource(decodedPNGOutputStream); // a custom class
063 * StringBuffer msg = new StringBuffer();
064 * msg.append("&lt;html&gt;&lt;body&gt;");
065 * msg.append("&lt;img src=cid:").append(he.embed(img)).append("&gt;");
066 * msg.append("&lt;img src=cid:").append(he.embed(png)).append("&gt;");
067 * msg.append("&lt;/body&gt;&lt;/html&gt;");
068 * he.setHtmlMsg(msg.toString());
069 * // code to set the other email fields (not shown)
070 * </pre>
071 * <p>
072 * Embedded entities are tracked by their name, which for {@code File}s is the file name itself and for {@code URL}s is the canonical path. It is an error to
073 * bind the same name to more than one entity, and this class will attempt to validate that for {@code File}s and {@code URL}s. When embedding a
074 * {@code DataSource}, the code uses the {@code equals()} method defined on the {@code DataSource}s to make the determination.
075 * </p>
076 *
077 * @since 1.0
078 */
079public class HtmlEmail extends MultiPartEmail {
080
081    /**
082     * Private bean class that encapsulates data about URL contents that are embedded in the final email.
083     *
084     * @since 1.1
085     */
086    private static final class InlineImage {
087
088        /** Content id. */
089        private final String cid;
090
091        /** {@code DataSource} for the content. */
092        private final DataSource dataSource;
093
094        /** The {@code MimeBodyPart} that contains the encoded data. */
095        private final MimeBodyPart mimeBodyPart;
096
097        /**
098         * Creates an InlineImage object to represent the specified content ID and {@code MimeBodyPart}.
099         *
100         * @param cid          the generated content ID, not null.
101         * @param dataSource   the {@code DataSource} that represents the content, not null.
102         * @param mimeBodyPart the {@code MimeBodyPart} that contains the encoded data, not null.
103         */
104        private InlineImage(final String cid, final DataSource dataSource, final MimeBodyPart mimeBodyPart) {
105            this.cid = Objects.requireNonNull(cid, "cid");
106            this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
107            this.mimeBodyPart = Objects.requireNonNull(mimeBodyPart, "mimeBodyPart");
108        }
109
110        @Override
111        public boolean equals(final Object obj) {
112            if (this == obj) {
113                return true;
114            }
115            if (!(obj instanceof InlineImage)) {
116                return false;
117            }
118            final InlineImage other = (InlineImage) obj;
119            return Objects.equals(cid, other.cid);
120        }
121
122        /**
123         * Returns the unique content ID of this InlineImage.
124         *
125         * @return the unique content ID of this InlineImage
126         */
127        private String getCid() {
128            return cid;
129        }
130
131        /**
132         * Returns the {@code DataSource} that represents the encoded content.
133         *
134         * @return the {@code DataSource} representing the encoded content
135         */
136        private DataSource getDataSource() {
137            return dataSource;
138        }
139
140        /**
141         * Returns the {@code MimeBodyPart} that contains the encoded InlineImage data.
142         *
143         * @return the {@code MimeBodyPart} containing the encoded InlineImage data
144         */
145        private MimeBodyPart getMimeBodyPart() {
146            return mimeBodyPart;
147        }
148
149        @Override
150        public int hashCode() {
151            return Objects.hash(cid);
152        }
153    }
154
155    /** Definition of the length of generated CID's. */
156    public static final int CID_LENGTH = 10;
157
158    /** Prefix for default HTML mail. */
159    private static final String HTML_MESSAGE_START = "<html><body><pre>";
160
161    /** Suffix for default HTML mail. */
162    private static final String HTML_MESSAGE_END = "</pre></body></html>";
163
164    /**
165     * Text part of the message. This will be used as alternative text if the email client does not support HTML messages.
166     *
167     * @deprecated Use getters and getters.
168     */
169    @Deprecated
170    protected String text;
171
172    /**
173     * HTML part of the message.
174     *
175     * @deprecated Use getters and getters.
176     */
177    @Deprecated
178    protected String html;
179
180    /**
181     * @deprecated As of commons-email 1.1, no longer used. Inline embedded objects are now stored in {@link #inlineEmbeds}.
182     */
183    @Deprecated
184    protected List<InlineImage> inlineImages;
185
186    /**
187     * Embedded images Map&lt;String, InlineImage&gt; where the key is the user-defined image name.
188     *
189     * @deprecated Use getters and getters.
190     */
191    @Deprecated
192    protected Map<String, InlineImage> inlineEmbeds = new HashMap<>();
193
194    /**
195     * Constructs a new instance.
196     */
197    public HtmlEmail() {
198        // empty
199    }
200
201    /**
202     * @throws EmailException     EmailException
203     * @throws MessagingException MessagingException
204     */
205    private void build() throws MessagingException, EmailException {
206        final MimeMultipart rootContainer = getContainer();
207        MimeMultipart bodyEmbedsContainer = rootContainer;
208        MimeMultipart bodyContainer = rootContainer;
209        MimeBodyPart msgHtml = null;
210        MimeBodyPart msgText = null;
211
212        rootContainer.setSubType("mixed");
213
214        // determine how to form multiparts of email
215
216        if (EmailUtils.isNotEmpty(html) && !EmailUtils.isEmpty(inlineEmbeds)) {
217            // If HTML body and embeds are used, create a related container and add it to the root container
218            bodyEmbedsContainer = new MimeMultipart("related");
219            bodyContainer = bodyEmbedsContainer;
220            addPart(bodyEmbedsContainer, 0);
221
222            // If TEXT body was specified, create a alternative container and add it to the embeds container
223            if (EmailUtils.isNotEmpty(text)) {
224                bodyContainer = new MimeMultipart("alternative");
225                final BodyPart bodyPart = createBodyPart();
226                try {
227                    bodyPart.setContent(bodyContainer);
228                    bodyEmbedsContainer.addBodyPart(bodyPart, 0);
229                } catch (final MessagingException e) {
230                    throw new EmailException(e);
231                }
232            }
233        } else if (EmailUtils.isNotEmpty(text) && EmailUtils.isNotEmpty(html)) {
234            // EMAIL-142: if we have both an HTML and TEXT body, but no attachments or
235            // inline images, the root container should have mimetype
236            // "multipart/alternative".
237            // reference: http://tools.ietf.org/html/rfc2046#section-5.1.4
238            if (!EmailUtils.isEmpty(inlineEmbeds) || isBoolHasAttachments()) {
239                // If both HTML and TEXT bodies are provided, create an alternative
240                // container and add it to the root container
241                bodyContainer = new MimeMultipart("alternative");
242                this.addPart(bodyContainer, 0);
243            } else {
244                // no attachments or embedded images present, change the mimetype
245                // of the root container (= body container)
246                rootContainer.setSubType("alternative");
247            }
248        }
249
250        if (EmailUtils.isNotEmpty(html)) {
251            msgHtml = new MimeBodyPart();
252            bodyContainer.addBodyPart(msgHtml, 0);
253
254            // EMAIL-104: call explicitly setText to use default mime charset
255            // (property "mail.mime.charset") in case none has been set
256            msgHtml.setText(html, getCharsetName(), EmailConstants.TEXT_SUBTYPE_HTML);
257
258            // EMAIL-147: work-around for buggy JavaMail implementations;
259            // in case setText(...) does not set the correct content type,
260            // use the setContent() method instead.
261            final String contentType = msgHtml.getContentType();
262            if (contentType == null || !contentType.equals(EmailConstants.TEXT_HTML)) {
263                // apply default charset if one has been set
264                if (EmailUtils.isNotEmpty(getCharsetName())) {
265                    msgHtml.setContent(html, EmailConstants.TEXT_HTML + "; charset=" + getCharsetName());
266                } else {
267                    // unfortunately, MimeUtility.getDefaultMIMECharset() is package private
268                    // and thus can not be used to set the default system charset in case
269                    // no charset has been provided by the user
270                    msgHtml.setContent(html, EmailConstants.TEXT_HTML);
271                }
272            }
273
274            for (final InlineImage image : inlineEmbeds.values()) {
275                bodyEmbedsContainer.addBodyPart(image.getMimeBodyPart());
276            }
277        }
278
279        if (EmailUtils.isNotEmpty(text)) {
280            msgText = new MimeBodyPart();
281            bodyContainer.addBodyPart(msgText, 0);
282
283            // EMAIL-104: call explicitly setText to use default mime charset
284            // (property "mail.mime.charset") in case none has been set
285            msgText.setText(text, getCharsetName());
286        }
287    }
288
289    /**
290     * Builds the MimeMessage. Please note that a user rarely calls this method directly and only if he/she is interested in the sending the underlying
291     * MimeMessage without commons-email.
292     *
293     * @throws EmailException if there was an error.
294     * @since 1.0
295     */
296    @Override
297    public void buildMimeMessage() throws EmailException {
298        try {
299            build();
300        } catch (final MessagingException e) {
301            throw new EmailException(e);
302        }
303        super.buildMimeMessage();
304    }
305
306    /**
307     * Embeds the specified {@code DataSource} in the HTML using a randomly generated Content-ID. Returns the generated Content-ID string.
308     *
309     * @param dataSource the {@code DataSource} to embed
310     * @param name       the name that will be set in the file name header field
311     * @return the generated Content-ID for this {@code DataSource}
312     * @throws EmailException if the embedding fails or if {@code name} is null or empty
313     * @see #embed(DataSource, String, String)
314     * @since 1.1
315     */
316    public String embed(final DataSource dataSource, final String name) throws EmailException {
317        // check if the DataSource has already been attached;
318        // if so, return the cached CID value.
319        final InlineImage inlineImage = inlineEmbeds.get(name);
320        if (inlineImage != null) {
321            // make sure the supplied URL points to the same thing
322            // as the one already associated with this name.
323            if (dataSource.equals(inlineImage.getDataSource())) {
324                return inlineImage.getCid();
325            }
326            throw new EmailException("embedded DataSource '" + name + "' is already bound to name " + inlineImage.getDataSource().toString()
327                    + "; existing names cannot be rebound");
328        }
329
330        final String cid = EmailUtils.toLower(EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH));
331        return embed(dataSource, name, cid);
332    }
333
334    /**
335     * Embeds the specified {@code DataSource} in the HTML using the specified Content-ID. Returns the specified Content-ID string.
336     *
337     * @param dataSource the {@code DataSource} to embed
338     * @param name       the name that will be set in the file name header field
339     * @param cid        the Content-ID to use for this {@code DataSource}
340     * @return the URL encoded Content-ID for this {@code DataSource}
341     * @throws EmailException if the embedding fails or if {@code name} is null or empty
342     * @since 1.1
343     */
344    public String embed(final DataSource dataSource, final String name, final String cid) throws EmailException {
345        EmailException.checkNonEmpty(name, () -> "Name cannot be null or empty");
346        final MimeBodyPart mbp = new MimeBodyPart();
347        try {
348            // URL encode the cid according to RFC 2392
349            final String encodedCid = EmailUtils.encodeUrl(cid);
350            mbp.setDataHandler(new DataHandler(dataSource));
351            mbp.setFileName(name);
352            mbp.setDisposition(EmailAttachment.INLINE);
353            mbp.setContentID("<" + encodedCid + ">");
354            this.inlineEmbeds.put(name, new InlineImage(encodedCid, dataSource, mbp));
355            return encodedCid;
356        } catch (final MessagingException e) {
357            throw new EmailException(e);
358        }
359    }
360
361    /**
362     * Embeds a file in the HTML. This implementation delegates to {@link #embed(File, String)}.
363     *
364     * @param file The {@code File} object to embed
365     * @return A String with the Content-ID of the file.
366     * @throws EmailException when the supplied {@code File} cannot be used; also see {@link javax.mail.internet.MimeBodyPart} for definitions
367     *
368     * @see #embed(File, String)
369     * @since 1.1
370     */
371    public String embed(final File file) throws EmailException {
372        return embed(file, EmailUtils.toLower(EmailUtils.randomAlphabetic(HtmlEmail.CID_LENGTH)));
373    }
374
375    /**
376     * Embeds a file in the HTML.
377     *
378     * <p>
379     * This method embeds a file located by an URL into the mail body. It allows, for instance, to add inline images to the email. Inline files may be
380     * referenced with a {@code cid:xxxxxx} URL, where xxxxxx is the Content-ID returned by the embed function. Files are bound to their names, which is the
381     * value returned by {@link java.io.File#getName()}. If the same file is embedded multiple times, the same CID is guaranteed to be returned.
382     *
383     * <p>
384     * While functionally the same as passing {@code FileDataSource} to {@link #embed(DataSource, String, String)}, this method attempts to validate the file
385     * before embedding it in the message and will throw {@code EmailException} if the validation fails. In this case, the {@code HtmlEmail} object will not be
386     * changed.
387     *
388     * @param file The {@code File} to embed
389     * @param cid  the Content-ID to use for the embedded {@code File}
390     * @return A String with the Content-ID of the file.
391     * @throws EmailException when the supplied {@code File} cannot be used or if the file has already been embedded; also see
392     *                        {@link javax.mail.internet.MimeBodyPart} for definitions
393     * @since 1.1
394     */
395    public String embed(final File file, final String cid) throws EmailException {
396        EmailException.checkNonEmpty(file.getName(), () -> "File name cannot be null or empty");
397
398        // verify that the File can provide a canonical path
399        String filePath = null;
400        try {
401            filePath = file.getCanonicalPath();
402        } catch (final IOException e) {
403            throw new EmailException("couldn't get canonical path for " + file.getName(), e);
404        }
405
406        // check if a FileDataSource for this name has already been attached;
407        // if so, return the cached CID value.
408        final InlineImage inlineImage = inlineEmbeds.get(file.getName());
409        if (inlineImage != null) {
410            final FileDataSource fileDataSource = (FileDataSource) inlineImage.getDataSource();
411            // make sure the supplied file has the same canonical path
412            // as the one already associated with this name.
413            String existingFilePath = null;
414            try {
415                existingFilePath = fileDataSource.getFile().getCanonicalPath();
416            } catch (final IOException e) {
417                throw new EmailException("couldn't get canonical path for file " + fileDataSource.getFile().getName() + "which has already been embedded", e);
418            }
419            if (filePath.equals(existingFilePath)) {
420                return inlineImage.getCid();
421            }
422            throw new EmailException(
423                    "embedded name '" + file.getName() + "' is already bound to file " + existingFilePath + "; existing names cannot be rebound");
424        }
425
426        // verify that the file is valid
427        if (!file.exists()) {
428            throw new EmailException("file " + filePath + " doesn't exist");
429        }
430        if (!file.isFile()) {
431            throw new EmailException("file " + filePath + " isn't a normal file");
432        }
433        if (!file.canRead()) {
434            throw new EmailException("file " + filePath + " isn't readable");
435        }
436
437        return embed(new FileDataSource(file), file.getName(), cid);
438    }
439
440    /**
441     * Parses the specified {@code String} as a URL that will then be embedded in the message.
442     *
443     * @param urlString String representation of the URL.
444     * @param name      The name that will be set in the file name header field.
445     * @return A String with the Content-ID of the URL.
446     * @throws EmailException when URL supplied is invalid or if {@code name} is null or empty; also see {@link javax.mail.internet.MimeBodyPart} for
447     *                        definitions
448     *
449     * @see #embed(URL, String)
450     * @since 1.1
451     */
452    public String embed(final String urlString, final String name) throws EmailException {
453        try {
454            return embed(new URL(urlString), name);
455        } catch (final MalformedURLException e) {
456            throw new EmailException("Invalid URL", e);
457        }
458    }
459
460    /**
461     * Embeds an URL in the HTML.
462     *
463     * <p>
464     * This method embeds a file located by an URL into the mail body. It allows, for instance, to add inline images to the email. Inline files may be
465     * referenced with a {@code cid:xxxxxx} URL, where xxxxxx is the Content-ID returned by the embed function. It is an error to bind the same name to more
466     * than one URL; if the same URL is embedded multiple times, the same Content-ID is guaranteed to be returned.
467     * </p>
468     * <p>
469     * While functionally the same as passing {@code URLDataSource} to {@link #embed(DataSource, String, String)}, this method attempts to validate the URL
470     * before embedding it in the message and will throw {@code EmailException} if the validation fails. In this case, the {@code HtmlEmail} object will not be
471     * changed.
472     * </p>
473     * <p>
474     * NOTE: Clients should take care to ensure that different URLs are bound to different names. This implementation tries to detect this and throw
475     * {@code EmailException}. However, it is not guaranteed to catch all cases, especially when the URL refers to a remote HTTP host that may be part of a
476     * virtual host cluster.
477     * </p>
478     *
479     * @param url  The URL of the file.
480     * @param name The name that will be set in the file name header field.
481     * @return A String with the Content-ID of the file.
482     * @throws EmailException when URL supplied is invalid or if {@code name} is null or empty; also see {@link javax.mail.internet.MimeBodyPart} for
483     *                        definitions
484     * @since 1.0
485     */
486    public String embed(final URL url, final String name) throws EmailException {
487        EmailException.checkNonEmpty(name, () -> "Name cannot be null or empty");
488        // check if a URLDataSource for this name has already been attached;
489        // if so, return the cached CID value.
490        final InlineImage inlineImage = inlineEmbeds.get(name);
491        if (inlineImage != null) {
492            final URLDataSource urlDataSource = (URLDataSource) inlineImage.getDataSource();
493            // make sure the supplied URL points to the same thing
494            // as the one already associated with this name.
495            // NOTE: Comparing URLs with URL.equals() is a blocking operation
496            // in the case of a network failure therefore we use
497            // url.toExternalForm().equals() here.
498            if (url.toExternalForm().equals(urlDataSource.getURL().toExternalForm())) {
499                return inlineImage.getCid();
500            }
501            throw new EmailException("embedded name '" + name + "' is already bound to URL " + urlDataSource.getURL() + "; existing names cannot be rebound");
502        }
503        // verify that the URL is valid
504        try (InputStream inputStream = url.openStream()) {
505            // Make sure we can read.
506            inputStream.read();
507        } catch (final IOException e) {
508            throw new EmailException("Invalid URL", e);
509        }
510        return embed(new URLDataSource(url), name);
511    }
512
513    /**
514     * Gets the HTML content.
515     *
516     * @return the HTML content.
517     * @since 1.6.0
518     */
519    public String getHtml() {
520        return html;
521    }
522
523    /**
524     * Gets the message text.
525     *
526     * @return the message text.
527     * @since 1.6.0
528     */
529    public String getText() {
530        return text;
531    }
532
533    /**
534     * Sets the HTML content.
535     *
536     * @param html A String.
537     * @return An HtmlEmail.
538     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
539     * @since 1.0
540     */
541    public HtmlEmail setHtmlMsg(final String html) throws EmailException {
542        this.html = EmailException.checkNonEmpty(html, () -> "Invalid message.");
543        return this;
544    }
545
546    /**
547     * Sets the message.
548     *
549     * <p>
550     * This method overrides {@link MultiPartEmail#setMsg(String)} in order to send an HTML message instead of a plain text message in the mail body. The
551     * message is formatted in HTML for the HTML part of the message; it is left as is in the alternate text part.
552     * </p>
553     *
554     * @param msg the message text to use
555     * @return this {@code HtmlEmail}
556     * @throws EmailException if msg is null or empty; see javax.mail.internet.MimeBodyPart for definitions
557     * @since 1.0
558     */
559    @Override
560    public Email setMsg(final String msg) throws EmailException {
561        setTextMsg(msg);
562        final StringBuilder htmlMsgBuf = new StringBuilder(msg.length() + HTML_MESSAGE_START.length() + HTML_MESSAGE_END.length());
563        htmlMsgBuf.append(HTML_MESSAGE_START).append(msg).append(HTML_MESSAGE_END);
564        setHtmlMsg(htmlMsgBuf.toString());
565        return this;
566    }
567
568    /**
569     * Sets the text content.
570     *
571     * @param text A String.
572     * @return An HtmlEmail.
573     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
574     * @since 1.0
575     */
576    public HtmlEmail setTextMsg(final String text) throws EmailException {
577        this.text = EmailException.checkNonEmpty(text, () -> "Invalid message.");
578        return this;
579    }
580}