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.mail2.jakarta;
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.URL;
24 import java.nio.file.Files;
25 import java.nio.file.OpenOption;
26 import java.nio.file.Path;
27 import java.util.Objects;
28
29 import org.apache.commons.mail2.core.EmailException;
30 import org.apache.commons.mail2.core.EmailUtils;
31 import org.apache.commons.mail2.jakarta.activation.PathDataSource;
32
33 import jakarta.activation.DataHandler;
34 import jakarta.activation.DataSource;
35 import jakarta.activation.FileDataSource;
36 import jakarta.activation.FileTypeMap;
37 import jakarta.activation.URLDataSource;
38 import jakarta.mail.BodyPart;
39 import jakarta.mail.MessagingException;
40 import jakarta.mail.internet.MimeBodyPart;
41 import jakarta.mail.internet.MimeMultipart;
42 import jakarta.mail.internet.MimePart;
43 import jakarta.mail.internet.MimeUtility;
44
45 /**
46 * A multipart email.
47 * <p>
48 * This class is used to send multi-part internet email like messages with attachments.
49 * </p>
50 * <p>
51 * 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.
52 * </p>
53 *
54 * @since 1.0
55 */
56 public class MultiPartEmail extends Email {
57
58 /** Body portion of the email. */
59 private MimeMultipart container;
60
61 /** The message container. */
62 private BodyPart primaryBodyPart;
63
64 /** The MIME subtype. */
65 private String subType;
66
67 /** Indicates if the message has been initialized. */
68 private boolean initialized;
69
70 /** Indicates if attachments have been added to the message. */
71 private boolean hasAttachments;
72
73 /**
74 * Constructs a new instance.
75 */
76 public MultiPartEmail() {
77 // empty
78 }
79
80 /**
81 * Adds a new part to the email.
82 *
83 * @param multipart The MimeMultipart.
84 * @return An Email.
85 * @throws EmailException see jakarta.mail.internet.MimeBodyPart for definitions
86 * @since 1.0
87 */
88 public Email addPart(final MimeMultipart multipart) throws EmailException {
89 try {
90 return addPart(multipart, getContainer().getCount());
91 } catch (final MessagingException e) {
92 throw new EmailException(e);
93 }
94 }
95
96 /**
97 * Adds a new part to the email.
98 *
99 * @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 jakarta.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 jakarta.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 jakarta.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 jakarta.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 jakarta.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 jakarta.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 jakarta.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 jakarta.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 jakarta.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 }