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