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

evolvedbinary / elemental / 932

28 Apr 2025 01:10AM UTC coverage: 56.402% (-0.01%) from 56.413%
932

push

circleci

adamretter
[bugfix] Correct release process instructions

28446 of 55846 branches covered (50.94%)

Branch coverage included in aggregate %.

77456 of 131918 relevant lines covered (58.72%)

0.59 hits per line

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

28.03
/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
 * 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; version 2.1.
11
 *
12
 * This library is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15
 * Lesser General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Lesser General Public
18
 * License along with this library; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
 *
21
 * NOTE: Parts of this file contain code from 'The eXist-db Authors'.
22
 *       The original license header is included below.
23
 *
24
 * =====================================================================
25
 *
26
 * eXist-db Open Source Native XML Database
27
 * Copyright (C) 2001 The eXist-db Authors
28
 *
29
 * info@exist-db.org
30
 * http://www.exist-db.org
31
 *
32
 * This library is free software; you can redistribute it and/or
33
 * modify it under the terms of the GNU Lesser General Public
34
 * License as published by the Free Software Foundation; either
35
 * version 2.1 of the License, or (at your option) any later version.
36
 *
37
 * This library is distributed in the hope that it will be useful,
38
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
39
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
40
 * Lesser General Public License for more details.
41
 *
42
 * You should have received a copy of the GNU Lesser General Public
43
 * License along with this library; if not, write to the Free Software
44
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
45
 */
46
package org.exist.xquery.modules.mail;
47

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

59
import jakarta.activation.DataHandler;
60
import jakarta.mail.*;
61
import jakarta.mail.internet.*;
62
import jakarta.mail.util.ByteArrayDataSource;
63

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

76
import static org.exist.util.StringUtil.isNullOrEmpty;
77

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

91
    private static final Logger LOGGER = LogManager.getLogger(SendEmailFunction.class);
1✔
92
    private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
1✔
93

94
    private final static int MIME_BASE64_MAX_LINE_LENGTH = 76; //RFC 2045, page 24
95

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

101
    static final String ERROR_MSG_NON_MIME_CLIENT = "Error your mail client is not MIME Compatible";
102

103
    private static final Random RANDOM = new Random();
1✔
104

105
    public final static FunctionSignature deprecated = new FunctionSignature(
1✔
106
            new QName("send-email", MailModule.NAMESPACE_URI, MailModule.PREFIX),
107
            "Sends an email through the SMTP Server.",
108
            new SequenceType[]
109
                    {
110
                            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>."),
111
                            new FunctionParameterSequenceType("server", Type.STRING, Cardinality.ZERO_OR_ONE, "The SMTP server.  If empty, then it tries to use the local sendmail program."),
112
                            new FunctionParameterSequenceType("charset", Type.STRING, Cardinality.ZERO_OR_ONE, "The charset value used in the \"Content-Type\" message header (Defaults to UTF-8)")
113
                    },
114
            new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.ONE_OR_MORE, "true if the email message was successfully sent")
115
    );
116

117
    public final static FunctionSignature[] signatures = {
1✔
118
            new FunctionSignature(
119
                    new QName("send-email", MailModule.NAMESPACE_URI, MailModule.PREFIX),
120
                    "Sends an email using javax.mail messaging libraries.",
121
                    new SequenceType[]
122
                            {
123
                                    new FunctionParameterSequenceType("mail-handle", Type.LONG, Cardinality.EXACTLY_ONE, "The JavaMail session handle retrieved from mail:get-mail-session()"),
124
                                    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>.")
125
                            },
126
                    new SequenceType(Type.EMPTY_SEQUENCE, Cardinality.EMPTY_SEQUENCE)
127
            )
128
    };
129

130
    public SendEmailFunction(final XQueryContext context, final FunctionSignature signature) {
131
        super(context, signature);
×
132
    }
×
133

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

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

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

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

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

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

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

203
            final ValueSequence results = new ValueSequence();
×
204

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

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

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

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

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

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

253
        //Get a string of all recipients email addresses
254
        final StringBuilder recipients = new StringBuilder();
×
255

256
        for (final String recipient : allrecipients) {
×
257
            recipients.append(" ");
×
258

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

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

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

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

288
        return true;
×
289
    }
290

291
    private static class SMTPException extends Exception {
292
        private static final long serialVersionUID = 4859093648476395159L;
293

294
        public SMTPException(final String message) {
295
            super(message);
×
296
        }
×
297

298
        public SMTPException(final Throwable cause) {
299
            super(cause);
×
300
        }
×
301
    }
302

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

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

326
        String smtpResult;             //Holds the server Result code when an SMTP Command is executed
327

328
        final boolean[] sendMailResults = new boolean[mails.length];
×
329

330
        try (
331
                //Create a Socket and connect to the SMTP Server
332
                final Socket smtpSock = new Socket(smtpHost, smtpPort);
×
333

334
                //Create a Buffered Reader for the Socket
335
                final BufferedReader smtpIn = new BufferedReader(new InputStreamReader(smtpSock.getInputStream()));
×
336

337
                //Create an Output Writer for the Socket
338
                final PrintWriter smtpOut = new PrintWriter(new OutputStreamWriter(smtpSock.getOutputStream(), charset))) {
×
339

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

348
            //Say "HELO"
349
            smtpOut.print("HELO " + InetAddress.getLocalHost().getHostName() + "\r\n");
×
350
            smtpOut.flush();
×
351

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

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

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

372
            //all done, time to "QUIT"
373
            smtpOut.print("QUIT\r\n");
×
374
            smtpOut.flush();
×
375

376
        } catch (final IOException ioe) {
×
377
            LOGGER.error(ioe.getMessage(), ioe);
×
378
            throw new SMTPException(ioe);
×
379
        }
×
380

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

386
        return sendMailResults;
×
387
    }
388

389
    private boolean writeSMTPMessage(final Mail mail, final PrintWriter smtpOut, final BufferedReader smtpIn, final String charset) {
390
        try {
391
            String smtpResult;
392

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

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

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

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

438
            //SEND "DATA"
439
            smtpOut.print("DATA\r\n");
×
440
            smtpOut.flush();
×
441

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

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

454
            //Send the Message
455
            writeMessage(smtpOut, mail, true, charset);
×
456

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

468
        return true;
×
469
    }
470

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

485
        //write the message headers
486

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

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

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

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

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

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

514

515
        boolean multipartAlternative = false;
1✔
516
        int multipartInstanceCount = 0;
1✔
517
        final Deque<String> multipartBoundary = new ArrayDeque<>();
1✔
518

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

531
        //content type
532
        if (!multipartBoundary.isEmpty()) {
1✔
533
            //multipart message
534

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

537
            //Mime warning
538
            out.print(eol);
1✔
539
            out.print(ERROR_MSG_NON_MIME_CLIENT + eol);
1✔
540
            out.print(eol);
1✔
541

542
            out.print("--" + multipartBoundary.peekFirst() + eol);
1✔
543
        }
544

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

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

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

559
            //now send the txt message
560
            out.print(eol);
1✔
561
            out.print(aMail.getText() + eol);
1✔
562

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

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

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

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

592
                    out.print("--" + multipartBoundary.peekFirst() + eol);
1✔
593

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

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

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

613

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

622
                if (itAttachment.hasNext()) {
1✔
623
                    out.print("--" + multipartBoundary.peekFirst() + eol);
1✔
624
                }
625
            }
1✔
626

627
            // End multipart message
628
            out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
1✔
629
            multipartBoundary.removeFirst();
1✔
630
        }
631

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

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

663
        int i = 0;
×
664
        for (final Element mailElement : mailElements) {
×
665

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

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

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

723
                    //next node
724
                    child = child.getNextSibling();
×
725

726
                }
727
                mails[i++] = mail;
×
728
            }
729
        }
730

731
        if (i != mailElements.length) {
×
732
            mails = Arrays.copyOf(mails, i);
×
733
        }
734

735
        return mails;
×
736
    }
737

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

766
        Message[] mails = new Message[mailElements.length];
×
767

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

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

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

829
                                    final Element elementBodyPart = (Element) bodyPart;
×
830
                                    String content = null;
×
831
                                    String contentType = null;
×
832

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

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

862
                                        if (body != null && multibody == null) {
×
863
                                            multibody = new MimeMultipart("alternative");
×
864
                                            multibody.addBodyPart(body);
×
865
                                        }
866

867
                                        if (isNullOrEmpty(charset)) {
×
868
                                            charset = "UTF-8";
×
869
                                        }
870

871
                                        if (isNullOrEmpty(encoding)) {
×
872
                                            encoding = "quoted-printable";
×
873
                                        }
874

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

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

928
                    //next node
929
                    child = child.getNextSibling();
×
930
                }
931

932
                // Lost from
933
                if (!fromWasSet) {
×
934
                    msg.setFrom();
×
935
                }
936

937
                msg.setReplyTo(replyTo.toArray(new InternetAddress[0]));
×
938

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

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

968
                msg.saveChanges();
×
969
                mails[i++] = msg;
×
970
            }
971
        }
972

973
        if (i != mailElements.length) {
×
974
            mails = Arrays.copyOf(mails, i);
×
975
        }
976

977
        return mails;
×
978
    }
979

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

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

1001
        dateString += ", ";
1✔
1002

1003
        //Date
1004
        dateString += rightNow.get(Calendar.DAY_OF_MONTH);
1✔
1005
        dateString += " ";
1✔
1006

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

1013
            case Calendar.FEBRUARY:
1014
                dateString += "Feb";
×
1015
                break;
×
1016

1017
            case Calendar.MARCH:
1018
                dateString += "Mar";
×
1019
                break;
×
1020

1021
            case Calendar.APRIL:
1022
                dateString += "Apr";
1✔
1023
                break;
1✔
1024

1025
            case Calendar.MAY:
1026
                dateString += "May";
×
1027
                break;
×
1028

1029
            case Calendar.JUNE:
1030
                dateString += "Jun";
×
1031
                break;
×
1032

1033
            case Calendar.JULY:
1034
                dateString += "Jul";
×
1035
                break;
×
1036

1037
            case Calendar.AUGUST:
1038
                dateString += "Aug";
×
1039
                break;
×
1040

1041
            case Calendar.SEPTEMBER:
1042
                dateString += "Sep";
×
1043
                break;
×
1044

1045
            case Calendar.OCTOBER:
1046
                dateString += "Oct";
×
1047
                break;
×
1048

1049
            case Calendar.NOVEMBER:
1050
                dateString += "Nov";
×
1051
                break;
×
1052

1053
            case Calendar.DECEMBER:
1054
                dateString += "Dec";
×
1055
                break;
1056
        }
1057
        dateString += " ";
1✔
1058

1059
        //Year
1060
        dateString += rightNow.get(Calendar.YEAR);
1✔
1061
        dateString += " ";
1✔
1062

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

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

1074
        String tSecond = Integer.toString(rightNow.get(Calendar.SECOND));
1✔
1075
        if (tSecond.length() == 1) {
1!
1076
            tSecond = "0" + tSecond;
1✔
1077
        }
1078

1079
        dateString += tHour + ":" + tMinute + ":" + tSecond + " ";
1✔
1080

1081
        //TimeZone Correction
1082
        final String tzSign;
1083
        String tzHours = "";
1✔
1084
        String tzMinutes = "";
1✔
1085

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

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

1099
        //Calc Hours and Minutes?
1100
        if (tzOffset >= 60) {
1!
1101
            //Minutes and Hours
1102
            tzHours += (tzOffset / 60); //hours
1✔
1103

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

1109
            tzMinutes += (tzOffset % 60); //minutes
1✔
1110

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

1125
        dateString += tzSign + tzHours + tzMinutes;
1✔
1126

1127
        return dateString;
1✔
1128
    }
1129

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

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

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

1164
        return result;
1✔
1165
    }
1166

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

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

1196
        //From
1197
        public void setFrom(final String from) {
1198
            this.from = from;
1✔
1199
        }
1✔
1200

1201
        public String getFrom() {
1202
            return this.from;
1✔
1203
        }
1204

1205
        //reply-to
1206
        public void setReplyTo(final String replyTo) {
1207
            this.replyTo = replyTo;
×
1208
        }
×
1209

1210
        public String getReplyTo() {
1211
            return replyTo;
1✔
1212
        }
1213

1214
        //To
1215
        public void addTo(final String to) {
1216
            this.to.add(to);
1✔
1217
        }
1✔
1218

1219
        public int countTo() {
1220
            return to.size();
1✔
1221
        }
1222

1223
        public String getTo(final int index) {
1224
            return to.get(index);
1✔
1225
        }
1226

1227
        public List<String> getTo() {
1228
            return to;
×
1229
        }
1230

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

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

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

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

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

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

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

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

1289
        //Subject
1290
        public void setSubject(final String subject) {
1291
            this.subject = subject;
1✔
1292
        }
1✔
1293

1294
        public String getSubject() {
1295
            return subject;
1✔
1296
        }
1297

1298
        //text
1299
        public void setText(final String text) {
1300
            this.text = text;
1✔
1301
        }
1✔
1302

1303
        public String getText() {
1304
            return text;
1✔
1305
        }
1306

1307
        //xhtml
1308
        public void setXHTML(final String xhtml) {
1309
            this.xhtml = xhtml;
1✔
1310
        }
1✔
1311

1312
        public String getXHTML() {
1313
            return xhtml;
1✔
1314
        }
1315

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

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

1331
    private static boolean nonEmpty(@Nullable final String str) {
1332
        return str != null && !str.isEmpty();
1!
1333
    }
1334

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

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

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

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

1392
    /**
1393
     * Generates a positive random integer.
1394
     *
1395
     * @return the integer
1396
     */
1397
    private static int nextRandomPositiveInteger() {
1398
        return RANDOM.nextInt() & Integer.MAX_VALUE;
1✔
1399
    }
1400
}
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