All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.exist.xquery.modules.mail.SendEmailFunction Maven / Gradle / Ivy

/*
 * eXist-db Open Source Native XML Database
 * Copyright (C) 2001 The eXist-db Authors
 *
 * [email protected]
 * http://www.exist-db.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.exist.xquery.modules.mail;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.Version;
import org.exist.dom.QName;
import org.exist.util.MimeTable;
import org.exist.xquery.*;
import org.exist.xquery.value.*;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import jakarta.activation.DataHandler;
import jakarta.mail.*;
import jakarta.mail.internet.*;
import jakarta.mail.util.ByteArrayDataSource;

import javax.annotation.Nullable;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.util.*;
import java.util.regex.Pattern;

/**
 * eXist-db Mail Module Extension SendEmailFunction.
 *
 * The email sending functionality of the eXist-db Mail Module Extension that
 * allows email to be sent from XQuery using either SMTP or Sendmail.
 *
 * @author Adam Retter
 * @author Robert Walpole
 * @author Andrzej Taramina
 * @author José María Fernández
 */
public class SendEmailFunction extends BasicFunction {

    private static final Logger LOGGER = LogManager.getLogger(SendEmailFunction.class);
    private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();

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

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

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

    private static final Random RANDOM = new Random();

    public final static FunctionSignature deprecated = new FunctionSignature(
            new QName("send-email", MailModule.NAMESPACE_URI, MailModule.PREFIX),
            "Sends an email through the SMTP Server.",
            new SequenceType[]
                    {
                            new FunctionParameterSequenceType("email", Type.ELEMENT, Cardinality.ONE_OR_MORE, "The email message in the following format:            xs:base64Binary ."),
                            new FunctionParameterSequenceType("server", Type.STRING, Cardinality.ZERO_OR_ONE, "The SMTP server.  If empty, then it tries to use the local sendmail program."),
                            new FunctionParameterSequenceType("charset", Type.STRING, Cardinality.ZERO_OR_ONE, "The charset value used in the \"Content-Type\" message header (Defaults to UTF-8)")
                    },
            new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.ONE_OR_MORE, "true if the email message was successfully sent")
    );

    public final static FunctionSignature[] signatures = {
            new FunctionSignature(
                    new QName("send-email", MailModule.NAMESPACE_URI, MailModule.PREFIX),
                    "Sends an email using javax.mail messaging libraries.",
                    new SequenceType[]
                            {
                                    new FunctionParameterSequenceType("mail-handle", Type.LONG, Cardinality.EXACTLY_ONE, "The JavaMail session handle retrieved from mail:get-mail-session()"),
                                    new FunctionParameterSequenceType("email", Type.ELEMENT, Cardinality.ONE_OR_MORE, "The email message in the following format:            xs:base64Binary .")
                            },
                    new SequenceType(Type.ITEM, Cardinality.EMPTY_SEQUENCE)
            )
    };

    public SendEmailFunction(final XQueryContext context, final FunctionSignature signature) {
        super(context, signature);
    }

    @Override
    public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
        if (args.length == 3) {
            return deprecatedSendEmail(args, contextSequence);
        } else {
            return sendEmail(args, contextSequence);
        }
    }

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

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

        try {
            final Message[] messages = parseInputEmails(session, args[1]);
            String proto = session.getProperty("mail.transport.protocol");
            if (proto == null) {
                proto = "smtp";
            }
            try (final Transport t = session.getTransport(proto)) {
                if (session.getProperty("mail." + proto + ".auth") != null) {
                    t.connect(session.getProperty("mail." + proto + ".user"), session.getProperty("mail." + proto + ".password"));
                } else {
                    t.connect();
                }
                for (final Message msg : messages) {
                    t.sendMessage(msg, msg.getAllRecipients());
                }
            }

            return Sequence.EMPTY_SEQUENCE;
        } catch (final TransformerException e) {
            throw new XPathException(this, "Could not Transform XHTML Message Body: " + e.getMessage(), e);
        } catch (final MessagingException e) {
            throw new XPathException(this, "Could not send message(s): " + e.getMessage(), e);
        } catch (final IOException e) {
            throw new XPathException(this, "Attachment in some message could not be prepared: " + e.getMessage(), e);
        } catch (final Throwable t) {
            throw new XPathException(this, "Unexpected error from JavaMail layer (Is your message well structured?): " + t.getMessage(), t);
        }
    }

    public Sequence deprecatedSendEmail(final Sequence[] args, final Sequence contextSequence) throws XPathException {
        try {
            //get the charset parameter, default to UTF-8
            final String charset;
            if (!args[2].isEmpty()) {
                charset = args[2].getStringValue();
            } else {
                charset = "UTF-8";
            }

            // Parse the XML  Elements into mail Objects
            final int len = args[0].getItemCount();
            final Element[] mailElements = new Element[len];
            for (int i = 0; i < len; i++) {
                mailElements[i] = (Element) args[0].itemAt(i);
            }
            final Mail[] mails = parseMailElement(mailElements);

            final ValueSequence results = new ValueSequence();

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

                for (final boolean mailResult : mailResults) {
                    results.add(BooleanValue.valueOf(mailResult));
                }
            } else {
                for (final Mail mail : mails) {
                    final boolean result = sendBySendmail(mail, charset);
                    results.add(BooleanValue.valueOf(result));
                }
            }

            return results;
        } catch (final TransformerException | IOException e) {
            throw new XPathException(this, "Could not Transform XHTML Message Body: " + e.getMessage(), e);
        } catch (final SMTPException e) {
            throw new XPathException(this, "Could not send message(s)" + e.getMessage(), e);
        }
    }

    private Message[] parseInputEmails(final Session session, final Sequence arg) throws IOException, MessagingException, TransformerException {
        // Parse the XML  Elements into mail Objects
        final int len = arg.getItemCount();
        final Element[] mailElements = new Element[len];
        for (int i = 0; i < len; i++) {
            mailElements[i] = (Element) arg.itemAt(i);
        }
        return parseMessageElement(session, mailElements);
    }

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

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

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

        for (final String recipient : allrecipients) {
            recipients.append(" ");

            //Check format of to address does it include a name as well as the email address?
            if (recipient.contains("<")) {
                //yes, just add the email address
                recipients.append(recipient, recipient.indexOf("<") + 1, recipient.indexOf(">"));
            } else {
                //add the email address
                recipients.append(recipient);
            }
        }

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

            //Get a Buffered Print Writer to the Processes stdOut
            try (final PrintWriter out = new PrintWriter(new OutputStreamWriter(p.getOutputStream(), charset))) {
                //Send the Message
                writeMessage(out, mail, false, charset);
            }
        } catch (final IOException e) {
            LOGGER.error(e.getMessage(), e);
            return false;
        }

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

        return true;
    }

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

        public SMTPException(final String message) {
            super(message);
        }

        public SMTPException(final Throwable cause) {
            super(cause);
        }
    }

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

        if (smtpServerArg != null && !smtpServerArg.isEmpty()) {
            final int idx = smtpServerArg.indexOf(':');
            if (idx > -1) {
                smtpHost = smtpServerArg.substring(0, idx);
                smtpPort = Integer.parseInt(smtpServerArg.substring(idx + 1));
            } else {
                smtpHost = smtpServerArg;
            }
        }

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

        final boolean[] sendMailResults = new boolean[mails.length];

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

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

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

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

            //Say "HELO"
            smtpOut.print("HELO " + InetAddress.getLocalHost().getHostName() + "\r\n");
            smtpOut.flush();

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

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

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

            //all done, time to "QUIT"
            smtpOut.print("QUIT\r\n");
            smtpOut.flush();

        } catch (final IOException ioe) {
            LOGGER.error(ioe.getMessage(), ioe);
            throw new SMTPException(ioe);
        }

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

        return sendMailResults;
    }

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

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

            //Get "MAIL FROM:" response
            smtpResult = smtpIn.readLine();
            if (smtpResult == null) {
                LOGGER.error("Error - Unexpected null response to SMTP MAIL FROM");
                return false;
            }
            if (!smtpResult.startsWith("250")) {
                LOGGER.error("Error - SMTP MAIL FROM failed: {}", smtpResult);
                return false;
            }

            //RCPT TO should be issued for each to, cc and bcc recipient
            final List allrecipients = new ArrayList<>();
            allrecipients.addAll(mail.getTo());
            allrecipients.addAll(mail.getCC());
            allrecipients.addAll(mail.getBCC());

            for (final String recipient : allrecipients) {
                //Send "RCPT TO:"
                //Check format of to address does it include a name as well as the email address?
                if (recipient.contains("<")) {
                    //yes, just send the email address
                    smtpOut.print("RCPT TO:<" + recipient.substring(recipient.indexOf("<") + 1, recipient.indexOf(">")) + ">\r\n");
                } else {
                    smtpOut.print("RCPT TO:<" + recipient + ">\r\n");
                }
                smtpOut.flush();
                //Get "RCPT TO:" response
                smtpResult = smtpIn.readLine();
                if (!smtpResult.startsWith("250")) {
                    LOGGER.error("Error - SMTP RCPT TO failed: {}", smtpResult);
                }
            }

            //SEND "DATA"
            smtpOut.print("DATA\r\n");
            smtpOut.flush();

            //Get "DATA" response, should be "354 blah blah" (optionally preceded by "250 OK")
            smtpResult = smtpIn.readLine();
            if (smtpResult.startsWith("250")) {
                // should then be followed by "354 blah blah"
                smtpResult = smtpIn.readLine();
            }

            if (!smtpResult.startsWith("354")) {
                LOGGER.error("Error - SMTP DATA failed: {}", smtpResult);
                return false;
            }

            //Send the Message
            writeMessage(smtpOut, mail, true, charset);

            //Get end message response, should be "250 blah blah"
            smtpResult = smtpIn.readLine();
            if (!smtpResult.startsWith("250")) {
                LOGGER.error("Error - Message not accepted: {}", smtpResult);
                return false;
            }
        } catch (final IOException e) {
            LOGGER.error(e.getMessage(), e);
            return false;
        }

        return true;
    }

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

        //write the message headers

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

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

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

        for (int x = 0; x < aMail.countCC(); x++) {
            out.print("CC: " + encode64Address(aMail.getCC(x), charset) + eol);
        }

        for (int x = 0; x < aMail.countBCC(); x++) {
            out.print("BCC: " + encode64Address(aMail.getBCC(x), charset) + eol);
        }

        out.print("Date: " + getDateRFC822() + eol);
        String subject = aMail.getSubject();
        if (subject == null) {
            subject = "";
        }
        out.print("Subject: " + encode64(subject, charset) + eol);
        out.print("X-Mailer: eXist-db " + Version.getVersion() + " mail:send-email()" + eol);
        out.print("MIME-Version: 1.0" + eol);


        boolean multipartAlternative = false;
        int multipartInstanceCount = 0;
        final Deque multipartBoundary = new ArrayDeque<>();

        if (aMail.attachmentIterator().hasNext()) {
            // we have an attachment as well as text and/or html, so we need a multipart/mixed message
            multipartBoundary.addFirst(multipartBoundary(++multipartInstanceCount));
        } else if (nonEmpty(aMail.getText()) && nonEmpty(aMail.getXHTML())) {
            // we have text and html, so we need a multipart/alternative message and no attachment
            multipartAlternative = true;
            multipartBoundary.addFirst(multipartBoundary(++multipartInstanceCount));
        }
//        else {
//            // we have either text or html and no attachment this message is not multipart
//        }

        //content type
        if (!multipartBoundary.isEmpty()) {
            //multipart message

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

            //Mime warning
            out.print(eol);
            out.print(ERROR_MSG_NON_MIME_CLIENT + eol);
            out.print(eol);

            out.print("--" + multipartBoundary.peekFirst() + eol);
        }

        if (nonEmpty(aMail.getText()) && nonEmpty(aMail.getXHTML()) && aMail.attachmentIterator().hasNext()) {
            // we are a multipart inside a multipart
            multipartBoundary.addFirst(multipartBoundary(++multipartInstanceCount));

            out.print("Content-Type: multipart/alternative; boundary=" + parameterValue(multipartBoundary.peekFirst()) + eol);
            out.print(eol);
            out.print("--" + multipartBoundary.peekFirst() + eol);
        }

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

            //now send the txt message
            out.print(eol);
            out.print(aMail.getText() + eol);

            if (!multipartBoundary.isEmpty()) {
                if (nonEmpty(aMail.getXHTML()) || aMail.attachmentIterator().hasNext()) {
                    out.print("--" + multipartBoundary.peekFirst() + eol);
                } else {
                    // End multipart message
                    out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
                    multipartBoundary.removeFirst();
                }
            }
        }

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

            //now send the html message
            out.print(eol);
            out.print("" + eol);
            out.print(aMail.getXHTML() + eol);

            if (!multipartBoundary.isEmpty()) {
                if (aMail.attachmentIterator().hasNext()) {
                    if (nonEmpty(aMail.getText()) && nonEmpty(aMail.getXHTML()) && aMail.attachmentIterator().hasNext()) {
                        // End multipart message
                        out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
                        multipartBoundary.removeFirst();
                    }

                    out.print("--" + multipartBoundary.peekFirst() + eol);

                } else {
                    // End multipart message
                    out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
                    multipartBoundary.removeFirst();
                }
            }
        }

        //attachments
        if (aMail.attachmentIterator().hasNext()) {
            for (final Iterator itAttachment = aMail.attachmentIterator(); itAttachment.hasNext(); ) {
                final MailAttachment ma = itAttachment.next();

                out.print("Content-Type: " + ma.getMimeType() + "; name=" + parameterValue(ma.getFilename()) + eol);
                out.print("Content-Transfer-Encoding: base64" + eol);
                out.print("Content-Description: " + ma.getFilename() + eol);
                out.print("Content-Disposition: attachment; filename=" + parameterValue(ma.getFilename()) + eol);
                out.print(eol);


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

                if (itAttachment.hasNext()) {
                    out.print("--" + multipartBoundary.peekFirst() + eol);
                }
            }

            // End multipart message
            out.print("--" + multipartBoundary.peekFirst() + "--" + eol);
            multipartBoundary.removeFirst();
        }

        //end the message, .
        out.print(eol);
        out.print("." + eol);
        out.flush();
    }

    /**
     * Constructs a mail Object from an XML representation of an email
     * 

* The XML email Representation is expected to look something like this * * * * * * * * * * * * * * * @param mailElements The XML mail Node * @throws TransformerException if a transformation error occurs * @return A mail Object representing the XML mail Node */ private Mail[] parseMailElement(final Element[] mailElements) throws TransformerException, IOException { Mail[] mails = new Mail[mailElements.length]; int i = 0; for (final Element mailElement : mailElements) { //Make sure that message has a Mail node if ("mail".equals(mailElement.getLocalName())) { final Mail mail = new Mail(); //Get the First Child Node child = mailElement.getFirstChild(); while (child != null) { //Parse each of the child nodes if (Node.ELEMENT_NODE == child.getNodeType() && child.hasChildNodes()) { switch (child.getLocalName()) { case "from": mail.setFrom(child.getFirstChild().getNodeValue()); break; case "reply-to": mail.setReplyTo(child.getFirstChild().getNodeValue()); break; case "to": mail.addTo(child.getFirstChild().getNodeValue()); break; case "cc": mail.addCC(child.getFirstChild().getNodeValue()); break; case "bcc": mail.addBCC(child.getFirstChild().getNodeValue()); break; case "subject": mail.setSubject(child.getFirstChild().getNodeValue()); break; case "message": //If the message node, then parse the child text and xhtml nodes Node bodyPart = child.getFirstChild(); while (bodyPart != null) { if ("text".equals(bodyPart.getLocalName())) { mail.setText(bodyPart.getFirstChild().getNodeValue()); } else if ("xhtml".equals(bodyPart.getLocalName())) { //Convert everything inside to text final Transformer transformer = TRANSFORMER_FACTORY.newTransformer(); final DOMSource source = new DOMSource(bodyPart.getFirstChild()); try (final StringWriter strWriter = new StringWriter()) { final StreamResult result = new StreamResult(strWriter); transformer.transform(source, result); mail.setXHTML(strWriter.toString()); } } //next body part bodyPart = bodyPart.getNextSibling(); } break; case "attachment": final Element attachment = (Element) child; final MailAttachment ma = new MailAttachment(attachment.getAttribute("filename"), attachment.getAttribute("mimetype"), attachment.getFirstChild().getNodeValue()); mail.addAttachment(ma); break; } } //next node child = child.getNextSibling(); } mails[i++] = mail; } } if (i != mailElements.length) { mails = Arrays.copyOf(mails, i); } return mails; } /** * Constructs a mail Object from an XML representation of an email *

* The XML email Representation is expected to look something like this * * * * * * * * * * * * * * * * * @param mailElements The XML mail Node * @throws IOException if an I/O error occurs * @throws MessagingException if an email error occurs * @throws TransformerException if a transformation error occurs * @return A mail Object representing the XML mail Node */ private Message[] parseMessageElement(final Session session, final Element[] mailElements) throws IOException, MessagingException, TransformerException { Message[] mails = new Message[mailElements.length]; int i = 0; for (final Element mailElement : mailElements) { //Make sure that message has a Mail node if ("mail".equals(mailElement.getLocalName())) { //New message Object // create a message final MimeMessage msg = new MimeMessage(session); boolean fromWasSet = false; final List replyTo = new ArrayList<>(); MimeBodyPart body = null; Multipart multibody = null; final List attachments = new ArrayList<>(); String firstContent = null; String firstContentType = null; String firstCharset = null; String firstEncoding = null; //Get the First Child Node child = mailElement.getFirstChild(); while (child != null) { //Parse each of the child nodes if (Node.ELEMENT_NODE == child.getNodeType() && child.hasChildNodes()) { switch (child.getLocalName()) { case "from": // set the from and to address final InternetAddress[] addressFrom = { new InternetAddress(child.getFirstChild().getNodeValue()) }; msg.addFrom(addressFrom); fromWasSet = true; break; case "reply-to": // As we can only set the reply-to, not add them, let's keep // all of them in a list replyTo.add(new InternetAddress(child.getFirstChild().getNodeValue())); break; case "to": msg.addRecipient(Message.RecipientType.TO, new InternetAddress(child.getFirstChild().getNodeValue())); break; case "cc": msg.addRecipient(Message.RecipientType.CC, new InternetAddress(child.getFirstChild().getNodeValue())); break; case "bcc": msg.addRecipient(Message.RecipientType.BCC, new InternetAddress(child.getFirstChild().getNodeValue())); break; case "subject": msg.setSubject(child.getFirstChild().getNodeValue()); break; case "header": // Optional : You can also set your custom headers in the Email if you Want msg.addHeader(((Element) child).getAttribute("name"), child.getFirstChild().getNodeValue()); break; case "message": //If the message node, then parse the child text and xhtml nodes Node bodyPart = child.getFirstChild(); while (bodyPart != null) { if (Node.ELEMENT_NODE != bodyPart.getNodeType()) { continue; } final Element elementBodyPart = (Element) bodyPart; String content = null; String contentType = null; switch (bodyPart.getLocalName()) { case "text": // Setting the Subject and Content Type content = bodyPart.getFirstChild().getNodeValue(); contentType = "plain"; break; case "xhtml": //Convert everything inside to text final Transformer transformer = TRANSFORMER_FACTORY.newTransformer(); final DOMSource source = new DOMSource(bodyPart.getFirstChild()); try (final StringWriter strWriter = new StringWriter()) { final StreamResult result = new StreamResult(strWriter); transformer.transform(source, result); content = strWriter.toString(); } contentType = "html"; break; case "generic": // Setting the Subject and Content Type content = elementBodyPart.getFirstChild().getNodeValue(); contentType = elementBodyPart.getAttribute("type"); break; } // Now, time to store it if (content != null && contentType != null && !contentType.isEmpty()) { String charset = elementBodyPart.getAttribute("charset"); String encoding = elementBodyPart.getAttribute("encoding"); if (body != null && multibody == null) { multibody = new MimeMultipart("alternative"); multibody.addBodyPart(body); } if (StringUtils.isEmpty(charset)) { charset = "UTF-8"; } if (StringUtils.isEmpty(encoding)) { encoding = "quoted-printable"; } if (body == null) { firstContent = content; firstCharset = charset; firstContentType = contentType; firstEncoding = encoding; } body = new MimeBodyPart(); body.setText(content, charset, contentType); if (encoding != null) { body.setHeader("Content-Transfer-Encoding", encoding); } if (multibody != null) { multibody.addBodyPart(body); } } //next body part bodyPart = bodyPart.getNextSibling(); } break; case "attachment": final Element attachment = (Element) child; final MimeBodyPart part; // if mimetype indicates a binary resource, assume the content is base64 encoded if (MimeTable.getInstance().isTextContent(attachment.getAttribute("mimetype"))) { part = new MimeBodyPart(); } else { part = new PreencodedMimeBodyPart("base64"); } final StringBuilder content = new StringBuilder(); Node attachChild = attachment.getFirstChild(); while (attachChild != null) { if (Node.ELEMENT_NODE == attachChild.getNodeType()) { final Transformer transformer = TRANSFORMER_FACTORY.newTransformer(); final DOMSource source = new DOMSource(attachChild); try (final StringWriter strWriter = new StringWriter()) { final StreamResult result = new StreamResult(strWriter); transformer.transform(source, result); content.append(strWriter); } } else { content.append(attachChild.getNodeValue()); } attachChild = attachChild.getNextSibling(); } part.setDataHandler(new DataHandler(new ByteArrayDataSource(content.toString(), attachment.getAttribute("mimetype")))); part.setFileName(attachment.getAttribute("filename")); // part.setHeader("Content-Transfer-Encoding", "base64"); attachments.add(part); break; } } //next node child = child.getNextSibling(); } // Lost from if (!fromWasSet) { msg.setFrom(); } msg.setReplyTo(replyTo.toArray(new InternetAddress[0])); // Preparing content and attachments if (!attachments.isEmpty()) { if (multibody == null) { multibody = new MimeMultipart("mixed"); if (body != null) { multibody.addBodyPart(body); } } else { final MimeMultipart container = new MimeMultipart("mixed"); final MimeBodyPart containerBody = new MimeBodyPart(); containerBody.setContent(multibody); container.addBodyPart(containerBody); multibody = container; } for (final MimeBodyPart part : attachments) { multibody.addBodyPart(part); } } // And now setting-up content if (multibody != null) { msg.setContent(multibody); } else if (body != null) { msg.setText(firstContent, firstCharset, firstContentType); if (firstEncoding != null) { msg.setHeader("Content-Transfer-Encoding", firstEncoding); } } msg.saveChanges(); mails[i++] = msg; } } if (i != mailElements.length) { mails = Arrays.copyOf(mails, i); } return mails; } /** * Returns the current date and time in an RFC822 format, suitable for an email Date Header * * @return RFC822 formated date and time as a String */ private static String getDateRFC822() { String dateString = ""; final Calendar rightNow = Calendar.getInstance(); //Day of the week switch (rightNow.get(Calendar.DAY_OF_WEEK)) { case Calendar.MONDAY: dateString = "Mon"; break; case Calendar.TUESDAY: dateString = "Tue"; break; case Calendar.WEDNESDAY: dateString = "Wed"; break; case Calendar.THURSDAY: dateString = "Thu"; break; case Calendar.FRIDAY: dateString = "Fri"; break; case Calendar.SATURDAY: dateString = "Sat"; break; case Calendar.SUNDAY: dateString = "Sun"; break; } dateString += ", "; //Date dateString += rightNow.get(Calendar.DAY_OF_MONTH); dateString += " "; //Month switch (rightNow.get(Calendar.MONTH)) { case Calendar.JANUARY: dateString += "Jan"; break; case Calendar.FEBRUARY: dateString += "Feb"; break; case Calendar.MARCH: dateString += "Mar"; break; case Calendar.APRIL: dateString += "Apr"; break; case Calendar.MAY: dateString += "May"; break; case Calendar.JUNE: dateString += "Jun"; break; case Calendar.JULY: dateString += "Jul"; break; case Calendar.AUGUST: dateString += "Aug"; break; case Calendar.SEPTEMBER: dateString += "Sep"; break; case Calendar.OCTOBER: dateString += "Oct"; break; case Calendar.NOVEMBER: dateString += "Nov"; break; case Calendar.DECEMBER: dateString += "Dec"; break; } dateString += " "; //Year dateString += rightNow.get(Calendar.YEAR); dateString += " "; //Time String tHour = Integer.toString(rightNow.get(Calendar.HOUR_OF_DAY)); if (tHour.length() == 1) { tHour = "0" + tHour; } String tMinute = Integer.toString(rightNow.get(Calendar.MINUTE)); if (tMinute.length() == 1) { tMinute = "0" + tMinute; } String tSecond = Integer.toString(rightNow.get(Calendar.SECOND)); if (tSecond.length() == 1) { tSecond = "0" + tSecond; } dateString += tHour + ":" + tMinute + ":" + tSecond + " "; //TimeZone Correction final String tzSign; String tzHours = ""; String tzMinutes = ""; final TimeZone thisTZ = rightNow.getTimeZone(); int tzOffset = thisTZ.getOffset(rightNow.getTime().getTime()); //get timezone offset in milliseconds tzOffset = (tzOffset / 1000); //convert to seconds tzOffset = (tzOffset / 60); //convert to minutes //Sign if (tzOffset > 1) { tzSign = "+"; } else { tzSign = "-"; tzOffset *= -1; } //Calc Hours and Minutes? if (tzOffset >= 60) { //Minutes and Hours tzHours += (tzOffset / 60); //hours // do we need to prepend a 0 if (tzHours.length() == 1) { tzHours = "0" + tzHours; } tzMinutes += (tzOffset % 60); //minutes // do we need to prepend a 0 if (tzMinutes.length() == 1) { tzMinutes = "0" + tzMinutes; } } else { //Just Minutes tzHours = "00"; tzMinutes += tzOffset; // do we need to prepend a 0 if (tzMinutes.length() == 1) { tzMinutes = "0" + tzMinutes; } } dateString += tzSign + tzHours + tzMinutes; return dateString; } /** * Base64 Encodes a string (used for message subject). * * Access is package-private for unit testing purposes. * * @param str The String to encode * @throws java.io.UnsupportedEncodingException if the encocding is unsupported * @return The encoded String */ static String encode64(final String str, final String charset) throws java.io.UnsupportedEncodingException { String result = Base64.encodeBase64String(str.getBytes(charset)); result = result.replaceAll("\n", "?=\n =?" + charset + "?B?"); result = "=?" + charset + "?B?" + result + "?="; return result; } /** * Base64 Encodes an email address * * @param str The email address as a String to encode * @param charset the character set * @throws java.io.UnsupportedEncodingException if the encocding is unsupported * @return The encoded email address String */ private static String encode64Address(final String str, final String charset) throws java.io.UnsupportedEncodingException { final int idx = str.indexOf("<"); final String result; if (idx != -1) { result = encode64(str.substring(0, idx), charset) + " " + str.substring(idx); } else { result = str; } return result; } /** * A simple data class to represent an email attachment. * * It doesn't do anything fancy, it just has private * members and get and set methods. * * Access is package-private for unit testing purposes. */ static class MailAttachment { private final String filename; private final String mimeType; private final String data; public MailAttachment(final String filename, final String mimeType, final String data) { this.filename = filename; this.mimeType = mimeType; this.data = data; } public String getData() { return data; } public String getFilename() { return filename; } public String getMimeType() { return mimeType; } } /** * A simple data class to represent an email. * It doesn't do anything fancy, it just has private * members and get and set methods. * * Access is package-private for unit testing purposes. */ static class Mail { private String from; //Who is the mail from private String replyTo; //Who should you reply to private final List to = new ArrayList<>(1); //Who is the mail going to private List cc; //Carbon Copy to private List bcc; //Blind Carbon Copy to private String subject; //Subject of the mail private String text; //Body text of the mail private String xhtml; //Body XHTML of the mail private List attachments; //Any attachments //From public void setFrom(final String from) { this.from = from; } public String getFrom() { return this.from; } //reply-to public void setReplyTo(final String replyTo) { this.replyTo = replyTo; } public String getReplyTo() { return replyTo; } //To public void addTo(final String to) { this.to.add(to); } public int countTo() { return to.size(); } public String getTo(final int index) { return to.get(index); } public List getTo() { return to; } //CC public void addCC(final String cc) { if (this.cc == null) { this.cc = new ArrayList<>(); } this.cc.add(cc); } public int countCC() { if (this.cc == null) { return 0; } return cc.size(); } public String getCC(final int index) { if (this.cc == null) { throw new IndexOutOfBoundsException(); } return cc.get(index); } public List getCC() { if (this.cc == null) { return Collections.EMPTY_LIST; } return cc; } //BCC public void addBCC(final String bcc) { if (this.bcc == null) { this.bcc = new ArrayList<>(); } this.bcc.add(bcc); } public int countBCC() { if (this.bcc == null) { return 0; } return bcc.size(); } public String getBCC(final int index) { if (this.bcc == null) { throw new IndexOutOfBoundsException(); } return bcc.get(index); } public List getBCC() { if (this.bcc == null) { return Collections.EMPTY_LIST; } return bcc; } //Subject public void setSubject(final String subject) { this.subject = subject; } public String getSubject() { return subject; } //text public void setText(final String text) { this.text = text; } public String getText() { return text; } //xhtml public void setXHTML(final String xhtml) { this.xhtml = xhtml; } public String getXHTML() { return xhtml; } public void addAttachment(final MailAttachment ma) { if (this.attachments == null) { this.attachments = new ArrayList<>(); } attachments.add(ma); } public Iterator attachmentIterator() { if (this.attachments == null) { return Collections.EMPTY_LIST.iterator(); } return attachments.iterator(); } } private static boolean nonEmpty(@Nullable final String str) { return str != null && !str.isEmpty(); } /** * Creates a "quoted-string" of the parameter value * if it contains a non-token value (See {@link #isNonToken(String)}), * otherwise it returns the parameter value as is. * * Access is package-private for unit testing purposes. * * @param value parameter value. * * @return the quoted string parameter value, or the parameter value as is. */ static String parameterValue(final String value) { if (isNonToken(value)) { return "\"" + value + "\""; } else { return value; } } /** * Determines if the string contains SPACE, CTLs, or `tspecial` (special token) * according to RFC 2045 - Section 5. * * @param str the string to test * * @return true if the string contains a non-token, false otherwise. */ private static boolean isNonToken(final String str) { return NON_TOKEN_PATTERN.matcher(str).matches(); } /** * Produce a multi-part boundary string. * * Access is package-private for unit testing purposes. * * @param multipartInstance the number of this multipart instance. * * @return the multi-part boundary string. */ private static String multipartBoundary(final int multipartInstance) { return multipartBoundaryPrefix(multipartInstance) + "_" + nextRandomPositiveInteger() + "." + System.currentTimeMillis(); } /** * Produce the prefix of a multi-part boundary string. * * Access is package-private for unit testing purposes. * * @param multipartInstance the number of this multipart instance. * * @return the multi-part boundary string prefix. */ static String multipartBoundaryPrefix(final int multipartInstance) { return "----=_mail.mime.boundary_" + multipartInstance; } /** * Generates a positive random integer. * * @return the integer */ private static int nextRandomPositiveInteger() { return RANDOM.nextInt() & Integer.MAX_VALUE; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy