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.mail2.javax;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.UnsupportedEncodingException;
023import java.net.URL;
024import java.nio.file.Files;
025import java.nio.file.OpenOption;
026import java.nio.file.Path;
027import java.util.Objects;
028
029import javax.activation.DataHandler;
030import javax.activation.DataSource;
031import javax.activation.FileDataSource;
032import javax.activation.FileTypeMap;
033import javax.activation.URLDataSource;
034import javax.mail.BodyPart;
035import javax.mail.MessagingException;
036import javax.mail.internet.MimeBodyPart;
037import javax.mail.internet.MimeMultipart;
038import javax.mail.internet.MimePart;
039import javax.mail.internet.MimeUtility;
040
041import org.apache.commons.mail2.core.EmailException;
042import org.apache.commons.mail2.core.EmailUtils;
043import org.apache.commons.mail2.javax.activation.PathDataSource;
044
045/**
046 * A multipart email.
047 * <p>
048 * This class is used to send multi-part internet email like messages with attachments.
049 * </p>
050 * <p>
051 * 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.
052 * </p>
053 *
054 * @since 1.0
055 */
056public class MultiPartEmail extends Email {
057
058    /** Body portion of the email. */
059    private MimeMultipart container;
060
061    /** The message container. */
062    private BodyPart primaryBodyPart;
063
064    /** The MIME subtype. */
065    private String subType;
066
067    /** Indicates if the message has been initialized. */
068    private boolean initialized;
069
070    /** Indicates if attachments have been added to the message. */
071    private boolean hasAttachments;
072
073    /**
074     * Constructs a new instance.
075     */
076    public MultiPartEmail() {
077        // empty
078    }
079
080    /**
081     * Adds a new part to the email.
082     *
083     * @param multipart The MimeMultipart.
084     * @return An Email.
085     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
086     * @since 1.0
087     */
088    public Email addPart(final MimeMultipart multipart) throws EmailException {
089        try {
090            return addPart(multipart, getContainer().getCount());
091        } catch (final MessagingException e) {
092            throw new EmailException(e);
093        }
094    }
095
096    /**
097     * Adds a new part to the email.
098     *
099     * @param multipart The part to add.
100     * @param index     The index to add at.
101     * @return The email.
102     * @throws EmailException An error occurred while adding the part.
103     * @since 1.0
104     */
105    public Email addPart(final MimeMultipart multipart, final int index) throws EmailException {
106        final BodyPart bodyPart = createBodyPart();
107        try {
108            bodyPart.setContent(multipart);
109            getContainer().addBodyPart(bodyPart, index);
110        } catch (final MessagingException e) {
111            throw new EmailException(e);
112        }
113
114        return this;
115    }
116
117    /**
118     * Adds a new part to the email.
119     *
120     * @param partContent     The content.
121     * @param partContentType The content type.
122     * @return An Email.
123     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
124     * @since 1.0
125     */
126    public Email addPart(final String partContent, final String partContentType) throws EmailException {
127        final BodyPart bodyPart = createBodyPart();
128        try {
129            bodyPart.setContent(partContent, partContentType);
130            getContainer().addBodyPart(bodyPart);
131        } catch (final MessagingException e) {
132            throw new EmailException(e);
133        }
134
135        return this;
136    }
137
138    /**
139     * Attaches a file specified as a DataSource interface.
140     *
141     * @param dataSource  A DataSource interface for the file.
142     * @param name        The name field for the attachment.
143     * @param description A description for the attachment.
144     * @return A MultiPartEmail.
145     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
146     * @since 1.0
147     */
148    public MultiPartEmail attach(final DataSource dataSource, final String name, final String description) throws EmailException {
149        EmailException.checkNonNull(dataSource, () -> "Invalid Datasource.");
150        // verify that the DataSource is valid
151        try (InputStream inputStream = dataSource.getInputStream()) {
152            EmailException.checkNonNull(inputStream, () -> "Invalid Datasource.");
153        } catch (final IOException e) {
154            throw new EmailException("Invalid Datasource.", e);
155        }
156        return attach(dataSource, name, description, EmailAttachment.ATTACHMENT);
157    }
158
159    /**
160     * Attaches a file specified as a DataSource interface.
161     *
162     * @param dataSource  A DataSource interface for the file.
163     * @param name        The name field for the attachment.
164     * @param description A description for the attachment.
165     * @param disposition Either mixed or inline.
166     * @return A MultiPartEmail.
167     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
168     * @since 1.0
169     */
170    public MultiPartEmail attach(final DataSource dataSource, String name, final String description, final String disposition) throws EmailException {
171        if (EmailUtils.isEmpty(name)) {
172            name = dataSource.getName();
173        }
174        try {
175            final BodyPart bodyPart = createBodyPart();
176            bodyPart.setDisposition(disposition);
177            bodyPart.setFileName(MimeUtility.encodeText(name));
178            bodyPart.setDescription(description);
179            bodyPart.setDataHandler(new DataHandler(dataSource));
180            getContainer().addBodyPart(bodyPart);
181        } catch (final UnsupportedEncodingException | MessagingException e) {
182            // in case the file name could not be encoded
183            throw new EmailException(e);
184        }
185        setBoolHasAttachments(true);
186        return this;
187    }
188
189    /**
190     * Attaches an EmailAttachment.
191     *
192     * @param attachment An EmailAttachment.
193     * @return A MultiPartEmail.
194     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
195     * @since 1.0
196     */
197    public MultiPartEmail attach(final EmailAttachment attachment) throws EmailException {
198        EmailException.checkNonNull(attachment, () -> "Invalid attachment.");
199        MultiPartEmail result = null;
200        final URL url = attachment.getURL();
201        if (url == null) {
202            String fileName = null;
203            try {
204                fileName = attachment.getPath();
205                final File file = new File(fileName);
206                if (!file.exists()) {
207                    throw new IOException("\"" + fileName + "\" does not exist");
208                }
209                result = attach(new FileDataSource(file), attachment.getName(), attachment.getDescription(), attachment.getDisposition());
210            } catch (final IOException e) {
211                throw new EmailException("Cannot attach file \"" + fileName + "\"", e);
212            }
213        } else {
214            result = attach(url, attachment.getName(), attachment.getDescription(), attachment.getDisposition());
215        }
216        return result;
217    }
218
219    /**
220     * Attaches a file.
221     *
222     * @param file A file attachment
223     * @return A MultiPartEmail.
224     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
225     * @since 1.3
226     */
227    public MultiPartEmail attach(final File file) throws EmailException {
228        final String fileName = file.getAbsolutePath();
229        try {
230            if (!file.exists()) {
231                throw new IOException("\"" + fileName + "\" does not exist");
232            }
233            return attach(new FileDataSource(file), file.getName(), null, EmailAttachment.ATTACHMENT);
234        } catch (final IOException e) {
235            throw new EmailException("Cannot attach file \"" + fileName + "\"", e);
236        }
237    }
238
239    /**
240     * Attaches a path.
241     *
242     * @param file    A file attachment.
243     * @param options options for opening file streams.
244     * @return A MultiPartEmail.
245     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
246     * @since 1.6.0
247     */
248    public MultiPartEmail attach(final Path file, final OpenOption... options) throws EmailException {
249        final Path fileName = file.toAbsolutePath();
250        try {
251            if (!Files.exists(file)) {
252                throw new IOException("\"" + fileName + "\" does not exist");
253            }
254            return attach(new PathDataSource(file, FileTypeMap.getDefaultFileTypeMap(), options), Objects.toString(file.getFileName(), null), null,
255                    EmailAttachment.ATTACHMENT);
256        } catch (final IOException e) {
257            throw new EmailException("Cannot attach file \"" + fileName + "\"", e);
258        }
259    }
260
261    /**
262     * Attaches a file located by its URL. The disposition of the file is set to mixed.
263     *
264     * @param url         The URL of the file (may be any valid URL).
265     * @param name        The name field for the attachment.
266     * @param description A description for the attachment.
267     * @return A MultiPartEmail.
268     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
269     * @since 1.0
270     */
271    public MultiPartEmail attach(final URL url, final String name, final String description) throws EmailException {
272        return attach(url, name, description, EmailAttachment.ATTACHMENT);
273    }
274
275    /**
276     * Attaches a file located by its URL.
277     *
278     * @param url         The URL of the file (may be any valid URL).
279     * @param name        The name field for the attachment.
280     * @param description A description for the attachment.
281     * @param disposition Either mixed or inline.
282     * @return A MultiPartEmail.
283     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
284     * @since 1.0
285     */
286    public MultiPartEmail attach(final URL url, final String name, final String description, final String disposition) throws EmailException {
287        // verify that the URL is valid
288        try {
289            url.openStream().close();
290        } catch (final IOException e) {
291            throw new EmailException("Invalid URL set:" + url, e);
292        }
293        return attach(new URLDataSource(url), name, description, disposition);
294    }
295
296    /**
297     * 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
298     * MimeMessage without commons-email.
299     *
300     * @throws EmailException if there was an error.
301     * @since 1.0
302     */
303    @Override
304    public void buildMimeMessage() throws EmailException {
305        try {
306            if (primaryBodyPart != null) {
307                // before a multipart message can be sent, we must make sure that
308                // the content for the main body part was actually set. If not,
309                // an IOException will be thrown during super.send().
310
311                final BodyPart body = getPrimaryBodyPart();
312                try {
313                    body.getContent();
314                } catch (final IOException e) { // NOPMD
315                    // do nothing here.
316                    // content will be set to an empty string as a result.
317                    // (Should this really be rethrown as an email exception?)
318                    // throw new EmailException(e);
319                }
320            }
321
322            if (subType != null) {
323                getContainer().setSubType(subType);
324            }
325
326            super.buildMimeMessage();
327        } catch (final MessagingException e) {
328            throw new EmailException(e);
329        }
330    }
331
332    /**
333     * Creates a body part object. Can be overridden if you don't want to create a BodyPart.
334     *
335     * @return the created body part
336     */
337    protected BodyPart createBodyPart() {
338        return new MimeBodyPart();
339    }
340
341    /**
342     * Creates a mime multipart object.
343     *
344     * @return the created mime part
345     */
346    protected MimeMultipart createMimeMultipart() {
347        return new MimeMultipart();
348    }
349
350    /**
351     * Gets the message container.
352     *
353     * @return The message container.
354     * @since 1.0
355     */
356    protected MimeMultipart getContainer() {
357        if (!initialized) {
358            init();
359        }
360        return container;
361    }
362
363    /**
364     * Gets first body part of the message.
365     *
366     * @return The primary body part.
367     * @throws MessagingException An error occurred while getting the primary body part.
368     * @since 1.0
369     */
370    protected BodyPart getPrimaryBodyPart() throws MessagingException {
371        if (!initialized) {
372            init();
373        }
374        // Add the first body part to the message. The fist body part must be
375        if (primaryBodyPart == null) {
376            primaryBodyPart = createBodyPart();
377            getContainer().addBodyPart(primaryBodyPart, 0);
378        }
379        return primaryBodyPart;
380    }
381
382    /**
383     * Gets the MIME subtype of the email.
384     *
385     * @return MIME subtype of the email
386     * @since 1.0
387     */
388    public String getSubType() {
389        return subType;
390    }
391
392    /**
393     * Initialize the multipart email.
394     *
395     * @since 1.0
396     */
397    protected void init() {
398        if (initialized) {
399            throw new IllegalStateException("Already initialized");
400        }
401        container = createMimeMultipart();
402        super.setContent(container);
403        initialized = true;
404    }
405
406    /**
407     * Tests whether there are attachments.
408     *
409     * @return true if there are attachments
410     * @since 1.0
411     */
412    public boolean isBoolHasAttachments() {
413        return hasAttachments;
414    }
415
416    /**
417     * Tests if this object is initialized.
418     *
419     * @return true if initialized
420     */
421    protected boolean isInitialized() {
422        return initialized;
423    }
424
425    /**
426     * Sets whether there are attachments.
427     *
428     * @param hasAttachments the attachments flag
429     * @since 1.0
430     */
431    public void setBoolHasAttachments(final boolean hasAttachments) {
432        this.hasAttachments = hasAttachments;
433    }
434
435    /**
436     * Sets the initialized status of this object.
437     *
438     * @param initialized the initialized status flag
439     */
440    protected void setInitialized(final boolean initialized) {
441        this.initialized = initialized;
442    }
443
444    /**
445     * Sets the message of the email.
446     *
447     * @param msg A String.
448     * @return An Email.
449     * @throws EmailException see javax.mail.internet.MimeBodyPart for definitions
450     * @since 1.0
451     */
452    @Override
453    public Email setMsg(final String msg) throws EmailException {
454        EmailException.checkNonEmpty(msg, () -> "Invalid message.");
455        try {
456            final BodyPart primary = getPrimaryBodyPart();
457            if (primary instanceof MimePart && EmailUtils.isNotEmpty(getCharsetName())) {
458                ((MimePart) primary).setText(msg, getCharsetName());
459            } else {
460                primary.setText(msg);
461            }
462        } catch (final MessagingException e) {
463            throw new EmailException(e);
464        }
465        return this;
466    }
467
468    /**
469     * Sets the MIME subtype of the email.
470     *
471     * @param subType MIME subtype of the email
472     * @since 1.0
473     */
474    public void setSubType(final String subType) {
475        this.subType = subType;
476    }
477
478}