• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

evolvedbinary / elemental / 1020

01 May 2025 02:30AM UTC coverage: 56.421% (+0.01%) from 56.409%
1020

push

circleci

adamretter
[feature] Improve README.md badges

28459 of 55847 branches covered (50.96%)

Branch coverage included in aggregate %.

77483 of 131924 relevant lines covered (58.73%)

0.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.92
/extensions/modules/mail/src/main/java/org/exist/xquery/modules/mail/SendEmailFunction.java
1
/*
2
 * Elemental
3
 * Copyright (C) 2024, Evolved Binary Ltd
4
 *
5
 * admin@evolvedbinary.com
6
 * https://www.evolvedbinary.com | https://www.elemental.xyz
7
 *
8
 * Use of this software is governed by the Business Source License 1.1
9
 * included in the LICENSE file and at www.mariadb.com/bsl11.
10
 *
11
 * Change Date: 2028-04-27
12
 *
13
 * On the date above, in accordance with the Business Source License, use
14
 * of this software will be governed by the Apache License, Version 2.0.
15
 *
16
 * Additional Use Grant: Production use of the Licensed Work for a permitted
17
 * purpose. A Permitted Purpose is any purpose other than a Competing Use.
18
 * A Competing Use means making the Software available to others in a commercial
19
 * product or service that: substitutes for the Software; substitutes for any
20
 * other product or service we offer using the Software that exists as of the
21
 * date we make the Software available; or offers the same or substantially
22
 * similar functionality as the Software.
23
 *
24
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
25
 *       The original license header is included below.
26
 *
27
 * =====================================================================
28
 *
29
 * eXist-db Open Source Native XML Database
30
 * Copyright (C) 2001 The eXist-db Authors
31
 *
32
 * info@exist-db.org
33
 * http://www.exist-db.org
34
 *
35
 * This library is free software; you can redistribute it and/or
36
 * modify it under the terms of the GNU Lesser General Public
37
 * License as published by the Free Software Foundation; either
38
 * version 2.1 of the License, or (at your option) any later version.
39
 *
40
 * This library is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43
 * Lesser General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Lesser General Public
46
 * License along with this library; if not, write to the Free Software
47
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
48
 */
49
package org.exist.xquery.modules.mail;
50

51
import org.apache.commons.codec.binary.Base64;
52
import org.apache.logging.log4j.LogManager;
53
import org.apache.logging.log4j.Logger;
54
import org.exist.Version;
55
import org.exist.dom.QName;
56
import org.exist.util.MimeTable;
57
import org.exist.xquery.*;
58
import org.exist.xquery.value.*;
59
import org.w3c.dom.Element;
60
import org.w3c.dom.Node;
61

62
import jakarta.activation.DataHandler;
63
import jakarta.mail.*;
64
import jakarta.mail.internet.*;
65
import jakarta.mail.util.ByteArrayDataSource;
66

67
import javax.annotation.Nullable;
68
import javax.xml.transform.Transformer;
69
import javax.xml.transform.TransformerException;
70
import javax.xml.transform.TransformerFactory;
71
import javax.xml.transform.dom.DOMSource;
72
import javax.xml.transform.stream.StreamResult;
73
import java.io.*;
74
import java.net.InetAddress;
75
import java.net.Socket;
76
import java.util.*;
77
import java.util.regex.Pattern;
78

79
import static org.exist.util.StringUtil.isNullOrEmpty;
80

81
/**
82
 * Mail Module Extension SendEmailFunction.
83
 *
84
 * The email sending functionality of the Mail Module Extension that
85
 * allows email to be sent from XQuery using either SMTP or Sendmail.
86
 *
87
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
88
 * @author <a href="mailto:robert.walpole@devon.gov.uk">Robert Walpole</a>
89
 * @author <a href="mailto:andrzej@chaeron.com">Andrzej Taramina</a>
90
 * @author <a href="mailto:josemariafg@gmail.com">José María Fernández</a>
91
 */
92
public class SendEmailFunction extends BasicFunction {
93

94
    private static final Logger LOGGER = LogManager.getLogger(SendEmailFunction.class);
1✔
95
    private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
1✔
96

97
    private final static int MIME_BASE64_MAX_LINE_LENGTH = 76; //RFC 2045, page 24
98

99
    /**
100
     * Regular expression for checking for an RFC 2045 non-token.
101
     */
102
    private static final Pattern NON_TOKEN_PATTERN = Pattern.compile("^.*[\\s\\p{Cntrl}()<>@,;:\\\"/\\[\\]?=].*$");
1✔
103

104
    static final String ERROR_MSG_NON_MIME_CLIENT = "Error your mail client is not MIME Compatible";
105

106
    private static final Random RANDOM = new Random();
1✔
107

108
    public final static FunctionSignature deprecated = new FunctionSignature(
1✔
109
            new QName("send-email", MailModule.NAMESPACE_URI, MailModule.PREFIX),
110
            "Sends an email through the SMTP Server.",
111
            new SequenceType[]
112
                    {
113
                            new FunctionParameterSequenceType("email", Type.ELEMENT, Cardinality.ONE_OR_MORE, "The email message in the following format: <mail> <from/> <reply-to/> <to/> <cc/> <bcc/> <subject/> <message> <text/> <xhtml/> </message> <attachment filename=\"\" mimetype=\"\">xs:base64Binary</attachment> </mail>."),
114
                            new FunctionParameterSequenceType("server", Type.STRING, Cardinality.ZERO_OR_ONE, "The SMTP server.  If empty, then it tries to use the local sendmail program."),
115
                            new FunctionParameterSequenceType("charset", Type.STRING, Cardinality.ZERO_OR_ONE, "The charset value used in the \"Content-Type\" message header (Defaults to UTF-8)")
116
                    },
117
            new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.ONE_OR_MORE, "true if the email message was successfully sent")
118
    );
119

120
    public final static FunctionSignature[] signatures = {
1✔
121
            new FunctionSignature(
122
                    new QName("send-email", MailModule.NAMESPACE_URI, MailModule.PREFIX),
123
                    "Sends an email using javax.mail messaging libraries.",
124
                    new SequenceType[]
125
                            {
126
                                    new FunctionParameterSequenceType("mail-handle", Type.LONG, Cardinality.EXACTLY_ONE, "The JavaMail session handle retrieved from mail:get-mail-session()"),
127
                                    new FunctionParameterSequenceType("email", Type.ELEMENT, Cardinality.ONE_OR_MORE, "The email message in the following format: <mail> <from/> <reply-to/> <to/> <cc/> <bcc/> <subject/> <message> <text/> <xhtml/> </message> <attachment filename=\"\" mimetype=\"\">xs:base64Binary</attachment> </mail>.")
128
                            },
129
                    new SequenceType(Type.EMPTY_SEQUENCE, Cardinality.EMPTY_SEQUENCE)
130
            )
131
    };
132

133
    public SendEmailFunction(final XQueryContext context, final FunctionSignature signature) {
134
        super(context, signature);
×
135
    }
×
136

137
    @Override
138
    public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
139
        if (args.length == 3) {
×
140
            return deprecatedSendEmail(args, contextSequence);
×
141
        } else {
142
            return sendEmail(args, contextSequence);
×
143
        }
144
    }
145

146
    public Sequence sendEmail(final Sequence[] args, final Sequence contextSequence) throws XPathException {
147
        // was a session handle specified?
148
        if (args[0].isEmpty()) {
×
149
            throw new XPathException(this, "Session handle not specified");
×
150
        }
151

152
        // get the Session
153
        final long sessionHandle = ((IntegerValue) args[0].itemAt(0)).getLong();
×
154
        final Session session = MailModule.retrieveSession(context, sessionHandle);
×
155
        if (session == null) {
×
156
            throw new XPathException(this, "Invalid Session handle specified");
×
157
        }
158

159
        try {
160
            final Message[] messages = parseInputEmails(session, args[1]);
×
161
            String proto = session.getProperty("mail.transport.protocol");
×
162
            if (proto == null) {
×
163
                proto = "smtp";
×
164
            }
165
            try (final Transport t = session.getTransport(proto)) {
×
166
                if (session.getProperty("mail." + proto + ".auth") != null) {
×
167
                    t.connect(session.getProperty("mail." + proto + ".user"), session.getProperty("mail." + proto + ".password"));
×
168
                } else {
169
                    t.connect();
×
170
                }
171
                for (final Message msg : messages) {
×
172
                    t.sendMessage(msg, msg.getAllRecipients());
×
173
                }
174
            }
175

176
            return Sequence.EMPTY_SEQUENCE;
×
177
        } catch (final TransformerException e) {
×
178
            throw new XPathException(this, "Could not Transform XHTML Message Body: " + e.getMessage(), e);
×
179
        } catch (final MessagingException e) {
×
180
            throw new XPathException(this, "Could not send message(s): " + e.getMessage(), e);
×
181
        } catch (final IOException e) {
×
182
            throw new XPathException(this, "Attachment in some message could not be prepared: " + e.getMessage(), e);
×
183
        } catch (final Throwable t) {
×
184
            throw new XPathException(this, "Unexpected error from JavaMail layer (Is your message well structured?): " + t.getMessage(), t);
×
185
        }
186
    }
187

188
    public Sequence deprecatedSendEmail(final Sequence[] args, final Sequence contextSequence) throws XPathException {
189
        try {
190
            //get the charset parameter, default to UTF-8
191
            final String charset;
192
            if (!args[2].isEmpty()) {
×
193
                charset = args[2].getStringValue();
×
194
            } else {
195
                charset = "UTF-8";
×
196
            }
197

198
            // Parse the XML <mail> Elements into mail Objects
199
            final int len = args[0].getItemCount();
×
200
            final Element[] mailElements = new Element[len];
×
201
            for (int i = 0; i < len; i++) {
×
202
                mailElements[i] = (Element) args[0].itemAt(i);
×
203
            }
204
            final Mail[] mails = parseMailElement(mailElements);
×
205

206
            final ValueSequence results = new ValueSequence();
×
207

208
            //Send email with Sendmail or SMTP?
209
            if (!args[1].isEmpty()) {
×
210
                //SMTP
211
                final boolean[] mailResults = sendBySMTP(mails, args[1].getStringValue(), charset);
×
212

213
                for (final boolean mailResult : mailResults) {
×
214
                    results.add(BooleanValue.valueOf(mailResult));
×
215
                }
216
            } else {
×
217
                for (final Mail mail : mails) {
×
218
                    final boolean result = sendBySendmail(mail, charset);
×
219
                    results.add(BooleanValue.valueOf(result));
×
220
                }
221
            }
222

223
            return results;
×
224
        } catch (final TransformerException | IOException e) {
×
225
            throw new XPathException(this, "Could not Transform XHTML Message Body: " + e.getMessage(), e);
×
226
        } catch (final SMTPException e) {
×
227
            throw new XPathException(this, "Could not send message(s)" + e.getMessage(), e);
×
228
        }
229
    }
230

231
    private Message[] parseInputEmails(final Session session, final Sequence arg) throws IOException, MessagingException, TransformerException {
232
        // Parse the XML <mail> Elements into mail Objects
233
        final int len = arg.getItemCount();
×
234
        final Element[] mailElements = new Element[len];
×
235
        for (int i = 0; i < len; i++) {
×
236
            mailElements[i] = (Element) arg.itemAt(i);
×
237
        }
238
        return parseMessageElement(session, mailElements);
×
239
    }
240

241
    /**
242
     * Sends an email using the Operating Systems sendmail application
243
     *
244
     * @param mail representation of the email to send
245
     * @param charset the character set
246
     * @return boolean value of true of false indicating success or failure to send email
247
     */
248
    private boolean sendBySendmail(final Mail mail, final String charset) {
249

250
        //Create a list of all Recipients, should include to, cc and bcc recipient
251
        final List<String> allrecipients = new ArrayList<>();
×
252
        allrecipients.addAll(mail.getTo());
×
253
        allrecipients.addAll(mail.getCC());
×
254
        allrecipients.addAll(mail.getBCC());
×
255

256
        //Get a string of all recipients email addresses
257
        final StringBuilder recipients = new StringBuilder();
×
258

259
        for (final String recipient : allrecipients) {
×
260
            recipients.append(" ");
×
261

262
            //Check format of to address does it include a name as well as the email address?
263
            if (recipient.contains("<")) {
×
264
                //yes, just add the email address
265
                recipients.append(recipient, recipient.indexOf("<") + 1, recipient.indexOf(">"));
×
266
            } else {
267
                //add the email address
268
                recipients.append(recipient);
×
269
            }
270
        }
×
271

272
        try {
273
            //Create a sendmail Process
274
            final Process p = Runtime.getRuntime().exec("/usr/sbin/sendmail" + recipients);
×
275

276
            //Get a Buffered Print Writer to the Processes stdOut
277
            try (final PrintWriter out = new PrintWriter(new OutputStreamWriter(p.getOutputStream(), charset))) {
×
278
                //Send the Message
279
                writeMessage(out, mail, false, charset);
×
280
            }
281
        } catch (final IOException e) {
×
282
            LOGGER.error(e.getMessage(), e);
×
283
            return false;
×
284
        }
×
285

286
        // Message Sent Succesfully
287
        if (LOGGER.isDebugEnabled()) {
×
288
            LOGGER.debug("send-email() message sent using Sendmail {}", new Date());
×
289
        }
290

291
        return true;
×
292
    }
293

294
    private static class SMTPException extends Exception {
295
        private static final long serialVersionUID = 4859093648476395159L;
296

297
        public SMTPException(final String message) {
298
            super(message);
×
299
        }
×
300

301
        public SMTPException(final Throwable cause) {
302
            super(cause);
×
303
        }
×
304
    }
305

306
    /**
307
     * Sends an email using SMTP
308
     *
309
     * @param mails         A list of mail object representing the email to send
310
     * @param smtpServerArg The SMTP Server to send the email through
311
     * @param charset the character set
312
     * @return boolean value of true of false indicating success or failure to send email
313
     * @throws SMTPException if an I/O error occurs
314
     */
315
    private boolean[] sendBySMTP(final Mail[] mails, final String smtpServerArg, final String charset) throws SMTPException {
316
        String smtpHost = "localhost";
×
317
        int smtpPort = 25;
×
318

319
        if (smtpServerArg != null && !smtpServerArg.isEmpty()) {
×
320
            final int idx = smtpServerArg.indexOf(':');
×
321
            if (idx > -1) {
×
322
                smtpHost = smtpServerArg.substring(0, idx);
×
323
                smtpPort = Integer.parseInt(smtpServerArg.substring(idx + 1));
×
324
            } else {
325
                smtpHost = smtpServerArg;
×
326
            }
327
        }
328

329
        String smtpResult;             //Holds the server Result code when an SMTP Command is executed
330

331
        final boolean[] sendMailResults = new boolean[mails.length];
×
332

333
        try (
334
                //Create a Socket and connect to the SMTP Server
335
                final Socket smtpSock = new Socket(smtpHost, smtpPort);
×
336

337
                //Create a Buffered Reader for the Socket
338
                final BufferedReader smtpIn = new BufferedReader(new InputStreamReader(smtpSock.getInputStream()));
×
339

340
                //Create an Output Writer for the Socket
341
                final PrintWriter smtpOut = new PrintWriter(new OutputStreamWriter(smtpSock.getOutputStream(), charset))) {
×
342

343
            //First line sent to us from the SMTP server should be "220 blah blah", 220 indicates okay
344
            smtpResult = smtpIn.readLine();
×
345
            if (!smtpResult.startsWith("220")) {
×
346
                final String errMsg = "Error - SMTP Server not ready: '" + smtpResult + "'";
×
347
                LOGGER.error(errMsg);
×
348
                throw new SMTPException(errMsg);
×
349
            }
350

351
            //Say "HELO"
352
            smtpOut.print("HELO " + InetAddress.getLocalHost().getHostName() + "\r\n");
×
353
            smtpOut.flush();
×
354

355
            //get "HELLO" response, should be "250 blah blah"
356
            smtpResult = smtpIn.readLine();
×
357
            if (smtpResult == null) {
×
358
                final String errMsg = "Error - Unexpected null response to SMTP HELO";
×
359
                LOGGER.error(errMsg);
×
360
                throw new SMTPException(errMsg);
×
361
            }
362

363
            if (!smtpResult.startsWith("250")) {
×
364
                final String errMsg = "Error - SMTP HELO Failed: '" + smtpResult + "'";
×
365
                LOGGER.error(errMsg);
×
366
                throw new SMTPException(errMsg);
×
367
            }
368

369
            //write SMTP message(s)
370
            for (int i = 0; i < mails.length; i++) {
×
371
                final boolean mailResult = writeSMTPMessage(mails[i], smtpOut, smtpIn, charset);
×
372
                sendMailResults[i] = mailResult;
×
373
            }
374

375
            //all done, time to "QUIT"
376
            smtpOut.print("QUIT\r\n");
×
377
            smtpOut.flush();
×
378

379
        } catch (final IOException ioe) {
×
380
            LOGGER.error(ioe.getMessage(), ioe);
×
381
            throw new SMTPException(ioe);
×
382
        }
×
383

384
        //Message(s) Sent Succesfully
385
        if (LOGGER.isDebugEnabled()) {
×
386
            LOGGER.debug("send-email() message(s) sent using SMTP {}", new Date());
×
387
        }
388

389
        return sendMailResults;
×
390
    }
391

392
    private boolean writeSMTPMessage(final Mail mail, final PrintWriter smtpOut, final BufferedReader smtpIn, final String charset) {
393
        try {
394
            String smtpResult;
395

396
            //Send "MAIL FROM:"
397
            //Check format of from address does it include a name as well as the email address?
398
            if (mail.getFrom().contains("<")) {
×
399
                //yes, just send the email address
400
                smtpOut.print("MAIL FROM:<" + mail.getFrom().substring(mail.getFrom().indexOf("<") + 1, mail.getFrom().indexOf(">")) + ">\r\n");
×
401
            } else {
402
                //no, doesnt include a name so send the email address
403
                smtpOut.print("MAIL FROM:<" + mail.getFrom() + ">\r\n");
×
404
            }
405
            smtpOut.flush();
×
406

407
            //Get "MAIL FROM:" response
408
            smtpResult = smtpIn.readLine();
×
409
            if (smtpResult == null) {
×
410
                LOGGER.error("Error - Unexpected null response to SMTP MAIL FROM");
×
411
                return false;
×
412
            }
413
            if (!smtpResult.startsWith("250")) {
×
414
                LOGGER.error("Error - SMTP MAIL FROM failed: {}", smtpResult);
×
415
                return false;
×
416
            }
417

418
            //RCPT TO should be issued for each to, cc and bcc recipient
419
            final List<String> allrecipients = new ArrayList<>();
×
420
            allrecipients.addAll(mail.getTo());
×
421
            allrecipients.addAll(mail.getCC());
×
422
            allrecipients.addAll(mail.getBCC());
×
423

424
            for (final String recipient : allrecipients) {
×
425
                //Send "RCPT TO:"
426
                //Check format of to address does it include a name as well as the email address?
427
                if (recipient.contains("<")) {
×
428
                    //yes, just send the email address
429
                    smtpOut.print("RCPT TO:<" + recipient.substring(recipient.indexOf("<") + 1, recipient.indexOf(">")) + ">\r\n");
×
430
                } else {
431
                    smtpOut.print("RCPT TO:<" + recipient + ">\r\n");
×
432
                }
433
                smtpOut.flush();
×
434
                //Get "RCPT TO:" response
435
                smtpResult = smtpIn.readLine();
×
436
                if (!smtpResult.startsWith("250")) {
×
437
                    LOGGER.error("Error - SMTP RCPT TO failed: {}", smtpResult);
×
438
                }
439
            }
×
440

441
            //SEND "DATA"
442
            smtpOut.print("DATA\r\n");
×
443
            smtpOut.flush();
×
444

445
            //Get "DATA" response, should be "354 blah blah" (optionally preceded by "250 OK")
446
            smtpResult = smtpIn.readLine();
×
447
            if (smtpResult.startsWith("250")) {
×
448
                // should then be followed by "354 blah blah"
449
                smtpResult = smtpIn.readLine();
×
450
            }
451

452
            if (!smtpResult.startsWith("354")) {
×
453
                LOGGER.error("Error - SMTP DATA failed: {}", smtpResult);
×
454
                return false;
×
455
            }
456

457
            //Send the Message
458
            writeMessage(smtpOut, mail, true, charset);
×
459

460
            //Get end message response, should be "250 blah blah"
461
            smtpResult = smtpIn.readLine();
×
462
            if (!smtpResult.startsWith("250")) {
×
463
                LOGGER.error("Error - Message not accepted: {}", smtpResult);
×
464
                return false;
×
465
            }
466
        } catch (final IOException e) {
×
467
            LOGGER.error(e.getMessage(), e);
×
468
            return false;
×
469
        }
×
470

471
        return true;
×
472
    }
473

474
    /**
475
     * Writes an email payload (Headers + Body) from a mail object.
476
     *
477
     * Access is package-private for unit testing purposes.
478
     *
479
     * @param out   A PrintWriter to receive the email
480
     * @param aMail A mail object representing the email to write out
481
     * @param useCrLf true to use CRLF for line ending, false to use LF
482
     * @param charset the character set
483
     * @throws IOException if an I/O error occurs
484
     */
485
    static void writeMessage(final PrintWriter out, final Mail aMail, final boolean useCrLf, final String charset) throws IOException {
486
        final String eol = useCrLf ? "\r\n" : "\n";
1!
487

488
        //write the message headers
489

490
        out.print("From: " + encode64Address(aMail.getFrom(), charset) + eol);
1✔
491

492
        if (aMail.getReplyTo() != null) {
1!
493
            out.print("Reply-To: " + encode64Address(aMail.getReplyTo(), charset) + eol);
×
494
        }
495

496
        for (int x = 0; x < aMail.countTo(); x++) {
1✔
497
            out.print("To: " + encode64Address(aMail.getTo(x), charset) + eol);
1✔
498
        }
499

500
        for (int x = 0; x < aMail.countCC(); x++) {
1!
501
            out.print("CC: " + encode64Address(aMail.getCC(x), charset) + eol);
×
502
        }
503

504
        for (int x = 0; x < aMail.countBCC(); x++) {
1!
505
            out.print("BCC: " + encode64Address(aMail.getBCC(x), charset) + eol);
×
506
        }
507

508
        out.print("Date: " + getDateRFC822() + eol);
1✔
509
        String subject = aMail.getSubject();
1✔
510
        if (subject == null) {
1!
511
            subject = "";
×
512
        }
513
        out.print("Subject: " + encode64(subject, charset) + eol);
1✔
514
        out.print("X-Mailer: Elemental " + Version.getVersion() + " mail:send-email()" + eol);
1✔
515
        out.print("MIME-Version: 1.0" + eol);
1✔
516

517

518
        boolean multipartAlternative = false;
1✔
519
        int multipartInstanceCount = 0;
1✔
520
        final Deque<String> multipartBoundary = new ArrayDeque<>();
1✔
521

522
        if (aMail.attachmentIterator().hasNext()) {
1✔
523
            // we have an attachment as well as text and/or html, so we need a multipart/mixed message
524
            multipartBoundary.addFirst(multipartBoundary(++multipartInstanceCount));
1✔
525
        } else if (nonEmpty(aMail.getText()) && nonEmpty(aMail.getXHTML())) {
1✔
526
            // we have text and html, so we need a multipart/alternative message and no attachment
527
            multipartAlternative = true;
1✔
528
            multipartBoundary.addFirst(multipartBoundary(++multipartInstanceCount));
1✔
529
        }
530
//        else {
531
//            // we have either text or html and no attachment this message is not multipart
532
//        }
533

534
        //content type
535
        if (!multipartBoundary.isEmpty()) {
1✔
536
            //multipart message
537

538
            out.print("Content-Type: " + (multipartAlternative ? "multipart/alternative" : "multipart/mixed") + "; boundary=" + parameterValue(multipartBoundary.peekFirst()) + eol);
1✔
539

540
            //Mime warning
541
            out.print(eol);
1✔
542
            out.print(ERROR_MSG_NON_MIME_CLIENT + eol);
1✔
543
            out.print(eol);
1✔
544

545
            out.print("--" + multipartBoundary.peekFirst() + eol);
1✔
546
        }
547

548
        if (nonEmpty(aMail.getText()) && nonEmpty(aMail.getXHTML()) && aMail.attachmentIterator().hasNext()) {
1✔
549
            // we are a multipart inside a multipart
550
            multipartBoundary.addFirst(multipartBoundary(++multipartInstanceCount));
1✔
551

552
            out.print("Content-Type: multipart/alternative; boundary=" + parameterValue(multipartBoundary.peekFirst()) + eol);
1✔
553
            out.print(eol);
1✔
554
            out.print("--" + multipartBoundary.peekFirst() + eol);
1✔
555
        }
556

557
        //text email
558
        if (nonEmpty(aMail.getText())) {
1✔
559
            out.print("Content-Type: text/plain; charset=" + charset + eol);
1✔
560
            out.print("Content-Transfer-Encoding: 8bit" + eol);
1✔
561

562
            //now send the txt message
563
            out.print(eol);
1✔
564
            out.print(aMail.getText() + eol);
1✔
565

566
            if (!multipartBoundary.isEmpty()) {
1✔
567
                if (nonEmpty(aMail.getXHTML()) || aMail.attachmentIterator().hasNext()) {
1!
568
                    out.print("--" + multipartBoundary.peekFirst() + eol);
1✔
569
                } else {
570
                    // End multipart message
571
                    out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
×
572
                    multipartBoundary.removeFirst();
×
573
                }
574
            }
575
        }
576

577
        //HTML email
578
        if (nonEmpty(aMail.getXHTML())) {
1✔
579
            out.print("Content-Type: text/html; charset=" + charset + eol);
1✔
580
            out.print("Content-Transfer-Encoding: 8bit" + eol);
1✔
581

582
            //now send the html message
583
            out.print(eol);
1✔
584
            out.print("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" + eol);
1✔
585
            out.print(aMail.getXHTML() + eol);
1✔
586

587
            if (!multipartBoundary.isEmpty()) {
1✔
588
                if (aMail.attachmentIterator().hasNext()) {
1✔
589
                    if (nonEmpty(aMail.getText()) && nonEmpty(aMail.getXHTML()) && aMail.attachmentIterator().hasNext()) {
1!
590
                        // End multipart message
591
                        out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
1✔
592
                        multipartBoundary.removeFirst();
1✔
593
                    }
594

595
                    out.print("--" + multipartBoundary.peekFirst() + eol);
1✔
596

597
                } else {
598
                    // End multipart message
599
                    out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
1✔
600
                    multipartBoundary.removeFirst();
1✔
601
                }
602
            }
603
        }
604

605
        //attachments
606
        if (aMail.attachmentIterator().hasNext()) {
1✔
607
            for (final Iterator<MailAttachment> itAttachment = aMail.attachmentIterator(); itAttachment.hasNext(); ) {
1✔
608
                final MailAttachment ma = itAttachment.next();
1✔
609

610
                out.print("Content-Type: " + ma.mimeType() + "; name=" + parameterValue(ma.filename()) + eol);
1✔
611
                out.print("Content-Transfer-Encoding: base64" + eol);
1✔
612
                out.print("Content-Description: " + ma.filename() + eol);
1✔
613
                out.print("Content-Disposition: attachment; filename=" + parameterValue(ma.filename()) + eol);
1✔
614
                out.print(eol);
1✔
615

616

617
                //write out the attachment encoded data in fixed width lines
618
                final char[] buf = new char[MIME_BASE64_MAX_LINE_LENGTH];
1✔
619
                int read = -1;
1✔
620
                final Reader attachmentDataReader = new StringReader(ma.data());
1✔
621
                while ((read = attachmentDataReader.read(buf, 0, MIME_BASE64_MAX_LINE_LENGTH)) > -1) {
1✔
622
                    out.print(String.valueOf(buf, 0, read) + eol);
1✔
623
                }
624

625
                if (itAttachment.hasNext()) {
1✔
626
                    out.print("--" + multipartBoundary.peekFirst() + eol);
1✔
627
                }
628
            }
1✔
629

630
            // End multipart message
631
            out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
1✔
632
            multipartBoundary.removeFirst();
1✔
633
        }
634

635
        //end the message, <cr><lf>.<cr><lf>
636
        out.print(eol);
1✔
637
        out.print("." + eol);
1✔
638
        out.flush();
1✔
639
    }
1✔
640

641
    /**
642
     * Constructs a mail Object from an XML representation of an email
643
     * <p>
644
     * The XML email Representation is expected to look something like this
645
     *
646
     * <mail>
647
     * <from></from>
648
     * <reply-to></reply-to>
649
     * <to></to>
650
     * <cc></cc>
651
     * <bcc></bcc>
652
     * <subject></subject>
653
     * <message>
654
     * <text></text>
655
     * <xhtml></xhtml>
656
     * </message>
657
     * </mail>
658
     *
659
     * @param mailElements The XML mail Node
660
     * @throws TransformerException if a transformation error occurs
661
     * @return A mail Object representing the XML mail Node
662
     */
663
    private Mail[] parseMailElement(final Element[] mailElements) throws TransformerException, IOException {
664
        Mail[] mails = new Mail[mailElements.length];
×
665

666
        int i = 0;
×
667
        for (final Element mailElement : mailElements) {
×
668

669
            //Make sure that message has a Mail node
670
            if ("mail".equals(mailElement.getLocalName())) {
×
671
                final Mail mail = new Mail();
×
672

673
                //Get the First Child
674
                Node child = mailElement.getFirstChild();
×
675
                while (child != null) {
×
676
                    //Parse each of the child nodes
677
                    if (Node.ELEMENT_NODE == child.getNodeType() && child.hasChildNodes()) {
×
678
                        switch (child.getLocalName()) {
×
679
                            case "from":
680
                                mail.setFrom(child.getFirstChild().getNodeValue());
×
681
                                break;
×
682
                            case "reply-to":
683
                                mail.setReplyTo(child.getFirstChild().getNodeValue());
×
684
                                break;
×
685
                            case "to":
686
                                mail.addTo(child.getFirstChild().getNodeValue());
×
687
                                break;
×
688
                            case "cc":
689
                                mail.addCC(child.getFirstChild().getNodeValue());
×
690
                                break;
×
691
                            case "bcc":
692
                                mail.addBCC(child.getFirstChild().getNodeValue());
×
693
                                break;
×
694
                            case "subject":
695
                                mail.setSubject(child.getFirstChild().getNodeValue());
×
696
                                break;
×
697
                            case "message":
698
                                //If the message node, then parse the child text and xhtml nodes
699
                                Node bodyPart = child.getFirstChild();
×
700
                                while (bodyPart != null) {
×
701
                                    if ("text".equals(bodyPart.getLocalName())) {
×
702
                                        mail.setText(bodyPart.getFirstChild().getNodeValue());
×
703
                                    } else if ("xhtml".equals(bodyPart.getLocalName())) {
×
704
                                        //Convert everything inside <xhtml></xhtml> to text
705
                                        final Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
×
706
                                        final DOMSource source = new DOMSource(bodyPart.getFirstChild());
×
707
                                        try (final StringWriter strWriter = new StringWriter()) {
×
708
                                            final StreamResult result = new StreamResult(strWriter);
×
709
                                            transformer.transform(source, result);
×
710
                                            mail.setXHTML(strWriter.toString());
×
711
                                        }
712
                                    }
713

714
                                    //next body part
715
                                    bodyPart = bodyPart.getNextSibling();
×
716
                                }
717
                                break;
718
                            case "attachment":
719
                                final Element attachment = (Element) child;
×
720
                                final MailAttachment ma = new MailAttachment(attachment.getAttribute("filename"), attachment.getAttribute("mimetype"), attachment.getFirstChild().getNodeValue());
×
721
                                mail.addAttachment(ma);
×
722
                                break;
723
                        }
724
                    }
725

726
                    //next node
727
                    child = child.getNextSibling();
×
728

729
                }
730
                mails[i++] = mail;
×
731
            }
732
        }
733

734
        if (i != mailElements.length) {
×
735
            mails = Arrays.copyOf(mails, i);
×
736
        }
737

738
        return mails;
×
739
    }
740

741
    /**
742
     * Constructs a mail Object from an XML representation of an email
743
     * <p>
744
     * The XML email Representation is expected to look something like this
745
     *
746
     * <mail>
747
     * <from></from>
748
     * <reply-to></reply-to>
749
     * <to></to>
750
     * <cc></cc>
751
     * <bcc></bcc>
752
     * <subject></subject>
753
     * <message>
754
     * <text charset="" encoding=""></text>
755
     * <xhtml charset="" encoding=""></xhtml>
756
     * <generic charset="" type="" encoding=""></generic>
757
     * </message>
758
     * <attachment mimetype="" filename=""></attachment>
759
     * </mail>
760
     *
761
     * @param mailElements The XML mail Node
762
     * @throws IOException          if an I/O error occurs
763
     * @throws MessagingException   if an email error occurs
764
     * @throws TransformerException if a transformation error occurs
765
     * @return A mail Object representing the XML mail Node
766
     */
767
    private Message[] parseMessageElement(final Session session, final Element[] mailElements) throws IOException, MessagingException, TransformerException {
768

769
        Message[] mails = new Message[mailElements.length];
×
770

771
        int i = 0;
×
772
        for (final Element mailElement : mailElements) {
×
773
            //Make sure that message has a Mail node
774
            if ("mail".equals(mailElement.getLocalName())) {
×
775
                //New message Object
776
                // create a message
777
                final MimeMessage msg = new MimeMessage(session);
×
778

779
                boolean fromWasSet = false;
×
780
                final List<InternetAddress> replyTo = new ArrayList<>();
×
781
                MimeBodyPart body = null;
×
782
                Multipart multibody = null;
×
783
                final List<MimeBodyPart> attachments = new ArrayList<>();
×
784
                String firstContent = null;
×
785
                String firstContentType = null;
×
786
                String firstCharset = null;
×
787
                String firstEncoding = null;
×
788

789
                //Get the First Child
790
                Node child = mailElement.getFirstChild();
×
791
                while (child != null) {
×
792
                    //Parse each of the child nodes
793
                    if (Node.ELEMENT_NODE == child.getNodeType() && child.hasChildNodes()) {
×
794
                        switch (child.getLocalName()) {
×
795
                            case "from":
796
                                // set the from and to address
797
                                final InternetAddress[] addressFrom = {
×
798
                                        new InternetAddress(child.getFirstChild().getNodeValue())
×
799
                                };
800
                                msg.addFrom(addressFrom);
×
801
                                fromWasSet = true;
×
802
                                break;
×
803
                            case "reply-to":
804
                                // As we can only set the reply-to, not add them, let's keep
805
                                // all of them in a list
806
                                replyTo.add(new InternetAddress(child.getFirstChild().getNodeValue()));
×
807
                                break;
×
808
                            case "to":
809
                                msg.addRecipient(Message.RecipientType.TO, new InternetAddress(child.getFirstChild().getNodeValue()));
×
810
                                break;
×
811
                            case "cc":
812
                                msg.addRecipient(Message.RecipientType.CC, new InternetAddress(child.getFirstChild().getNodeValue()));
×
813
                                break;
×
814
                            case "bcc":
815
                                msg.addRecipient(Message.RecipientType.BCC, new InternetAddress(child.getFirstChild().getNodeValue()));
×
816
                                break;
×
817
                            case "subject":
818
                                msg.setSubject(child.getFirstChild().getNodeValue());
×
819
                                break;
×
820
                            case "header":
821
                                // Optional : You can also set your custom headers in the Email if you Want
822
                                msg.addHeader(((Element) child).getAttribute("name"), child.getFirstChild().getNodeValue());
×
823
                                break;
×
824
                            case "message":
825
                                //If the message node, then parse the child text and xhtml nodes
826
                                Node bodyPart = child.getFirstChild();
×
827
                                while (bodyPart != null) {
×
828
                                    if (Node.ELEMENT_NODE != bodyPart.getNodeType()) {
×
829
                                        continue;
×
830
                                    }
831

832
                                    final Element elementBodyPart = (Element) bodyPart;
×
833
                                    String content = null;
×
834
                                    String contentType = null;
×
835

836
                                    switch (bodyPart.getLocalName()) {
×
837
                                        case "text":
838
                                            // Setting the Subject and Content Type
839
                                            content = bodyPart.getFirstChild().getNodeValue();
×
840
                                            contentType = "plain";
×
841
                                            break;
×
842
                                        case "xhtml":
843
                                            //Convert everything inside <xhtml></xhtml> to text
844
                                            final Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
×
845
                                            final DOMSource source = new DOMSource(bodyPart.getFirstChild());
×
846
                                            try (final StringWriter strWriter = new StringWriter()) {
×
847
                                                final StreamResult result = new StreamResult(strWriter);
×
848
                                                transformer.transform(source, result);
×
849
                                                content = strWriter.toString();
×
850
                                            }
851
                                            contentType = "html";
×
852
                                            break;
×
853
                                        case "generic":
854
                                            // Setting the Subject and Content Type
855
                                            content = elementBodyPart.getFirstChild().getNodeValue();
×
856
                                            contentType = elementBodyPart.getAttribute("type");
×
857
                                            break;
858
                                    }
859

860
                                    // Now, time to store it
861
                                    if (content != null && !contentType.isEmpty()) {
×
862
                                        String charset = elementBodyPart.getAttribute("charset");
×
863
                                        String encoding = elementBodyPart.getAttribute("encoding");
×
864

865
                                        if (body != null && multibody == null) {
×
866
                                            multibody = new MimeMultipart("alternative");
×
867
                                            multibody.addBodyPart(body);
×
868
                                        }
869

870
                                        if (isNullOrEmpty(charset)) {
×
871
                                            charset = "UTF-8";
×
872
                                        }
873

874
                                        if (isNullOrEmpty(encoding)) {
×
875
                                            encoding = "quoted-printable";
×
876
                                        }
877

878
                                        if (body == null) {
×
879
                                            firstContent = content;
×
880
                                            firstCharset = charset;
×
881
                                            firstContentType = contentType;
×
882
                                            firstEncoding = encoding;
×
883
                                        }
884
                                        body = new MimeBodyPart();
×
885
                                        body.setText(content, charset, contentType);
×
886
                                        if (encoding != null) {
×
887
                                            body.setHeader("Content-Transfer-Encoding", encoding);
×
888
                                        }
889
                                        if (multibody != null) {
×
890
                                            multibody.addBodyPart(body);
×
891
                                        }
892
                                    }
893

894
                                    //next body part
895
                                    bodyPart = bodyPart.getNextSibling();
×
896
                                }
×
897
                                break;
898
                            case "attachment":
899
                                final Element attachment = (Element) child;
×
900
                                final MimeBodyPart part;
901
                                // if mimetype indicates a binary resource, assume the content is base64 encoded
902
                                if (MimeTable.getInstance().isTextContent(attachment.getAttribute("mimetype"))) {
×
903
                                    part = new MimeBodyPart();
×
904
                                } else {
905
                                    part = new PreencodedMimeBodyPart("base64");
×
906
                                }
907
                                final StringBuilder content = new StringBuilder();
×
908
                                Node attachChild = attachment.getFirstChild();
×
909
                                while (attachChild != null) {
×
910
                                    if (Node.ELEMENT_NODE == attachChild.getNodeType()) {
×
911
                                        final Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
×
912
                                        final DOMSource source = new DOMSource(attachChild);
×
913
                                        try (final StringWriter strWriter = new StringWriter()) {
×
914
                                            final StreamResult result = new StreamResult(strWriter);
×
915
                                            transformer.transform(source, result);
×
916
                                            content.append(strWriter);
×
917
                                        }
918
                                    } else {
×
919
                                        content.append(attachChild.getNodeValue());
×
920
                                    }
921
                                    attachChild = attachChild.getNextSibling();
×
922
                                }
923
                                part.setDataHandler(new DataHandler(new ByteArrayDataSource(content.toString(), attachment.getAttribute("mimetype"))));
×
924
                                part.setFileName(attachment.getAttribute("filename"));
×
925
                                // part.setHeader("Content-Transfer-Encoding", "base64");
926
                                attachments.add(part);
×
927
                                break;
928
                        }
929
                    }
930

931
                    //next node
932
                    child = child.getNextSibling();
×
933
                }
934

935
                // Lost from
936
                if (!fromWasSet) {
×
937
                    msg.setFrom();
×
938
                }
939

940
                msg.setReplyTo(replyTo.toArray(new InternetAddress[0]));
×
941

942
                // Preparing content and attachments
943
                if (!attachments.isEmpty()) {
×
944
                    if (multibody == null) {
×
945
                        multibody = new MimeMultipart("mixed");
×
946
                        if (body != null) {
×
947
                            multibody.addBodyPart(body);
×
948
                        }
949
                    } else {
950
                        final MimeMultipart container = new MimeMultipart("mixed");
×
951
                        final MimeBodyPart containerBody = new MimeBodyPart();
×
952
                        containerBody.setContent(multibody);
×
953
                        container.addBodyPart(containerBody);
×
954
                        multibody = container;
×
955
                    }
956
                    for (final MimeBodyPart part : attachments) {
×
957
                        multibody.addBodyPart(part);
×
958
                    }
×
959
                }
960

961
                // And now setting-up content
962
                if (multibody != null) {
×
963
                    msg.setContent(multibody);
×
964
                } else if (body != null) {
×
965
                    msg.setText(firstContent, firstCharset, firstContentType);
×
966
                    if (firstEncoding != null) {
×
967
                        msg.setHeader("Content-Transfer-Encoding", firstEncoding);
×
968
                    }
969
                }
970

971
                msg.saveChanges();
×
972
                mails[i++] = msg;
×
973
            }
974
        }
975

976
        if (i != mailElements.length) {
×
977
            mails = Arrays.copyOf(mails, i);
×
978
        }
979

980
        return mails;
×
981
    }
982

983
    /**
984
     * Returns the current date and time in an RFC822 format, suitable for an email Date Header
985
     *
986
     * @return RFC822 formated date and time as a String
987
     */
988
    private static String getDateRFC822() {
989
        String dateString = "";
1✔
990
        final Calendar rightNow = Calendar.getInstance();
1✔
991

992
        //Day of the week
993
        dateString = switch (rightNow.get(Calendar.DAY_OF_WEEK)) {
1!
994
            case Calendar.MONDAY -> "Mon";
×
995
            case Calendar.TUESDAY -> "Tue";
×
996
            case Calendar.WEDNESDAY -> "Wed";
×
997
            case Calendar.THURSDAY -> "Thu";
1✔
998
            case Calendar.FRIDAY -> "Fri";
×
999
            case Calendar.SATURDAY -> "Sat";
×
1000
            case Calendar.SUNDAY -> "Sun";
×
1001
            default -> dateString;
1✔
1002
        };
1003

1004
        dateString += ", ";
1✔
1005

1006
        //Date
1007
        dateString += rightNow.get(Calendar.DAY_OF_MONTH);
1✔
1008
        dateString += " ";
1✔
1009

1010
        //Month
1011
        switch (rightNow.get(Calendar.MONTH)) {
1!
1012
            case Calendar.JANUARY:
1013
                dateString += "Jan";
×
1014
                break;
×
1015

1016
            case Calendar.FEBRUARY:
1017
                dateString += "Feb";
×
1018
                break;
×
1019

1020
            case Calendar.MARCH:
1021
                dateString += "Mar";
×
1022
                break;
×
1023

1024
            case Calendar.APRIL:
1025
                dateString += "Apr";
×
1026
                break;
×
1027

1028
            case Calendar.MAY:
1029
                dateString += "May";
1✔
1030
                break;
1✔
1031

1032
            case Calendar.JUNE:
1033
                dateString += "Jun";
×
1034
                break;
×
1035

1036
            case Calendar.JULY:
1037
                dateString += "Jul";
×
1038
                break;
×
1039

1040
            case Calendar.AUGUST:
1041
                dateString += "Aug";
×
1042
                break;
×
1043

1044
            case Calendar.SEPTEMBER:
1045
                dateString += "Sep";
×
1046
                break;
×
1047

1048
            case Calendar.OCTOBER:
1049
                dateString += "Oct";
×
1050
                break;
×
1051

1052
            case Calendar.NOVEMBER:
1053
                dateString += "Nov";
×
1054
                break;
×
1055

1056
            case Calendar.DECEMBER:
1057
                dateString += "Dec";
×
1058
                break;
1059
        }
1060
        dateString += " ";
1✔
1061

1062
        //Year
1063
        dateString += rightNow.get(Calendar.YEAR);
1✔
1064
        dateString += " ";
1✔
1065

1066
        //Time
1067
        String tHour = Integer.toString(rightNow.get(Calendar.HOUR_OF_DAY));
1✔
1068
        if (tHour.length() == 1) {
1!
1069
            tHour = "0" + tHour;
1✔
1070
        }
1071

1072
        String tMinute = Integer.toString(rightNow.get(Calendar.MINUTE));
1✔
1073
        if (tMinute.length() == 1) {
1!
1074
            tMinute = "0" + tMinute;
×
1075
        }
1076

1077
        String tSecond = Integer.toString(rightNow.get(Calendar.SECOND));
1✔
1078
        if (tSecond.length() == 1) {
1!
1079
            tSecond = "0" + tSecond;
×
1080
        }
1081

1082
        dateString += tHour + ":" + tMinute + ":" + tSecond + " ";
1✔
1083

1084
        //TimeZone Correction
1085
        final String tzSign;
1086
        String tzHours = "";
1✔
1087
        String tzMinutes = "";
1✔
1088

1089
        final TimeZone thisTZ = rightNow.getTimeZone();
1✔
1090
        int tzOffset = thisTZ.getOffset(rightNow.getTime().getTime()); //get timezone offset in milliseconds
1✔
1091
        tzOffset = (tzOffset / 1000); //convert to seconds
1✔
1092
        tzOffset = (tzOffset / 60); //convert to minutes
1✔
1093

1094
        //Sign
1095
        if (tzOffset > 1) {
1!
1096
            tzSign = "+";
1✔
1097
        } else {
1098
            tzSign = "-";
×
1099
            tzOffset *= -1;
×
1100
        }
1101

1102
        //Calc Hours and Minutes?
1103
        if (tzOffset >= 60) {
1!
1104
            //Minutes and Hours
1105
            tzHours += (tzOffset / 60); //hours
1✔
1106

1107
            // do we need to prepend a 0
1108
            if (tzHours.length() == 1) {
1!
1109
                tzHours = "0" + tzHours;
1✔
1110
            }
1111

1112
            tzMinutes += (tzOffset % 60); //minutes
1✔
1113

1114
            // do we need to prepend a 0
1115
            if (tzMinutes.length() == 1) {
1!
1116
                tzMinutes = "0" + tzMinutes;
1✔
1117
            }
1118
        } else {
1119
            //Just Minutes
1120
            tzHours = "00";
×
1121
            tzMinutes += tzOffset;
×
1122
            // do we need to prepend a 0
1123
            if (tzMinutes.length() == 1) {
×
1124
                tzMinutes = "0" + tzMinutes;
×
1125
            }
1126
        }
1127

1128
        dateString += tzSign + tzHours + tzMinutes;
1✔
1129

1130
        return dateString;
1✔
1131
    }
1132

1133
    /**
1134
     * Base64 Encodes a string (used for message subject).
1135
     *
1136
     * Access is package-private for unit testing purposes.
1137
     *
1138
     * @param str The String to encode
1139
     * @throws java.io.UnsupportedEncodingException if the encocding is unsupported
1140
     * @return The encoded String
1141
     */
1142
    static String encode64(final String str, final String charset) throws java.io.UnsupportedEncodingException {
1143
        String result = Base64.encodeBase64String(str.getBytes(charset));
1✔
1144
        result = result.replaceAll("\n", "?=\n =?" + charset + "?B?");
1✔
1145
        result = "=?" + charset + "?B?" + result + "?=";
1✔
1146
        return result;
1✔
1147
    }
1148

1149
    /**
1150
     * Base64 Encodes an email address
1151
     *
1152
     * @param str The email address as a String to encode
1153
     * @param charset the character set
1154
     * @throws java.io.UnsupportedEncodingException if the encocding is unsupported
1155
     * @return The encoded email address String
1156
     */
1157
    private static String encode64Address(final String str, final String charset) throws java.io.UnsupportedEncodingException {
1158
        final int idx = str.indexOf("<");
1✔
1159

1160
        final String result;
1161
        if (idx != -1) {
1!
1162
            result = encode64(str.substring(0, idx), charset) + " " + str.substring(idx);
×
1163
        } else {
1164
            result = str;
1✔
1165
        }
1166

1167
        return result;
1✔
1168
    }
1169

1170
    /**
1171
     * A simple data class to represent an email attachment.
1172
     * <p>
1173
     * It doesn't do anything fancy, it just has private
1174
     * members and get and set methods.
1175
     * <p>
1176
     * Access is package-private for unit testing purposes.
1177
     */
1178
        record MailAttachment(String filename, String mimeType, String data) {
1✔
1179
    }
1180

1181
    /**
1182
     * A simple data class to represent an email.
1183
     * It doesn't do anything fancy, it just has private
1184
     * members and get and set methods.
1185
     *
1186
     * Access is package-private for unit testing purposes.
1187
     */
1188
    static class Mail {
1✔
1189
        private String from;                                                //Who is the mail from
1190
        private String replyTo;                                             //Who should you reply to
1191
        private final List<String> to = new ArrayList<>(1);    //Who is the mail going to
1✔
1192
        private List<String> cc;                                            //Carbon Copy to
1193
        private List<String> bcc;                                           //Blind Carbon Copy to
1194
        private String subject;                                             //Subject of the mail
1195
        private String text;                                                //Body text of the mail
1196
        private String xhtml;                                               //Body XHTML of the mail
1197
        private List<MailAttachment> attachments;                            //Any attachments
1198

1199
        //From
1200
        public void setFrom(final String from) {
1201
            this.from = from;
1✔
1202
        }
1✔
1203

1204
        public String getFrom() {
1205
            return this.from;
1✔
1206
        }
1207

1208
        //reply-to
1209
        public void setReplyTo(final String replyTo) {
1210
            this.replyTo = replyTo;
×
1211
        }
×
1212

1213
        public String getReplyTo() {
1214
            return replyTo;
1✔
1215
        }
1216

1217
        //To
1218
        public void addTo(final String to) {
1219
            this.to.add(to);
1✔
1220
        }
1✔
1221

1222
        public int countTo() {
1223
            return to.size();
1✔
1224
        }
1225

1226
        public String getTo(final int index) {
1227
            return to.get(index);
1✔
1228
        }
1229

1230
        public List<String> getTo() {
1231
            return to;
×
1232
        }
1233

1234
        //CC
1235
        public void addCC(final String cc) {
1236
            if (this.cc == null) {
×
1237
                this.cc = new ArrayList<>();
×
1238
            }
1239
            this.cc.add(cc);
×
1240
        }
×
1241

1242
        public int countCC() {
1243
            if (this.cc == null) {
1!
1244
                return 0;
1✔
1245
            }
1246
            return cc.size();
×
1247
        }
1248

1249
        public String getCC(final int index) {
1250
            if (this.cc == null) {
×
1251
                throw new IndexOutOfBoundsException();
×
1252
            }
1253
            return cc.get(index);
×
1254
        }
1255

1256
        public List<String> getCC() {
1257
            if (this.cc == null) {
×
1258
                return Collections.EMPTY_LIST;
×
1259
            }
1260
            return cc;
×
1261
        }
1262

1263
        //BCC
1264
        public void addBCC(final String bcc) {
1265
            if (this.bcc == null) {
×
1266
                this.bcc = new ArrayList<>();
×
1267
            }
1268
            this.bcc.add(bcc);
×
1269
        }
×
1270

1271
        public int countBCC() {
1272
            if (this.bcc == null) {
1!
1273
                return 0;
1✔
1274
            }
1275
            return bcc.size();
×
1276
        }
1277

1278
        public String getBCC(final int index) {
1279
            if (this.bcc == null) {
×
1280
                throw new IndexOutOfBoundsException();
×
1281
            }
1282
            return bcc.get(index);
×
1283
        }
1284

1285
        public List<String> getBCC() {
1286
            if (this.bcc == null) {
×
1287
                return Collections.EMPTY_LIST;
×
1288
            }
1289
            return bcc;
×
1290
        }
1291

1292
        //Subject
1293
        public void setSubject(final String subject) {
1294
            this.subject = subject;
1✔
1295
        }
1✔
1296

1297
        public String getSubject() {
1298
            return subject;
1✔
1299
        }
1300

1301
        //text
1302
        public void setText(final String text) {
1303
            this.text = text;
1✔
1304
        }
1✔
1305

1306
        public String getText() {
1307
            return text;
1✔
1308
        }
1309

1310
        //xhtml
1311
        public void setXHTML(final String xhtml) {
1312
            this.xhtml = xhtml;
1✔
1313
        }
1✔
1314

1315
        public String getXHTML() {
1316
            return xhtml;
1✔
1317
        }
1318

1319
        public void addAttachment(final MailAttachment ma) {
1320
            if (this.attachments == null) {
1✔
1321
                this.attachments = new ArrayList<>();
1✔
1322
            }
1323
            attachments.add(ma);
1✔
1324
        }
1✔
1325

1326
        public Iterator<MailAttachment> attachmentIterator() {
1327
            if (this.attachments == null) {
1✔
1328
                return Collections.EMPTY_LIST.iterator();
1✔
1329
            }
1330
            return attachments.iterator();
1✔
1331
        }
1332
    }
1333

1334
    private static boolean nonEmpty(@Nullable final String str) {
1335
        return str != null && !str.isEmpty();
1!
1336
    }
1337

1338
    /**
1339
     * Creates a "quoted-string" of the parameter value
1340
     * if it contains a non-token value (See {@link #isNonToken(String)}),
1341
     * otherwise it returns the parameter value as is.
1342
     *
1343
     * Access is package-private for unit testing purposes.
1344
     *
1345
     * @param value parameter value.
1346
     *
1347
     * @return the quoted string parameter value, or the parameter value as is.
1348
     */
1349
    static String parameterValue(final String value) {
1350
        if (isNonToken(value)) {
1✔
1351
            return "\"" + value + "\"";
1✔
1352
        } else {
1353
            return value;
1✔
1354
        }
1355
    }
1356

1357
    /**
1358
     * Determines if the string contains SPACE, CTLs, or `tspecial` (special token)
1359
     * according to <a href="https://www.rfc-editor.org/rfc/rfc2045#section-5">RFC 2045 - Section 5</a>.
1360
     *
1361
     * @param str the string to test
1362
     *
1363
     * @return true if the string contains a non-token, false otherwise.
1364
     */
1365
    private static boolean isNonToken(final String str) {
1366
        return NON_TOKEN_PATTERN.matcher(str).matches();
1✔
1367
    }
1368

1369
    /**
1370
     * Produce a multi-part boundary string.
1371
     *
1372
     * Access is package-private for unit testing purposes.
1373
     *
1374
     * @param multipartInstance the number of this multipart instance.
1375
     *
1376
     * @return the multi-part boundary string.
1377
     */
1378
    private static String multipartBoundary(final int multipartInstance) {
1379
        return multipartBoundaryPrefix(multipartInstance) + "_" + nextRandomPositiveInteger() + "." + System.currentTimeMillis();
1✔
1380
    }
1381

1382
    /**
1383
     * Produce the prefix of a multi-part boundary string.
1384
     *
1385
     * Access is package-private for unit testing purposes.
1386
     *
1387
     * @param multipartInstance the number of this multipart instance.
1388
     *
1389
     * @return the multi-part boundary string prefix.
1390
     */
1391
    static String multipartBoundaryPrefix(final int multipartInstance) {
1392
        return "----=_mail.mime.boundary_" + multipartInstance;
1✔
1393
    }
1394

1395
    /**
1396
     * Generates a positive random integer.
1397
     *
1398
     * @return the integer
1399
     */
1400
    private static int nextRandomPositiveInteger() {
1401
        return RANDOM.nextInt() & Integer.MAX_VALUE;
1✔
1402
    }
1403
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc