MultiPartEmail.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.mail2.jakarta;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.util.Objects;

import org.apache.commons.mail2.core.EmailException;
import org.apache.commons.mail2.core.EmailUtils;
import org.apache.commons.mail2.jakarta.activation.PathDataSource;

import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.activation.FileDataSource;
import jakarta.activation.FileTypeMap;
import jakarta.activation.URLDataSource;
import jakarta.mail.BodyPart;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.internet.MimePart;
import jakarta.mail.internet.MimeUtility;

/**
 * A multipart email.
 * <p>
 * This class is used to send multi-part internet email like messages with attachments.
 * </p>
 * <p>
 * To create a multi-part email, call the default constructor and then you can call setMsg() to set the message and call the different attach() methods.
 * </p>
 *
 * @since 1.0
 */
public class MultiPartEmail extends Email {

    /** Body portion of the email. */
    private MimeMultipart container;

    /** The message container. */
    private BodyPart primaryBodyPart;

    /** The MIME subtype. */
    private String subType;

    /** Indicates if the message has been initialized. */
    private boolean initialized;

    /** Indicates if attachments have been added to the message. */
    private boolean hasAttachments;

    /**
     * Constructs a new instance.
     */
    public MultiPartEmail() {
        // empty
    }

    /**
     * Adds a new part to the email.
     *
     * @param multipart The MimeMultipart.
     * @return An Email.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.0
     */
    public Email addPart(final MimeMultipart multipart) throws EmailException {
        try {
            return addPart(multipart, getContainer().getCount());
        } catch (final MessagingException e) {
            throw new EmailException(e);
        }
    }

    /**
     * Adds a new part to the email.
     *
     * @param multipart The part to add.
     * @param index     The index to add at.
     * @return The email.
     * @throws EmailException An error occurred while adding the part.
     * @since 1.0
     */
    public Email addPart(final MimeMultipart multipart, final int index) throws EmailException {
        final BodyPart bodyPart = createBodyPart();
        try {
            bodyPart.setContent(multipart);
            getContainer().addBodyPart(bodyPart, index);
        } catch (final MessagingException e) {
            throw new EmailException(e);
        }

        return this;
    }

    /**
     * Adds a new part to the email.
     *
     * @param partContent     The content.
     * @param partContentType The content type.
     * @return An Email.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.0
     */
    public Email addPart(final String partContent, final String partContentType) throws EmailException {
        final BodyPart bodyPart = createBodyPart();
        try {
            bodyPart.setContent(partContent, partContentType);
            getContainer().addBodyPart(bodyPart);
        } catch (final MessagingException e) {
            throw new EmailException(e);
        }

        return this;
    }

    /**
     * Attaches a file specified as a DataSource interface.
     *
     * @param dataSource  A DataSource interface for the file.
     * @param name        The name field for the attachment.
     * @param description A description for the attachment.
     * @return A MultiPartEmail.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.0
     */
    public MultiPartEmail attach(final DataSource dataSource, final String name, final String description) throws EmailException {
        EmailException.checkNonNull(dataSource, () -> "Invalid Datasource.");
        // verify that the DataSource is valid
        try (InputStream inputStream = dataSource.getInputStream()) {
            EmailException.checkNonNull(inputStream, () -> "Invalid Datasource.");
        } catch (final IOException e) {
            throw new EmailException("Invalid Datasource.", e);
        }
        return attach(dataSource, name, description, EmailAttachment.ATTACHMENT);
    }

    /**
     * Attaches a file specified as a DataSource interface.
     *
     * @param dataSource  A DataSource interface for the file.
     * @param name        The name field for the attachment.
     * @param description A description for the attachment.
     * @param disposition Either mixed or inline.
     * @return A MultiPartEmail.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.0
     */
    public MultiPartEmail attach(final DataSource dataSource, String name, final String description, final String disposition) throws EmailException {
        if (EmailUtils.isEmpty(name)) {
            name = dataSource.getName();
        }
        try {
            final BodyPart bodyPart = createBodyPart();
            bodyPart.setDisposition(disposition);
            bodyPart.setFileName(MimeUtility.encodeText(name));
            bodyPart.setDescription(description);
            bodyPart.setDataHandler(new DataHandler(dataSource));
            getContainer().addBodyPart(bodyPart);
        } catch (final UnsupportedEncodingException | MessagingException e) {
            // in case the file name could not be encoded
            throw new EmailException(e);
        }
        setBoolHasAttachments(true);
        return this;
    }

    /**
     * Attaches an EmailAttachment.
     *
     * @param attachment An EmailAttachment.
     * @return A MultiPartEmail.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.0
     */
    public MultiPartEmail attach(final EmailAttachment attachment) throws EmailException {
        EmailException.checkNonNull(attachment, () -> "Invalid attachment.");
        MultiPartEmail result = null;
        final URL url = attachment.getURL();
        if (url == null) {
            String fileName = null;
            try {
                fileName = attachment.getPath();
                final File file = new File(fileName);
                if (!file.exists()) {
                    throw new IOException("\"" + fileName + "\" does not exist");
                }
                result = attach(new FileDataSource(file), attachment.getName(), attachment.getDescription(), attachment.getDisposition());
            } catch (final IOException e) {
                throw new EmailException("Cannot attach file \"" + fileName + "\"", e);
            }
        } else {
            result = attach(url, attachment.getName(), attachment.getDescription(), attachment.getDisposition());
        }
        return result;
    }

    /**
     * Attaches a file.
     *
     * @param file A file attachment
     * @return A MultiPartEmail.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.3
     */
    public MultiPartEmail attach(final File file) throws EmailException {
        final String fileName = file.getAbsolutePath();
        try {
            if (!file.exists()) {
                throw new IOException("\"" + fileName + "\" does not exist");
            }
            return attach(new FileDataSource(file), file.getName(), null, EmailAttachment.ATTACHMENT);
        } catch (final IOException e) {
            throw new EmailException("Cannot attach file \"" + fileName + "\"", e);
        }
    }

    /**
     * Attaches a path.
     *
     * @param file    A file attachment.
     * @param options options for opening file streams.
     * @return A MultiPartEmail.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.6.0
     */
    public MultiPartEmail attach(final Path file, final OpenOption... options) throws EmailException {
        final Path fileName = file.toAbsolutePath();
        try {
            if (!Files.exists(file)) {
                throw new IOException("\"" + fileName + "\" does not exist");
            }
            return attach(new PathDataSource(file, FileTypeMap.getDefaultFileTypeMap(), options), Objects.toString(file.getFileName(), null), null,
                    EmailAttachment.ATTACHMENT);
        } catch (final IOException e) {
            throw new EmailException("Cannot attach file \"" + fileName + "\"", e);
        }
    }

    /**
     * Attaches a file located by its URL. The disposition of the file is set to mixed.
     *
     * @param url         The URL of the file (may be any valid URL).
     * @param name        The name field for the attachment.
     * @param description A description for the attachment.
     * @return A MultiPartEmail.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.0
     */
    public MultiPartEmail attach(final URL url, final String name, final String description) throws EmailException {
        return attach(url, name, description, EmailAttachment.ATTACHMENT);
    }

    /**
     * Attaches a file located by its URL.
     *
     * @param url         The URL of the file (may be any valid URL).
     * @param name        The name field for the attachment.
     * @param description A description for the attachment.
     * @param disposition Either mixed or inline.
     * @return A MultiPartEmail.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.0
     */
    public MultiPartEmail attach(final URL url, final String name, final String description, final String disposition) throws EmailException {
        // verify that the URL is valid
        try {
            url.openStream().close();
        } catch (final IOException e) {
            throw new EmailException("Invalid URL set:" + url, e);
        }
        return attach(new URLDataSource(url), name, description, disposition);
    }

    /**
     * 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
     * MimeMessage without commons-email.
     *
     * @throws EmailException if there was an error.
     * @since 1.0
     */
    @Override
    public void buildMimeMessage() throws EmailException {
        try {
            if (primaryBodyPart != null) {
                // before a multipart message can be sent, we must make sure that
                // the content for the main body part was actually set. If not,
                // an IOException will be thrown during super.send().

                final BodyPart body = getPrimaryBodyPart();
                try {
                    body.getContent();
                } catch (final IOException e) { // NOPMD
                    // do nothing here.
                    // content will be set to an empty string as a result.
                    // (Should this really be rethrown as an email exception?)
                    // throw new EmailException(e);
                }
            }

            if (subType != null) {
                getContainer().setSubType(subType);
            }

            super.buildMimeMessage();
        } catch (final MessagingException e) {
            throw new EmailException(e);
        }
    }

    /**
     * Creates a body part object. Can be overridden if you don't want to create a BodyPart.
     *
     * @return the created body part
     */
    protected BodyPart createBodyPart() {
        return new MimeBodyPart();
    }

    /**
     * Creates a mime multipart object.
     *
     * @return the created mime part
     */
    protected MimeMultipart createMimeMultipart() {
        return new MimeMultipart();
    }

    /**
     * Gets the message container.
     *
     * @return The message container.
     * @since 1.0
     */
    protected MimeMultipart getContainer() {
        if (!initialized) {
            init();
        }
        return container;
    }

    /**
     * Gets first body part of the message.
     *
     * @return The primary body part.
     * @throws MessagingException An error occurred while getting the primary body part.
     * @since 1.0
     */
    protected BodyPart getPrimaryBodyPart() throws MessagingException {
        if (!initialized) {
            init();
        }
        // Add the first body part to the message. The fist body part must be
        if (primaryBodyPart == null) {
            primaryBodyPart = createBodyPart();
            getContainer().addBodyPart(primaryBodyPart, 0);
        }
        return primaryBodyPart;
    }

    /**
     * Gets the MIME subtype of the email.
     *
     * @return MIME subtype of the email
     * @since 1.0
     */
    public String getSubType() {
        return subType;
    }

    /**
     * Initialize the multipart email.
     *
     * @since 1.0
     */
    protected void init() {
        if (initialized) {
            throw new IllegalStateException("Already initialized");
        }
        container = createMimeMultipart();
        super.setContent(container);
        initialized = true;
    }

    /**
     * Tests whether there are attachments.
     *
     * @return true if there are attachments
     * @since 1.0
     */
    public boolean isBoolHasAttachments() {
        return hasAttachments;
    }

    /**
     * Tests if this object is initialized.
     *
     * @return true if initialized
     */
    protected boolean isInitialized() {
        return initialized;
    }

    /**
     * Sets whether there are attachments.
     *
     * @param hasAttachments the attachments flag
     * @since 1.0
     */
    public void setBoolHasAttachments(final boolean hasAttachments) {
        this.hasAttachments = hasAttachments;
    }

    /**
     * Sets the initialized status of this object.
     *
     * @param initialized the initialized status flag
     */
    protected void setInitialized(final boolean initialized) {
        this.initialized = initialized;
    }

    /**
     * Sets the message of the email.
     *
     * @param msg A String.
     * @return An Email.
     * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
     * @since 1.0
     */
    @Override
    public Email setMsg(final String msg) throws EmailException {
        EmailException.checkNonEmpty(msg, () -> "Invalid message.");
        try {
            final BodyPart primary = getPrimaryBodyPart();
            if (primary instanceof MimePart && EmailUtils.isNotEmpty(getCharsetName())) {
                ((MimePart) primary).setText(msg, getCharsetName());
            } else {
                primary.setText(msg);
            }
        } catch (final MessagingException e) {
            throw new EmailException(e);
        }
        return this;
    }

    /**
     * Sets the MIME subtype of the email.
     *
     * @param subType MIME subtype of the email
     * @since 1.0
     */
    public void setSubType(final String subType) {
        this.subType = subType;
    }

}