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

eXist-db / exist / 41019023168

30 Oct 2026 07:15AM UTC coverage: 58.191% (+0.002%) from 58.189%
41019023168

push

github

web-flow
Merge pull request #6951 from eXist-db/dependabot/maven/maven-plugins-tools-9d8b030d75

Bump org.jacoco:jacoco-maven-plugin from 0.8.15 to 0.8.16 in the maven-plugins-tools group

32497 of 61720 branches covered (52.65%)

Branch coverage included in aggregate %.

85590 of 141211 relevant lines covered (60.61%)

2.73 hits per line

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

67.7
/extensions/modules/mail/src/main/java/org/exist/xquery/modules/mail/SendEmailFunction.java
1
/*
2
 * eXist-db Open Source Native XML Database
3
 * Copyright (C) 2001 The eXist-db Authors
4
 *
5
 * info@exist-db.org
6
 * http://www.exist-db.org
7
 *
8
 * This library is free software; you can redistribute it and/or
9
 * modify it under the terms of the GNU Lesser General Public
10
 * License as published by the Free Software Foundation; either
11
 * version 2.1 of the License, or (at your option) any later version.
12
 *
13
 * This library is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16
 * Lesser General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Lesser General Public
19
 * License along with this library; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
21
 */
22
package org.exist.xquery.modules.mail;
23

24
import org.apache.commons.codec.binary.Base64;
25
import org.apache.logging.log4j.LogManager;
26
import org.apache.logging.log4j.Logger;
27
import org.exist.Version;
28
import org.exist.dom.QName;
29
import org.exist.util.MimeTable;
30
import org.exist.xquery.*;
31
import org.exist.xquery.value.*;
32
import org.w3c.dom.Element;
33
import org.w3c.dom.Node;
34

35
import jakarta.activation.DataHandler;
36
import jakarta.mail.*;
37
import jakarta.mail.internet.*;
38
import jakarta.mail.util.ByteArrayDataSource;
39

40
import javax.annotation.Nullable;
41
import javax.xml.transform.Transformer;
42
import javax.xml.transform.TransformerException;
43
import javax.xml.transform.TransformerFactory;
44
import javax.xml.transform.dom.DOMSource;
45
import javax.xml.transform.stream.StreamResult;
46
import java.io.*;
47
import java.net.InetAddress;
48
import java.net.Socket;
49
import java.util.*;
50
import java.util.regex.Pattern;
51

52
/**
53
 * eXist-db Mail Module Extension SendEmailFunction.
54
 *
55
 * The email sending functionality of the eXist-db Mail Module Extension that
56
 * allows email to be sent from XQuery using either SMTP or Sendmail.
57
 *
58
 * @author <a href="mailto:adam@evolvedbinary.com">Adam Retter</a>
59
 * @author <a href="mailto:robert.walpole@devon.gov.uk">Robert Walpole</a>
60
 * @author <a href="mailto:andrzej@chaeron.com">Andrzej Taramina</a>
61
 * @author <a href="mailto:josemariafg@gmail.com">José María Fernández</a>
62
 */
63
public class SendEmailFunction extends BasicFunction {
64

65
    private static final Logger LOGGER = LogManager.getLogger(SendEmailFunction.class);
3 ✔
66
    private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
2 ✔
67

68
    private final static int MIME_BASE64_MAX_LINE_LENGTH = 76; //RFC 2045, page 24
69

70
    /**
71
     * Regular expression for checking for an RFC 2045 non-token.
72
     */
73
    private static final Pattern NON_TOKEN_PATTERN = Pattern.compile("^.*[\\s\\p{Cntrl}()<>@,;:\\\"/\\[\\]?=].*$");
3 ✔
74

75
    static final String ERROR_MSG_NON_MIME_CLIENT = "Error your mail client is not MIME Compatible";
76

77
    private static final Random RANDOM = new Random();
4 ✔
78

79
    public final static FunctionSignature deprecated = new FunctionSignature(
49 ✔
80
            new QName("send-email", MailModule.NAMESPACE_URI, MailModule.PREFIX),
81
            "Sends an email through the SMTP Server.",
82
            new SequenceType[]
83
                    {
84
                            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>."),
85
                            new FunctionParameterSequenceType("server", Type.STRING, Cardinality.ZERO_OR_ONE, "The SMTP server.  If empty, then it tries to use the local sendmail program."),
86
                            new FunctionParameterSequenceType("charset", Type.STRING, Cardinality.ZERO_OR_ONE, "The charset value used in the \"Content-Type\" message header (Defaults to UTF-8)")
87
                    },
88
            new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.ONE_OR_MORE, "true if the email message was successfully sent")
89
    );
90

91
    public final static FunctionSignature[] signatures = {
44 ✔
92
            new FunctionSignature(
93
                    new QName("send-email", MailModule.NAMESPACE_URI, MailModule.PREFIX),
94
                    "Sends an email using javax.mail messaging libraries.",
95
                    new SequenceType[]
96
                            {
97
                                    new FunctionParameterSequenceType("mail-handle", Type.LONG, Cardinality.EXACTLY_ONE, "The JavaMail session handle retrieved from mail:get-mail-session()"),
98
                                    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>.")
99
                            },
100
                    new SequenceType(Type.EMPTY_SEQUENCE, Cardinality.EMPTY_SEQUENCE)
101
            )
102
    };
103

104
    public SendEmailFunction(final XQueryContext context, final FunctionSignature signature) {
105
        super(context, signature);
4 ✔
106
    }
1 ✔
107

108
    @Override
109
    public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
110
        if (args.length == 3) {
4 ✔
111
            return deprecatedSendEmail(args, contextSequence);
5 ✔
112
        } else {
113
            return sendEmail(args, contextSequence);
5 ✔
114
        }
115
    }
116

117
    public Sequence sendEmail(final Sequence[] args, final Sequence contextSequence) throws XPathException {
118
        // was a session handle specified?
119
        if (args[0].isEmpty()) {
5 !
120
            throw new XPathException(this, "Session handle not specified");
×
121
        }
122

123
        // get the Session
124
        final long sessionHandle = ((IntegerValue) args[0].itemAt(0)).getLong();
8 ✔
125
        final Session session = MailModule.retrieveSession(context, sessionHandle);
5 ✔
126
        if (session == null) {
2 !
127
            throw new XPathException(this, "Invalid Session handle specified");
×
128
        }
129

130
        try {
131
            final Message[] messages = parseInputEmails(session, args[1]);
7 ✔
132
            String proto = session.getProperty("mail.transport.protocol");
4 ✔
133
            if (proto == null) {
2 !
134
                proto = "smtp";
×
135
            }
136
            try (final Transport t = session.getTransport(proto)) {
4 ✔
137
                if (session.getProperty("mail." + proto + ".auth") != null) {
5 ✔
138
                    t.connect(session.getProperty("mail." + proto + ".user"), session.getProperty("mail." + proto + ".password"));
11 ✔
139
                } else {
140
                    t.connect();
2 ✔
141
                }
142
                for (final Message msg : messages) {
16 ✔
143
                    t.sendMessage(msg, msg.getAllRecipients());
5 ✔
144
                }
145
            }
146

147
            return Sequence.EMPTY_SEQUENCE;
2 ✔
148
        } catch (final TransformerException e) {
×
149
            throw new XPathException(this, "Could not Transform XHTML Message Body: " + e.getMessage(), e);
×
150
        } catch (final MessagingException e) {
×
151
            throw new XPathException(this, "Could not send message(s): " + e.getMessage(), e);
×
152
        } catch (final IOException e) {
×
153
            throw new XPathException(this, "Attachment in some message could not be prepared: " + e.getMessage(), e);
×
154
        } catch (final Throwable t) {
×
155
            throw new XPathException(this, "Unexpected error from JavaMail layer (Is your message well structured?): " + t.getMessage(), t);
×
156
        }
157
    }
158

159
    public Sequence deprecatedSendEmail(final Sequence[] args, final Sequence contextSequence) throws XPathException {
160
        try {
161
            //get the charset parameter, default to UTF-8
162
            final String charset;
163
            if (!args[2].isEmpty()) {
5 !
164
                charset = args[2].getStringValue();
×
165
            } else {
166
                charset = "UTF-8";
2 ✔
167
            }
168

169
            // Parse the XML <mail> Elements into mail Objects
170
            final int len = args[0].getItemCount();
5 ✔
171
            final Element[] mailElements = new Element[len];
3 ✔
172
            for (int i = 0; i < len; i++) {
7 ✔
173
                mailElements[i] = (Element) args[0].itemAt(i);
9 ✔
174
            }
175
            final Mail[] mails = parseMailElement(mailElements);
4 ✔
176

177
            final ValueSequence results = new ValueSequence();
4 ✔
178

179
            //Send email with Sendmail or SMTP?
180
            if (!args[1].isEmpty()) {
5 !
181
                //SMTP
182
                final boolean[] mailResults = sendBySMTP(mails, args[1].getStringValue(), charset);
9 ✔
183

184
                for (final boolean mailResult : mailResults) {
16 ✔
185
                    results.add(BooleanValue.valueOf(mailResult));
4 ✔
186
                }
187
            } else {
1 ✔
188
                for (final Mail mail : mails) {
×
189
                    final boolean result = sendBySendmail(mail, charset);
×
190
                    results.add(BooleanValue.valueOf(result));
×
191
                }
192
            }
193

194
            return results;
2 ✔
195
        } catch (final TransformerException | IOException e) {
×
196
            throw new XPathException(this, "Could not Transform XHTML Message Body: " + e.getMessage(), e);
×
197
        } catch (final SMTPException e) {
×
198
            throw new XPathException(this, "Could not send message(s)" + e.getMessage(), e);
×
199
        }
200
    }
201

202
    private Message[] parseInputEmails(final Session session, final Sequence arg) throws IOException, MessagingException, TransformerException {
203
        // Parse the XML <mail> Elements into mail Objects
204
        final int len = arg.getItemCount();
3 ✔
205
        final Element[] mailElements = new Element[len];
3 ✔
206
        for (int i = 0; i < len; i++) {
7 ✔
207
            mailElements[i] = (Element) arg.itemAt(i);
7 ✔
208
        }
209
        return parseMessageElement(session, mailElements);
5 ✔
210
    }
211

212
    /**
213
     * Sends an email using the Operating Systems sendmail application
214
     *
215
     * @param mail representation of the email to send
216
     * @param charset the character set
217
     * @return boolean value of true of false indicating success or failure to send email
218
     */
219
    private boolean sendBySendmail(final Mail mail, final String charset) {
220

221
        //Create a list of all Recipients, should include to, cc and bcc recipient
222
        final List<String> allrecipients = new ArrayList<>();
×
223
        allrecipients.addAll(mail.getTo());
×
224
        allrecipients.addAll(mail.getCC());
×
225
        allrecipients.addAll(mail.getBCC());
×
226

227
        // Normalize all recipients to plain email addresses for sendmail arguments
228
        final List<String> normalizedRecipients = new ArrayList<>(allrecipients.size());
×
229

230
        for (final String recipient : allrecipients) {
×
231
            //Check format of to address does it include a name as well as the email address?
232
            if (recipient.contains("<")) {
×
233
                //yes, just add the email address
234
                normalizedRecipients.add(recipient.substring(recipient.indexOf("<") + 1, recipient.indexOf(">")));
×
235
            } else {
236
                //add the email address
237
                normalizedRecipients.add(recipient);
×
238
            }
239
        }
×
240

241
        try {
242
            //Create a sendmail Process
243
            final List<String> command = new ArrayList<>(normalizedRecipients.size() + 1);
×
244
            command.add("/usr/sbin/sendmail");
×
245
            command.addAll(normalizedRecipients);
×
246
            final Process p = new ProcessBuilder(command).start();
×
247

248
            //Get a Buffered Print Writer to the Processes stdOut
249
            try (final PrintWriter out = new PrintWriter(new OutputStreamWriter(p.getOutputStream(), charset))) {
×
250
                //Send the Message
251
                writeMessage(out, mail, false, charset);
×
252
            }
253
        } catch (final IOException e) {
×
254
            LOGGER.error(e.getMessage(), e);
×
255
            return false;
×
256
        }
×
257

258
        // Message Sent Succesfully
259
        if (LOGGER.isDebugEnabled()) {
×
260
            LOGGER.debug("send-email() message sent using Sendmail {}", new Date());
×
261
        }
262

263
        return true;
×
264
    }
265

266
    private static class SMTPException extends Exception {
267
        private static final long serialVersionUID = 4859093648476395159L;
268

269
        public SMTPException(final String message) {
270
            super(message);
×
271
        }
×
272

273
        public SMTPException(final Throwable cause) {
274
            super(cause);
×
275
        }
×
276
    }
277

278
    /**
279
     * Sends an email using SMTP
280
     *
281
     * @param mails         A list of mail object representing the email to send
282
     * @param smtpServerArg The SMTP Server to send the email through
283
     * @param charset the character set
284
     * @return boolean value of true of false indicating success or failure to send email
285
     * @throws SMTPException if an I/O error occurs
286
     */
287
    private boolean[] sendBySMTP(final Mail[] mails, final String smtpServerArg, final String charset) throws SMTPException {
288
        String smtpHost = "localhost";
2 ✔
289
        int smtpPort = 25;
2 ✔
290

291
        if (smtpServerArg != null && !smtpServerArg.isEmpty()) {
5 !
292
            final int idx = smtpServerArg.indexOf(':');
4 ✔
293
            if (idx > -1) {
3 !
294
                smtpHost = smtpServerArg.substring(0, idx);
5 ✔
295
                smtpPort = Integer.parseInt(smtpServerArg.substring(idx + 1));
8 ✔
296
            } else {
297
                smtpHost = smtpServerArg;
×
298
            }
299
        }
300

301
        String smtpResult;             //Holds the server Result code when an SMTP Command is executed
302

303
        final boolean[] sendMailResults = new boolean[mails.length];
4 ✔
304

305
        try (
306
                //Create a Socket and connect to the SMTP Server
307
                final Socket smtpSock = new Socket(smtpHost, smtpPort);
6 ✔
308

309
                //Create a Buffered Reader for the Socket
310
                final BufferedReader smtpIn = new BufferedReader(new InputStreamReader(smtpSock.getInputStream()));
9 ✔
311

312
                //Create an Output Writer for the Socket
313
                final PrintWriter smtpOut = new PrintWriter(new OutputStreamWriter(smtpSock.getOutputStream(), charset))) {
10 ✔
314

315
            //First line sent to us from the SMTP server should be "220 blah blah", 220 indicates okay
316
            smtpResult = smtpIn.readLine();
3 ✔
317
            if (!smtpResult.startsWith("220")) {
4 !
318
                final String errMsg = "Error - SMTP Server not ready: '" + smtpResult + "'";
×
319
                LOGGER.error(errMsg);
×
320
                throw new SMTPException(errMsg);
×
321
            }
322

323
            //Say "HELO"
324
            smtpOut.print("HELO " + InetAddress.getLocalHost().getHostName() + "\r\n");
5 ✔
325
            smtpOut.flush();
2 ✔
326

327
            //get "HELLO" response, should be "250 blah blah"
328
            smtpResult = smtpIn.readLine();
3 ✔
329
            if (smtpResult == null) {
2 !
330
                final String errMsg = "Error - Unexpected null response to SMTP HELO";
×
331
                LOGGER.error(errMsg);
×
332
                throw new SMTPException(errMsg);
×
333
            }
334

335
            if (!smtpResult.startsWith("250")) {
4 !
336
                final String errMsg = "Error - SMTP HELO Failed: '" + smtpResult + "'";
×
337
                LOGGER.error(errMsg);
×
338
                throw new SMTPException(errMsg);
×
339
            }
340

341
            //write SMTP message(s)
342
            for (int i = 0; i < mails.length; i++) {
8 ✔
343
                final boolean mailResult = writeSMTPMessage(mails[i], smtpOut, smtpIn, charset);
9 ✔
344
                sendMailResults[i] = mailResult;
4 ✔
345
            }
346

347
            //all done, time to "QUIT"
348
            smtpOut.print("QUIT\r\n");
3 ✔
349
            smtpOut.flush();
2 ✔
350

351
        } catch (final IOException ioe) {
×
352
            LOGGER.error(ioe.getMessage(), ioe);
×
353
            throw new SMTPException(ioe);
×
354
        }
1 ✔
355

356
        //Message(s) Sent Succesfully
357
        if (LOGGER.isDebugEnabled()) {
3 !
358
            LOGGER.debug("send-email() message(s) sent using SMTP {}", new Date());
×
359
        }
360

361
        return sendMailResults;
2 ✔
362
    }
363

364
    private boolean writeSMTPMessage(final Mail mail, final PrintWriter smtpOut, final BufferedReader smtpIn, final String charset) {
365
        try {
366
            String smtpResult;
367

368
            //Send "MAIL FROM:"
369
            //Check format of from address does it include a name as well as the email address?
370
            if (mail.getFrom().contains("<")) {
5 !
371
                //yes, just send the email address
372
                smtpOut.print("MAIL FROM:<" + mail.getFrom().substring(mail.getFrom().indexOf("<") + 1, mail.getFrom().indexOf(">")) + ">\r\n");
×
373
            } else {
374
                //no, doesnt include a name so send the email address
375
                smtpOut.print("MAIL FROM:<" + mail.getFrom() + ">\r\n");
5 ✔
376
            }
377
            smtpOut.flush();
2 ✔
378

379
            //Get "MAIL FROM:" response
380
            smtpResult = smtpIn.readLine();
3 ✔
381
            if (smtpResult == null) {
2 !
382
                LOGGER.error("Error - Unexpected null response to SMTP MAIL FROM");
×
383
                return false;
×
384
            }
385
            if (!smtpResult.startsWith("250")) {
4 !
386
                LOGGER.error("Error - SMTP MAIL FROM failed: {}", smtpResult);
×
387
                return false;
×
388
            }
389

390
            //RCPT TO should be issued for each to, cc and bcc recipient
391
            final List<String> allrecipients = new ArrayList<>();
4 ✔
392
            allrecipients.addAll(mail.getTo());
5 ✔
393
            allrecipients.addAll(mail.getCC());
5 ✔
394
            allrecipients.addAll(mail.getBCC());
5 ✔
395

396
            for (final String recipient : allrecipients) {
10 ✔
397
                //Send "RCPT TO:"
398
                //Check format of to address does it include a name as well as the email address?
399
                if (recipient.contains("<")) {
4 !
400
                    //yes, just send the email address
401
                    smtpOut.print("RCPT TO:<" + recipient.substring(recipient.indexOf("<") + 1, recipient.indexOf(">")) + ">\r\n");
×
402
                } else {
403
                    smtpOut.print("RCPT TO:<" + recipient + ">\r\n");
4 ✔
404
                }
405
                smtpOut.flush();
2 ✔
406
                //Get "RCPT TO:" response
407
                smtpResult = smtpIn.readLine();
3 ✔
408
                if (!smtpResult.startsWith("250")) {
4 !
409
                    LOGGER.error("Error - SMTP RCPT TO failed: {}", smtpResult);
×
410
                }
411
            }
1 ✔
412

413
            //SEND "DATA"
414
            smtpOut.print("DATA\r\n");
3 ✔
415
            smtpOut.flush();
2 ✔
416

417
            //Get "DATA" response, should be "354 blah blah" (optionally preceded by "250 OK")
418
            smtpResult = smtpIn.readLine();
3 ✔
419
            if (smtpResult.startsWith("250")) {
4 !
420
                // should then be followed by "354 blah blah"
421
                smtpResult = smtpIn.readLine();
×
422
            }
423

424
            if (!smtpResult.startsWith("354")) {
4 !
425
                LOGGER.error("Error - SMTP DATA failed: {}", smtpResult);
×
426
                return false;
×
427
            }
428

429
            //Send the Message
430
            writeMessage(smtpOut, mail, true, charset);
5 ✔
431

432
            //Get end message response, should be "250 blah blah"
433
            smtpResult = smtpIn.readLine();
3 ✔
434
            if (!smtpResult.startsWith("250")) {
4 !
435
                LOGGER.error("Error - Message not accepted: {}", smtpResult);
×
436
                return false;
×
437
            }
438
        } catch (final IOException e) {
×
439
            LOGGER.error(e.getMessage(), e);
×
440
            return false;
×
441
        }
1 ✔
442

443
        return true;
2 ✔
444
    }
445

446
    /**
447
     * Writes an email payload (Headers + Body) from a mail object.
448
     *
449
     * Access is package-private for unit testing purposes.
450
     *
451
     * @param out   A PrintWriter to receive the email
452
     * @param aMail A mail object representing the email to write out
453
     * @param useCrLf true to use CRLF for line ending, false to use LF
454
     * @param charset the character set
455
     * @throws IOException if an I/O error occurs
456
     */
457
    static void writeMessage(final PrintWriter out, final Mail aMail, final boolean useCrLf, final String charset) throws IOException {
458
        final String eol = useCrLf ? "\r\n" : "\n";
5 !
459

460
        //write the message headers
461

462
        out.print("From: " + encode64Address(aMail.getFrom(), charset) + eol);
8 ✔
463

464
        if (aMail.getReplyTo() != null) {
3 !
465
            out.print("Reply-To: " + encode64Address(aMail.getReplyTo(), charset) + eol);
×
466
        }
467

468
        for (int x = 0; x < aMail.countTo(); x++) {
8 ✔
469
            out.print("To: " + encode64Address(aMail.getTo(x), charset) + eol);
9 ✔
470
        }
471

472
        for (int x = 0; x < aMail.countCC(); x++) {
6 !
473
            out.print("CC: " + encode64Address(aMail.getCC(x), charset) + eol);
×
474
        }
475

476
        for (int x = 0; x < aMail.countBCC(); x++) {
6 !
477
            out.print("BCC: " + encode64Address(aMail.getBCC(x), charset) + eol);
×
478
        }
479

480
        out.print("Date: " + getDateRFC822() + eol);
5 ✔
481
        String subject = aMail.getSubject();
3 ✔
482
        if (subject == null) {
2 !
483
            subject = "";
×
484
        }
485
        out.print("Subject: " + encode64(subject, charset) + eol);
7 ✔
486
        out.print("X-Mailer: eXist-db " + Version.getVersion() + " mail:send-email()" + eol);
5 ✔
487
        out.print("MIME-Version: 1.0" + eol);
4 ✔
488

489

490
        boolean multipartAlternative = false;
2 ✔
491
        int multipartInstanceCount = 0;
2 ✔
492
        final Deque<String> multipartBoundary = new ArrayDeque<>();
4 ✔
493

494
        if (aMail.attachmentIterator().hasNext()) {
4 ✔
495
            // we have an attachment as well as text and/or html, so we need a multipart/mixed message
496
            multipartBoundary.addFirst(multipartBoundary(++multipartInstanceCount));
6 ✔
497
        } else if (nonEmpty(aMail.getText()) && nonEmpty(aMail.getXHTML())) {
8 ✔
498
            // we have text and html, so we need a multipart/alternative message and no attachment
499
            multipartAlternative = true;
2 ✔
500
            multipartBoundary.addFirst(multipartBoundary(++multipartInstanceCount));
5 ✔
501
        }
502
//        else {
503
//            // we have either text or html and no attachment this message is not multipart
504
//        }
505

506
        //content type
507
        if (!multipartBoundary.isEmpty()) {
3 ✔
508
            //multipart message
509

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

512
            //Mime warning
513
            out.print(eol);
3 ✔
514
            out.print(ERROR_MSG_NON_MIME_CLIENT + eol);
4 ✔
515
            out.print(eol);
3 ✔
516

517
            out.print("--" + multipartBoundary.peekFirst() + eol);
7 ✔
518
        }
519

520
        if (nonEmpty(aMail.getText()) && nonEmpty(aMail.getXHTML()) && aMail.attachmentIterator().hasNext()) {
12 ✔
521
            // we are a multipart inside a multipart
522
            multipartBoundary.addFirst(multipartBoundary(++multipartInstanceCount));
5 ✔
523

524
            out.print("Content-Type: multipart/alternative; boundary=" + parameterValue(multipartBoundary.peekFirst()) + eol);
8 ✔
525
            out.print(eol);
3 ✔
526
            out.print("--" + multipartBoundary.peekFirst() + eol);
7 ✔
527
        }
528

529
        //text email
530
        if (nonEmpty(aMail.getText())) {
4 ✔
531
            out.print("Content-Type: text/plain; charset=" + charset + eol);
5 ✔
532
            out.print("Content-Transfer-Encoding: 8bit" + eol);
4 ✔
533

534
            //now send the txt message
535
            out.print(eol);
3 ✔
536
            out.print(aMail.getText() + eol);
6 ✔
537

538
            if (!multipartBoundary.isEmpty()) {
3 ✔
539
                if (nonEmpty(aMail.getXHTML()) || aMail.attachmentIterator().hasNext()) {
8 !
540
                    out.print("--" + multipartBoundary.peekFirst() + eol);
8 ✔
541
                } else {
542
                    // End multipart message
543
                    out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
×
544
                    multipartBoundary.removeFirst();
×
545
                }
546
            }
547
        }
548

549
        //HTML email
550
        if (nonEmpty(aMail.getXHTML())) {
4 ✔
551
            out.print("Content-Type: text/html; charset=" + charset + eol);
5 ✔
552
            out.print("Content-Transfer-Encoding: 8bit" + eol);
4 ✔
553

554
            //now send the html message
555
            out.print(eol);
3 ✔
556
            out.print("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" + eol);
4 ✔
557
            out.print(aMail.getXHTML() + eol);
6 ✔
558

559
            if (!multipartBoundary.isEmpty()) {
3 ✔
560
                if (aMail.attachmentIterator().hasNext()) {
4 ✔
561
                    if (nonEmpty(aMail.getText()) && nonEmpty(aMail.getXHTML()) && aMail.attachmentIterator().hasNext()) {
12 !
562
                        // End multipart message
563
                        out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
7 ✔
564
                        multipartBoundary.removeFirst();
3 ✔
565
                    }
566

567
                    out.print("--" + multipartBoundary.peekFirst() + eol);
8 ✔
568

569
                } else {
570
                    // End multipart message
571
                    out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
7 ✔
572
                    multipartBoundary.removeFirst();
3 ✔
573
                }
574
            }
575
        }
576

577
        //attachments
578
        if (aMail.attachmentIterator().hasNext()) {
4 ✔
579
            for (final Iterator<MailAttachment> itAttachment = aMail.attachmentIterator(); itAttachment.hasNext(); ) {
6 ✔
580
                final MailAttachment ma = itAttachment.next();
4 ✔
581

582
                out.print("Content-Type: " + ma.mimeType() + "; name=" + parameterValue(ma.filename()) + eol);
9 ✔
583
                out.print("Content-Transfer-Encoding: base64" + eol);
4 ✔
584
                out.print("Content-Description: " + ma.filename() + eol);
6 ✔
585
                out.print("Content-Disposition: attachment; filename=" + parameterValue(ma.filename()) + eol);
7 ✔
586
                out.print(eol);
3 ✔
587

588

589
                //write out the attachment encoded data in fixed width lines
590
                final char[] buf = new char[MIME_BASE64_MAX_LINE_LENGTH];
3 ✔
591
                int read = -1;
2 ✔
592
                final Reader attachmentDataReader = new StringReader(ma.data());
6 ✔
593
                while ((read = attachmentDataReader.read(buf, 0, MIME_BASE64_MAX_LINE_LENGTH)) > -1) {
9 ✔
594
                    out.print(String.valueOf(buf, 0, read) + eol);
9 ✔
595
                }
596

597
                if (itAttachment.hasNext()) {
3 ✔
598
                    out.print("--" + multipartBoundary.peekFirst() + eol);
7 ✔
599
                }
600
            }
1 ✔
601

602
            // End multipart message
603
            out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
7 ✔
604
            multipartBoundary.removeFirst();
3 ✔
605
        }
606

607
        //end the message, <cr><lf>.<cr><lf>
608
        out.print(eol);
3 ✔
609
        out.print("." + eol);
4 ✔
610
        out.flush();
2 ✔
611
    }
1 ✔
612

613
    /**
614
     * Constructs a mail Object from an XML representation of an email
615
     * <p>
616
     * The XML email Representation is expected to look something like this
617
     *
618
     * <mail>
619
     * <from></from>
620
     * <reply-to></reply-to>
621
     * <to></to>
622
     * <cc></cc>
623
     * <bcc></bcc>
624
     * <subject></subject>
625
     * <message>
626
     * <text></text>
627
     * <xhtml></xhtml>
628
     * </message>
629
     * </mail>
630
     *
631
     * @param mailElements The XML mail Node
632
     * @throws TransformerException if a transformation error occurs
633
     * @return A mail Object representing the XML mail Node
634
     */
635
    private Mail[] parseMailElement(final Element[] mailElements) throws TransformerException, IOException {
636
        Mail[] mails = new Mail[mailElements.length];
4 ✔
637

638
        int i = 0;
2 ✔
639
        for (final Element mailElement : mailElements) {
16 ✔
640

641
            //Make sure that message has a Mail node
642
            if ("mail".equals(mailElement.getLocalName())) {
5 !
643
                final Mail mail = new Mail();
4 ✔
644

645
                //Get the First Child
646
                Node child = mailElement.getFirstChild();
3 ✔
647
                while (child != null) {
2 ✔
648
                    //Parse each of the child nodes
649
                    if (Node.ELEMENT_NODE == child.getNodeType() && child.hasChildNodes()) {
7 !
650
                        switch (child.getLocalName()) {
9 !
651
                            case "from":
652
                                mail.setFrom(child.getFirstChild().getNodeValue());
5 ✔
653
                                break;
1 ✔
654
                            case "reply-to":
655
                                mail.setReplyTo(child.getFirstChild().getNodeValue());
×
656
                                break;
×
657
                            case "to":
658
                                mail.addTo(child.getFirstChild().getNodeValue());
5 ✔
659
                                break;
1 ✔
660
                            case "cc":
661
                                mail.addCC(child.getFirstChild().getNodeValue());
×
662
                                break;
×
663
                            case "bcc":
664
                                mail.addBCC(child.getFirstChild().getNodeValue());
×
665
                                break;
×
666
                            case "subject":
667
                                mail.setSubject(child.getFirstChild().getNodeValue());
5 ✔
668
                                break;
1 ✔
669
                            case "message":
670
                                //If the message node, then parse the child text and xhtml nodes
671
                                Node bodyPart = child.getFirstChild();
3 ✔
672
                                while (bodyPart != null) {
2 ✔
673
                                    if ("text".equals(bodyPart.getLocalName())) {
5 ✔
674
                                        mail.setText(bodyPart.getFirstChild().getNodeValue());
6 ✔
675
                                    } else if ("xhtml".equals(bodyPart.getLocalName())) {
5 !
676
                                        //Convert everything inside <xhtml></xhtml> to text
677
                                        final Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
3 ✔
678
                                        final DOMSource source = new DOMSource(bodyPart.getFirstChild());
6 ✔
679
                                        try (final StringWriter strWriter = new StringWriter()) {
4 ✔
680
                                            final StreamResult result = new StreamResult(strWriter);
5 ✔
681
                                            transformer.transform(source, result);
4 ✔
682
                                            mail.setXHTML(strWriter.toString());
4 ✔
683
                                        }
684
                                    }
685

686
                                    //next body part
687
                                    bodyPart = bodyPart.getNextSibling();
4 ✔
688
                                }
689
                                break;
690
                            case "attachment":
691
                                final Element attachment = (Element) child;
3 ✔
692
                                final MailAttachment ma = new MailAttachment(attachment.getAttribute("filename"), attachment.getAttribute("mimetype"), attachment.getFirstChild().getNodeValue());
13 ✔
693
                                mail.addAttachment(ma);
3 ✔
694
                                break;
695
                        }
696
                    }
697

698
                    //next node
699
                    child = child.getNextSibling();
4 ✔
700

701
                }
702
                mails[i++] = mail;
5 ✔
703
            }
704
        }
705

706
        if (i != mailElements.length) {
4 !
707
            mails = Arrays.copyOf(mails, i);
×
708
        }
709

710
        return mails;
2 ✔
711
    }
712

713
    /**
714
     * Constructs a mail Object from an XML representation of an email
715
     * <p>
716
     * The XML email Representation is expected to look something like this
717
     *
718
     * <mail>
719
     * <from></from>
720
     * <reply-to></reply-to>
721
     * <to></to>
722
     * <cc></cc>
723
     * <bcc></bcc>
724
     * <subject></subject>
725
     * <message>
726
     * <text charset="" encoding=""></text>
727
     * <xhtml charset="" encoding=""></xhtml>
728
     * <generic charset="" type="" encoding=""></generic>
729
     * </message>
730
     * <attachment mimetype="" filename=""></attachment>
731
     * </mail>
732
     *
733
     * @param mailElements The XML mail Node
734
     * @throws IOException          if an I/O error occurs
735
     * @throws MessagingException   if an email error occurs
736
     * @throws TransformerException if a transformation error occurs
737
     * @return A mail Object representing the XML mail Node
738
     */
739
    private Message[] parseMessageElement(final Session session, final Element[] mailElements) throws IOException, MessagingException, TransformerException {
740

741
        Message[] mails = new Message[mailElements.length];
4 ✔
742

743
        int i = 0;
2 ✔
744
        for (final Element mailElement : mailElements) {
16 ✔
745
            //Make sure that message has a Mail node
746
            if ("mail".equals(mailElement.getLocalName())) {
5 !
747
                //New message Object
748
                // create a message
749
                final MimeMessage msg = new MimeMessage(session);
5 ✔
750

751
                boolean fromWasSet = false;
2 ✔
752
                final List<InternetAddress> replyTo = new ArrayList<>();
4 ✔
753
                MimeBodyPart body = null;
2 ✔
754
                Multipart multibody = null;
2 ✔
755
                final List<MimeBodyPart> attachments = new ArrayList<>();
4 ✔
756
                String firstContent = null;
2 ✔
757
                String firstContentType = null;
2 ✔
758
                String firstCharset = null;
2 ✔
759
                String firstEncoding = null;
2 ✔
760

761
                //Get the First Child
762
                Node child = mailElement.getFirstChild();
3 ✔
763
                while (child != null) {
2 ✔
764
                    //Parse each of the child nodes
765
                    if (Node.ELEMENT_NODE == child.getNodeType() && child.hasChildNodes()) {
7 !
766
                        switch (child.getLocalName()) {
9 !
767
                            case "from":
768
                                // set the from and to address
769
                                final InternetAddress[] addressFrom = {
7 ✔
770
                                        new InternetAddress(child.getFirstChild().getNodeValue())
5 ✔
771
                                };
772
                                msg.addFrom(addressFrom);
3 ✔
773
                                fromWasSet = true;
2 ✔
774
                                break;
1 ✔
775
                            case "reply-to":
776
                                // As we can only set the reply-to, not add them, let's keep
777
                                // all of them in a list
778
                                replyTo.add(new InternetAddress(child.getFirstChild().getNodeValue()));
×
779
                                break;
×
780
                            case "to":
781
                                msg.addRecipient(Message.RecipientType.TO, new InternetAddress(child.getFirstChild().getNodeValue()));
9 ✔
782
                                break;
1 ✔
783
                            case "cc":
784
                                msg.addRecipient(Message.RecipientType.CC, new InternetAddress(child.getFirstChild().getNodeValue()));
×
785
                                break;
×
786
                            case "bcc":
787
                                msg.addRecipient(Message.RecipientType.BCC, new InternetAddress(child.getFirstChild().getNodeValue()));
×
788
                                break;
×
789
                            case "subject":
790
                                msg.setSubject(child.getFirstChild().getNodeValue());
5 ✔
791
                                break;
1 ✔
792
                            case "header":
793
                                // Optional : You can also set your custom headers in the Email if you Want
794
                                msg.addHeader(((Element) child).getAttribute("name"), child.getFirstChild().getNodeValue());
×
795
                                break;
×
796
                            case "message":
797
                                //If the message node, then parse the child text and xhtml nodes
798
                                Node bodyPart = child.getFirstChild();
3 ✔
799
                                while (bodyPart != null) {
2 ✔
800
                                    if (Node.ELEMENT_NODE != bodyPart.getNodeType()) {
4 !
801
                                        continue;
×
802
                                    }
803

804
                                    final Element elementBodyPart = (Element) bodyPart;
3 ✔
805
                                    String content = null;
2 ✔
806
                                    String contentType = null;
2 ✔
807

808
                                    switch (bodyPart.getLocalName()) {
9 !
809
                                        case "text":
810
                                            // Setting the Subject and Content Type
811
                                            content = bodyPart.getFirstChild().getNodeValue();
4 ✔
812
                                            contentType = "plain";
2 ✔
813
                                            break;
1 ✔
814
                                        case "xhtml":
815
                                            //Convert everything inside <xhtml></xhtml> to text
816
                                            final Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
3 ✔
817
                                            final DOMSource source = new DOMSource(bodyPart.getFirstChild());
6 ✔
818
                                            try (final StringWriter strWriter = new StringWriter()) {
4 ✔
819
                                                final StreamResult result = new StreamResult(strWriter);
5 ✔
820
                                                transformer.transform(source, result);
4 ✔
821
                                                content = strWriter.toString();
3 ✔
822
                                            }
823
                                            contentType = "html";
2 ✔
824
                                            break;
1 ✔
825
                                        case "generic":
826
                                            // Setting the Subject and Content Type
827
                                            content = elementBodyPart.getFirstChild().getNodeValue();
×
828
                                            contentType = elementBodyPart.getAttribute("type");
×
829
                                            break;
830
                                    }
831

832
                                    // Now, time to store it
833
                                    if (content != null && contentType != null && !contentType.isEmpty()) {
7 !
834
                                        String charset = elementBodyPart.getAttribute("charset");
4 ✔
835
                                        if (charset.isEmpty()) {
3 !
836
                                            charset = "UTF-8";
2 ✔
837
                                        }
838

839
                                        String encoding = elementBodyPart.getAttribute("encoding");
4 ✔
840
                                        if (encoding.isEmpty()) {
3 !
841
                                            encoding = "quoted-printable";
2 ✔
842
                                        }
843

844
                                        if (body != null && multibody == null) {
4 !
845
                                            multibody = new MimeMultipart("alternative");
5 ✔
846
                                            multibody.addBodyPart(body);
3 ✔
847
                                        }
848

849
                                        if (body == null) {
2 ✔
850
                                            firstContent = content;
2 ✔
851
                                            firstCharset = charset;
2 ✔
852
                                            firstContentType = contentType;
2 ✔
853
                                            firstEncoding = encoding;
2 ✔
854
                                        }
855
                                        body = new MimeBodyPart();
4 ✔
856
                                        body.setText(content, charset, contentType);
5 ✔
857
                                        body.setHeader("Content-Transfer-Encoding", encoding);
4 ✔
858
                                        if (multibody != null) {
2 ✔
859
                                            multibody.addBodyPart(body);
3 ✔
860
                                        }
861
                                    }
862

863
                                    //next body part
864
                                    bodyPart = bodyPart.getNextSibling();
3 ✔
865
                                }
1 ✔
866
                                break;
867
                            case "attachment":
868
                                final Element attachment = (Element) child;
3 ✔
869
                                final MimeBodyPart part;
870
                                // if mimetype indicates a binary resource, assume the content is base64 encoded
871
                                if (MimeTable.getInstance().isTextContent(attachment.getAttribute("mimetype"))) {
6 ✔
872
                                    part = new MimeBodyPart();
5 ✔
873
                                } else {
874
                                    part = new PreencodedMimeBodyPart("base64");
5 ✔
875
                                }
876
                                final StringBuilder content = new StringBuilder();
4 ✔
877
                                Node attachChild = attachment.getFirstChild();
3 ✔
878
                                while (attachChild != null) {
2 ✔
879
                                    if (Node.ELEMENT_NODE == attachChild.getNodeType()) {
4 !
880
                                        final Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
×
881
                                        final DOMSource source = new DOMSource(attachChild);
×
882
                                        try (final StringWriter strWriter = new StringWriter()) {
×
883
                                            final StreamResult result = new StreamResult(strWriter);
×
884
                                            transformer.transform(source, result);
×
885
                                            content.append(strWriter);
×
886
                                        }
887
                                    } else {
×
888
                                        content.append(attachChild.getNodeValue());
5 ✔
889
                                    }
890
                                    attachChild = attachChild.getNextSibling();
4 ✔
891
                                }
892
                                part.setDataHandler(new DataHandler(new ByteArrayDataSource(content.toString(), attachment.getAttribute("mimetype"))));
13 ✔
893
                                part.setFileName(attachment.getAttribute("filename"));
5 ✔
894
                                // part.setHeader("Content-Transfer-Encoding", "base64");
895
                                attachments.add(part);
4 ✔
896
                                break;
897
                        }
898
                    }
899

900
                    //next node
901
                    child = child.getNextSibling();
4 ✔
902
                }
903

904
                // Lost from
905
                if (!fromWasSet) {
2 !
906
                    msg.setFrom();
×
907
                }
908

909
                msg.setReplyTo(replyTo.toArray(new InternetAddress[0]));
7 ✔
910

911
                // Preparing content and attachments
912
                if (!attachments.isEmpty()) {
3 ✔
913
                    if (multibody == null) {
2 ✔
914
                        multibody = new MimeMultipart("mixed");
5 ✔
915
                        if (body != null) {
2 !
916
                            multibody.addBodyPart(body);
4 ✔
917
                        }
918
                    } else {
919
                        final MimeMultipart container = new MimeMultipart("mixed");
5 ✔
920
                        final MimeBodyPart containerBody = new MimeBodyPart();
4 ✔
921
                        containerBody.setContent(multibody);
3 ✔
922
                        container.addBodyPart(containerBody);
3 ✔
923
                        multibody = container;
2 ✔
924
                    }
925
                    for (final MimeBodyPart part : attachments) {
10 ✔
926
                        multibody.addBodyPart(part);
3 ✔
927
                    }
1 ✔
928
                }
929

930
                // And now setting-up content
931
                if (multibody != null) {
2 ✔
932
                    msg.setContent(multibody);
4 ✔
933
                } else if (body != null) {
2 !
934
                    msg.setText(firstContent, firstCharset, firstContentType);
5 ✔
935
                    if (firstEncoding != null) {
2 !
936
                        msg.setHeader("Content-Transfer-Encoding", firstEncoding);
4 ✔
937
                    }
938
                }
939

940
                msg.saveChanges();
2 ✔
941
                mails[i++] = msg;
5 ✔
942
            }
943
        }
944

945
        if (i != mailElements.length) {
4 !
946
            mails = Arrays.copyOf(mails, i);
×
947
        }
948

949
        return mails;
2 ✔
950
    }
951

952
    /**
953
     * Returns the current date and time in an RFC822 format, suitable for an email Date Header
954
     *
955
     * @return RFC822 formated date and time as a String
956
     */
957
    private static String getDateRFC822() {
958
        String dateString = "";
2 ✔
959
        final Calendar rightNow = Calendar.getInstance();
2 ✔
960

961
        //Day of the week
962
        dateString = switch (rightNow.get(Calendar.DAY_OF_WEEK)) {
4 !
963
            case Calendar.MONDAY -> "Mon";
×
964
            case Calendar.TUESDAY -> "Tue";
×
965
            case Calendar.WEDNESDAY -> "Wed";
×
966
            case Calendar.THURSDAY -> "Thu";
×
967
            case Calendar.FRIDAY -> "Fri";
2 ✔
968
            case Calendar.SATURDAY -> "Sat";
×
969
            case Calendar.SUNDAY -> "Sun";
×
970
            default -> dateString;
1 ✔
971
        };
972

973
        dateString += ", ";
3 ✔
974

975
        //Date
976
        dateString += rightNow.get(Calendar.DAY_OF_MONTH);
6 ✔
977
        dateString += " ";
3 ✔
978

979
        //Month
980
        switch (rightNow.get(Calendar.MONTH)) {
4 !
981
            case Calendar.JANUARY:
982
                dateString += "Jan";
×
983
                break;
×
984

985
            case Calendar.FEBRUARY:
986
                dateString += "Feb";
×
987
                break;
×
988

989
            case Calendar.MARCH:
990
                dateString += "Mar";
×
991
                break;
×
992

993
            case Calendar.APRIL:
994
                dateString += "Apr";
×
995
                break;
×
996

997
            case Calendar.MAY:
998
                dateString += "May";
×
999
                break;
×
1000

1001
            case Calendar.JUNE:
1002
                dateString += "Jun";
×
1003
                break;
×
1004

1005
            case Calendar.JULY:
1006
                dateString += "Jul";
×
1007
                break;
×
1008

1009
            case Calendar.AUGUST:
1010
                dateString += "Aug";
×
1011
                break;
×
1012

1013
            case Calendar.SEPTEMBER:
1014
                dateString += "Sep";
×
1015
                break;
×
1016

1017
            case Calendar.OCTOBER:
1018
                dateString += "Oct";
3 ✔
1019
                break;
1 ✔
1020

1021
            case Calendar.NOVEMBER:
1022
                dateString += "Nov";
×
1023
                break;
×
1024

1025
            case Calendar.DECEMBER:
1026
                dateString += "Dec";
×
1027
                break;
1028
        }
1029
        dateString += " ";
3 ✔
1030

1031
        //Year
1032
        dateString += rightNow.get(Calendar.YEAR);
6 ✔
1033
        dateString += " ";
3 ✔
1034

1035
        //Time
1036
        String tHour = Integer.toString(rightNow.get(Calendar.HOUR_OF_DAY));
5 ✔
1037
        if (tHour.length() == 1) {
4 !
1038
            tHour = "0" + tHour;
3 ✔
1039
        }
1040

1041
        String tMinute = Integer.toString(rightNow.get(Calendar.MINUTE));
5 ✔
1042
        if (tMinute.length() == 1) {
4 !
1043
            tMinute = "0" + tMinute;
×
1044
        }
1045

1046
        String tSecond = Integer.toString(rightNow.get(Calendar.SECOND));
5 ✔
1047
        if (tSecond.length() == 1) {
4 !
1048
            tSecond = "0" + tSecond;
×
1049
        }
1050

1051
        dateString += tHour + ":" + tMinute + ":" + tSecond + " ";
6 ✔
1052

1053
        //TimeZone Correction
1054
        final String tzSign;
1055
        String tzHours = "";
2 ✔
1056
        String tzMinutes = "";
2 ✔
1057

1058
        final TimeZone thisTZ = rightNow.getTimeZone();
3 ✔
1059
        int tzOffset = thisTZ.getOffset(rightNow.getTime().getTime()); //get timezone offset in milliseconds
6 ✔
1060
        tzOffset = (tzOffset / 1000); //convert to seconds
4 ✔
1061
        tzOffset = (tzOffset / 60); //convert to minutes
4 ✔
1062

1063
        //Sign
1064
        if (tzOffset > 1) {
3 !
1065
            tzSign = "+";
3 ✔
1066
        } else {
1067
            tzSign = "-";
×
1068
            tzOffset *= -1;
×
1069
        }
1070

1071
        //Calc Hours and Minutes?
1072
        if (tzOffset >= 60) {
3 !
1073
            //Minutes and Hours
1074
            tzHours += (tzOffset / 60); //hours
6 ✔
1075

1076
            // do we need to prepend a 0
1077
            if (tzHours.length() == 1) {
4 !
1078
                tzHours = "0" + tzHours;
3 ✔
1079
            }
1080

1081
            tzMinutes += (tzOffset % 60); //minutes
6 ✔
1082

1083
            // do we need to prepend a 0
1084
            if (tzMinutes.length() == 1) {
4 !
1085
                tzMinutes = "0" + tzMinutes;
4 ✔
1086
            }
1087
        } else {
1088
            //Just Minutes
1089
            tzHours = "00";
×
1090
            tzMinutes += tzOffset;
×
1091
            // do we need to prepend a 0
1092
            if (tzMinutes.length() == 1) {
×
1093
                tzMinutes = "0" + tzMinutes;
×
1094
            }
1095
        }
1096

1097
        dateString += tzSign + tzHours + tzMinutes;
6 ✔
1098

1099
        return dateString;
2 ✔
1100
    }
1101

1102
    /**
1103
     * Base64 Encodes a string (used for message subject).
1104
     *
1105
     * Access is package-private for unit testing purposes.
1106
     *
1107
     * @param str The String to encode
1108
     * @throws java.io.UnsupportedEncodingException if the encocding is unsupported
1109
     * @return The encoded String
1110
     */
1111
    static String encode64(final String str, final String charset) throws java.io.UnsupportedEncodingException {
1112
        String result = Base64.encodeBase64String(str.getBytes(charset));
5 ✔
1113
        result = result.replaceAll("\n", "?=\n =?" + charset + "?B?");
6 ✔
1114
        result = "=?" + charset + "?B?" + result + "?=";
4 ✔
1115
        return result;
2 ✔
1116
    }
1117

1118
    /**
1119
     * Base64 Encodes an email address
1120
     *
1121
     * @param str The email address as a String to encode
1122
     * @param charset the character set
1123
     * @throws java.io.UnsupportedEncodingException if the encocding is unsupported
1124
     * @return The encoded email address String
1125
     */
1126
    private static String encode64Address(final String str, final String charset) throws java.io.UnsupportedEncodingException {
1127
        final int idx = str.indexOf("<");
4 ✔
1128

1129
        final String result;
1130
        if (idx != -1) {
3 !
1131
            result = encode64(str.substring(0, idx), charset) + " " + str.substring(idx);
×
1132
        } else {
1133
            result = str;
2 ✔
1134
        }
1135

1136
        return result;
2 ✔
1137
    }
1138

1139
    /**
1140
     * A simple data class to represent an email attachment.
1141
     * <p>
1142
     * It doesn't do anything fancy, it just has private
1143
     * members and get and set methods.
1144
     * <p>
1145
     * Access is package-private for unit testing purposes.
1146
     */
1147
        record MailAttachment(String filename, String mimeType, String data) {
12 ✔
1148
    }
1149

1150
    /**
1151
     * A simple data class to represent an email.
1152
     * It doesn't do anything fancy, it just has private
1153
     * members and get and set methods.
1154
     *
1155
     * Access is package-private for unit testing purposes.
1156
     */
1157
    static class Mail {
2 ✔
1158
        private String from;                                                //Who is the mail from
1159
        private String replyTo;                                             //Who should you reply to
1160
        private final List<String> to = new ArrayList<>(1);    //Who is the mail going to
7 ✔
1161
        private List<String> cc;                                            //Carbon Copy to
1162
        private List<String> bcc;                                           //Blind Carbon Copy to
1163
        private String subject;                                             //Subject of the mail
1164
        private String text;                                                //Body text of the mail
1165
        private String xhtml;                                               //Body XHTML of the mail
1166
        private List<MailAttachment> attachments;                            //Any attachments
1167

1168
        //From
1169
        public void setFrom(final String from) {
1170
            this.from = from;
3 ✔
1171
        }
1 ✔
1172

1173
        public String getFrom() {
1174
            return this.from;
3 ✔
1175
        }
1176

1177
        //reply-to
1178
        public void setReplyTo(final String replyTo) {
1179
            this.replyTo = replyTo;
×
1180
        }
×
1181

1182
        public String getReplyTo() {
1183
            return replyTo;
3 ✔
1184
        }
1185

1186
        //To
1187
        public void addTo(final String to) {
1188
            this.to.add(to);
5 ✔
1189
        }
1 ✔
1190

1191
        public int countTo() {
1192
            return to.size();
4 ✔
1193
        }
1194

1195
        public String getTo(final int index) {
1196
            return to.get(index);
6 ✔
1197
        }
1198

1199
        public List<String> getTo() {
1200
            return to;
3 ✔
1201
        }
1202

1203
        //CC
1204
        public void addCC(final String cc) {
1205
            if (this.cc == null) {
×
1206
                this.cc = new ArrayList<>();
×
1207
            }
1208
            this.cc.add(cc);
×
1209
        }
×
1210

1211
        public int countCC() {
1212
            if (this.cc == null) {
3 !
1213
                return 0;
2 ✔
1214
            }
1215
            return cc.size();
×
1216
        }
1217

1218
        public String getCC(final int index) {
1219
            if (this.cc == null) {
×
1220
                throw new IndexOutOfBoundsException();
×
1221
            }
1222
            return cc.get(index);
×
1223
        }
1224

1225
        public List<String> getCC() {
1226
            if (this.cc == null) {
3 !
1227
                return Collections.EMPTY_LIST;
2 ✔
1228
            }
1229
            return cc;
×
1230
        }
1231

1232
        //BCC
1233
        public void addBCC(final String bcc) {
1234
            if (this.bcc == null) {
×
1235
                this.bcc = new ArrayList<>();
×
1236
            }
1237
            this.bcc.add(bcc);
×
1238
        }
×
1239

1240
        public int countBCC() {
1241
            if (this.bcc == null) {
3 !
1242
                return 0;
2 ✔
1243
            }
1244
            return bcc.size();
×
1245
        }
1246

1247
        public String getBCC(final int index) {
1248
            if (this.bcc == null) {
×
1249
                throw new IndexOutOfBoundsException();
×
1250
            }
1251
            return bcc.get(index);
×
1252
        }
1253

1254
        public List<String> getBCC() {
1255
            return Objects.requireNonNullElseGet(this.bcc, Collections::emptyList);
6 ✔
1256
        }
1257

1258
        //Subject
1259
        public void setSubject(final String subject) {
1260
            this.subject = subject;
3 ✔
1261
        }
1 ✔
1262

1263
        public String getSubject() {
1264
            return subject;
3 ✔
1265
        }
1266

1267
        //text
1268
        public void setText(final String text) {
1269
            this.text = text;
3 ✔
1270
        }
1 ✔
1271

1272
        public String getText() {
1273
            return text;
3 ✔
1274
        }
1275

1276
        //xhtml
1277
        public void setXHTML(final String xhtml) {
1278
            this.xhtml = xhtml;
3 ✔
1279
        }
1 ✔
1280

1281
        public String getXHTML() {
1282
            return xhtml;
3 ✔
1283
        }
1284

1285
        public void addAttachment(final MailAttachment ma) {
1286
            if (this.attachments == null) {
3 ✔
1287
                this.attachments = new ArrayList<>();
5 ✔
1288
            }
1289
            attachments.add(ma);
5 ✔
1290
        }
1 ✔
1291

1292
        public Iterator<MailAttachment> attachmentIterator() {
1293
            return (Iterator<MailAttachment>) Objects.requireNonNullElseGet(this.attachments, Collections::emptyList).iterator();
7 ✔
1294
        }
1295
    }
1296

1297
    private static boolean nonEmpty(@Nullable final String str) {
1298
        return str != null && !str.isEmpty();
9 !
1299
    }
1300

1301
    /**
1302
     * Creates a "quoted-string" of the parameter value
1303
     * if it contains a non-token value (See {@link #isNonToken(String)}),
1304
     * otherwise it returns the parameter value as is.
1305
     *
1306
     * Access is package-private for unit testing purposes.
1307
     *
1308
     * @param value parameter value.
1309
     *
1310
     * @return the quoted string parameter value, or the parameter value as is.
1311
     */
1312
    static String parameterValue(final String value) {
1313
        if (isNonToken(value)) {
3 ✔
1314
            return "\"" + value + "\"";
3 ✔
1315
        } else {
1316
            return value;
2 ✔
1317
        }
1318
    }
1319

1320
    /**
1321
     * Determines if the string contains SPACE, CTLs, or `tspecial` (special token)
1322
     * according to <a href="https://www.rfc-editor.org/rfc/rfc2045#section-5">RFC 2045 - Section 5</a>.
1323
     *
1324
     * @param str the string to test
1325
     *
1326
     * @return true if the string contains a non-token, false otherwise.
1327
     */
1328
    private static boolean isNonToken(final String str) {
1329
        return NON_TOKEN_PATTERN.matcher(str).matches();
5 ✔
1330
    }
1331

1332
    /**
1333
     * Produce a multi-part boundary string.
1334
     *
1335
     * Access is package-private for unit testing purposes.
1336
     *
1337
     * @param multipartInstance the number of this multipart instance.
1338
     *
1339
     * @return the multi-part boundary string.
1340
     */
1341
    private static String multipartBoundary(final int multipartInstance) {
1342
        return multipartBoundaryPrefix(multipartInstance) + "_" + nextRandomPositiveInteger() + "." + System.currentTimeMillis();
6 ✔
1343
    }
1344

1345
    /**
1346
     * Produce the prefix of a multi-part boundary string.
1347
     *
1348
     * Access is package-private for unit testing purposes.
1349
     *
1350
     * @param multipartInstance the number of this multipart instance.
1351
     *
1352
     * @return the multi-part boundary string prefix.
1353
     */
1354
    static String multipartBoundaryPrefix(final int multipartInstance) {
1355
        return "----=_mail.mime.boundary_" + multipartInstance;
3 ✔
1356
    }
1357

1358
    /**
1359
     * Generates a positive random integer.
1360
     *
1361
     * @return the integer
1362
     */
1363
    private static int nextRandomPositiveInteger() {
1364
        return RANDOM.nextInt() & Integer.MAX_VALUE;
5 ✔
1365
    }
1366
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc