GreenMailUtil.java

/*
 * Copyright (c) 2014 Wael Chatila / Icegreen Technologies. All Rights Reserved.
 * This software is released under the Apache license 2.0
 */
package com.icegreen.greenmail.util;

import com.icegreen.greenmail.user.GreenMailUser;
import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.mail.*;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.Properties;
import java.util.Random;

/**
 * @author Wael Chatila
 * @version $Id: $
 * @since Jan 29, 2006
 */
public class GreenMailUtil {
    private static final Logger log = LoggerFactory.getLogger(GreenMailUtil.class);
    /**
     * used internally for {@link #random()}
     */
    private static int generateCount = 0;
    private static final String GENERATE_SET = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPRSTUVWXYZ23456789";
    private static final int GENERATE_SET_SIZE = GENERATE_SET.length();
    private static final Random RANDOM = new Random();

    private GreenMailUtil() {
        //empty
    }

    /**
     * Writes the content of an input stream to an output stream
     *
     * @throws IOException on io error.
     */
    public static void copyStream(final InputStream src, OutputStream dest) throws IOException {
        byte[] buffer = new byte[1024];
        int read;
        while ((read = src.read(buffer)) > -1) {
            dest.write(buffer, 0, read);
        }
        dest.flush();
    }

    /**
     * Convenience method which creates a new {@link MimeMessage} from an input stream.
     *
     * @return the created mime message.
     */
    public static MimeMessage newMimeMessage(InputStream inputStream) {
        try {
            final Properties props = new Properties();
            props.setProperty("mail.mime.allowutf8", System.getProperty("mail.mime.allowutf8","true"));
            return new MimeMessage(Session.getDefaultInstance(props), inputStream);
        } catch (MessagingException e) {
            throw new IllegalArgumentException("Can not generate mime message for input stream " + inputStream, e);
        }
    }

    /**
     * Convenience method which creates a new {@link MimeMessage} from a string
     */
    public static MimeMessage newMimeMessage(String mailString) {
        return newMimeMessage(EncodingUtil.toStream(mailString, EncodingUtil.CHARSET_EIGHT_BIT_ENCODING));
    }

    public static boolean hasNonTextAttachments(Part m) {
        try {
            Object content = m.getContent();
            if (content instanceof MimeMultipart) {
                MimeMultipart mm = (MimeMultipart) content;
                for (int i = 0; i < mm.getCount(); i++) {
                    BodyPart p = mm.getBodyPart(i);
                    if (hasNonTextAttachments(p)) {
                        return true;
                    }
                }
                return false;
            } else {
                return !m.getContentType().trim().toLowerCase().startsWith("text");
            }
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Counts the number of lines.
     *
     * @param str the input string
     * @return Returns the number of lines terminated by '\n' in string
     */
    public static int getLineCount(String str) {
        if (null == str || str.isEmpty()) {
            return 0;
        }
        int count = 1;
        for (char c : str.toCharArray()) {
            if ('\n' == c) {
                count++;
            }
        }
        return count;
    }

    /**
     * @return The content part of an email (or a Part)
     * <p>
     * Note: You might have to use  MimeUtility.decodeText(contentPart)
     * on the result to decode the (possibly) quoted-printable encoded special characters.
     */
    public static String getBody(Part msg) {
        String all = getWholeMessage(msg);
        int i = all.indexOf("\r\n\r\n");
        return i < 0 ? "" /* empty body */ : all.substring(i + 4);
    }

    /**
     * @return The headers of an email (or a Part)
     */
    public static String getHeaders(Part msg) {
        String all = getWholeMessage(msg);
        int i = all.indexOf("\r\n\r\n");
        return i < 0 ? all : all.substring(0, i);
    }

    /**
     * @return The both header and body for an email (or a Part)
     */
    public static String getWholeMessage(Part msg) {
        try {
            ByteArrayOutputStream bodyOut = new ByteArrayOutputStream();
            msg.writeTo(bodyOut);
            return bodyOut.toString(EncodingUtil.EIGHT_BIT_ENCODING).trim();
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    public static byte[] getBodyAsBytes(Part msg) {
        return getBody(msg).getBytes(EncodingUtil.CHARSET_EIGHT_BIT_ENCODING);
    }

    public static byte[] getHeaderAsBytes(Part part) {
        return getHeaders(part).getBytes(EncodingUtil.CHARSET_EIGHT_BIT_ENCODING);
    }

    /**
     * @return same as {@link #getWholeMessage(jakarta.mail.Part)}
     */
    public static String toString(Part msg) {
        return getWholeMessage(msg);
    }


    /**
     * Generates a random generated password consisting of letters and digits
     * with a length variable between 5 and 8 characters long.
     * Passwords are further optimized for displays
     * that could potentially display the characters <i>1,l,I,0,O,Q</i> in a way
     * that a human could easily mix them up.
     *
     * @return the random string.
     */
    public static String random() {
        int nbrOfLetters = RANDOM.nextInt(3) + 5;
        return random(nbrOfLetters);
    }

    public static String random(int nbrOfLetters) {
        StringBuilder ret = new StringBuilder();
        for (/* empty */; nbrOfLetters > 0; nbrOfLetters--) {
            int pos = (RANDOM.nextInt(GENERATE_SET_SIZE) + (++generateCount)) % GENERATE_SET_SIZE;
            ret.append(GENERATE_SET.charAt(pos));
        }
        return ret.toString();
    }

    /**
     * Sends a text message using the default test setup for SMTP.
     *
     * @param to      the to address.
     * @param from    the from address.
     * @param subject the subject.
     * @param msg     the text message.
     * @see ServerSetupTest#SMTP
     */
    public static void sendTextEmailTest(String to, String from, String subject, String msg) {
        sendTextEmail(to, from, subject, msg, ServerSetupTest.SMTP);
    }

    /**
     * Sends a text message using the default test setup for SMTPS.
     *
     * @param to      the to address.
     * @param from    the from address.
     * @param subject the subject.
     * @param msg     the text message.
     * @see ServerSetupTest#SMTPS
     */
    public static void sendTextEmailSecureTest(String to, String from, String subject, String msg) {
        sendTextEmail(to, from, subject, msg, ServerSetupTest.SMTPS);
    }

    public static String getAddressList(Address[] addresses) {
        if (null == addresses) {
            return null;
        }
        StringBuilder ret = new StringBuilder();
        for (int i = 0; i < addresses.length; i++) {
            if (i > 0) {
                ret.append(", ");
            }
            ret.append(addresses[i].toString());
        }
        return ret.toString();
    }

    public static MimeMessage createTextEmail(String to, String from, String subject, String msg, final ServerSetup setup) {
        try {
            Session session = getSession(setup);

            MimeMessage mimeMessage = new MimeMessage(session);
            mimeMessage.setSubject(subject);
            mimeMessage.setSentDate(new Date());
            mimeMessage.setFrom(from);
            mimeMessage.setRecipients(Message.RecipientType.TO, to);

            mimeMessage.setText(msg);
            return mimeMessage;
        } catch (MessagingException e) {
            throw new IllegalArgumentException("Can not generate message", e);
        }
    }

    /**
     * Sends a text message using given server setup for SMTP.
     *
     * @param to      the to address.
     * @param from    the from address.
     * @param subject the subject.
     * @param msg     the test message.
     * @param setup   the SMTP setup.
     */
    public static void sendTextEmail(String to, String from, String subject, String msg, final ServerSetup setup) {
        sendMimeMessage(createTextEmail(to, from, subject, msg, setup));
    }

    /**
     * Send the message using the JavaMail session defined in the message
     *
     * @param mimeMessage Message to send
     */
    public static void sendMimeMessage(MimeMessage mimeMessage) {
        try {
            Transport.send(mimeMessage);
        } catch (MessagingException e) {
            throw new IllegalStateException("Can not send message " + mimeMessage, e);
        }
    }

    /**
     * Send the message using the JavaMail session defined in the message
     *
     * @param mimeMessage Message to send
     * @param username Username for authentication.
     * @param password Password for authentication.
     */
    public static void sendMimeMessage(MimeMessage mimeMessage, String username, String password) {
        try {
            Transport.send(mimeMessage, username, password);
        } catch (MessagingException e) {
            throw new IllegalStateException("Can not send message " + mimeMessage, e);
        }
    }

    /**
     * Send the message with the given attributes and the given body using the specified SMTP settings
     *
     * @param to          Destination address(es)
     * @param from        Sender address
     * @param subject     Message subject
     * @param body        Message content. May either be a MimeMultipart or another body that java mail recognizes
     * @param contentType MIME content type of body
     * @param serverSetup Server settings to use for connecting to the SMTP server
     */
    public static void sendMessageBody(String to, String from, String subject, Object body, String contentType, ServerSetup serverSetup) {
        try {
            Session smtpSession = getSession(serverSetup);
            MimeMessage mimeMessage = new MimeMessage(smtpSession);

            mimeMessage.setRecipients(Message.RecipientType.TO, to);
            mimeMessage.setFrom(from);
            mimeMessage.setSubject(subject);
            mimeMessage.setContent(body, contentType);
            sendMimeMessage(mimeMessage);
        } catch (MessagingException e) {
            throw new IllegalStateException("Can not send message", e);
        }
    }

    public static void sendAttachmentEmail(String to, String from,
                                           String subject, String msg, final byte[] attachment,
                                           final String contentType, final String filename,
                                           final String description, final ServerSetup setup) {
        MimeMultipart multiPart = createMultipartWithAttachment(msg, attachment, contentType, filename, description);
        sendMessageBody(to, from, subject, multiPart, null, setup);
    }

    /**
     * Create new multipart with a text part and an attachment
     *
     * @param msg         Message text
     * @param attachment  Attachment data
     * @param contentType MIME content type of body
     * @param filename    File name of the attachment
     * @param description Description of the attachment
     * @return New multipart
     */
    public static MimeMultipart createMultipartWithAttachment(String msg, final byte[] attachment, final String contentType,
                                                               final String filename, String description) {
        try {
            MimeMultipart multiPart = new MimeMultipart();

            MimeBodyPart textPart = new MimeBodyPart();
            multiPart.addBodyPart(textPart);
            textPart.setText(msg);

            MimeBodyPart binaryPart = new MimeBodyPart();
            multiPart.addBodyPart(binaryPart);

            DataSource ds = new DataSource() {
                @Override
                public InputStream getInputStream() {
                    return new ByteArrayInputStream(attachment);
                }

                @Override
                public OutputStream getOutputStream() throws IOException {
                    ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
                    byteStream.write(attachment);
                    return byteStream;
                }

                @Override
                public String getContentType() {
                    return contentType;
                }

                @Override
                public String getName() {
                    return filename;
                }
            };
            binaryPart.setDataHandler(new DataHandler(ds));
            binaryPart.setFileName(filename);
            binaryPart.setDescription(description);
            return multiPart;
        } catch (MessagingException e) {
            throw new IllegalArgumentException("Can not create multipart message with attachment", e);
        }
    }

    /**
     * Gets a JavaMail Session for given server type such as IMAP and additional props for JavaMail.
     *
     * @param setup     the setup type, such as <code>ServerSetup.IMAP</code>
     * @return the JavaMail session.
     */
    public static Session getSession(final ServerSetup setup) {
        return getSession(setup, null);
    }

    /**
     * Gets a JavaMail Session for given server type such as IMAP and additional props for JavaMail.
     *
     * @param setup     the setup type, such as <code>ServerSetup.IMAP</code>
     * @param mailProps additional mail properties.
     * @return the JavaMail session.
     */
    public static Session getSession(final ServerSetup setup, Properties mailProps) {
        return getSession(setup, mailProps, setup.isVerbose());
    }

    /**
     * Gets a JavaMail Session for given server type such as IMAP and additional props for JavaMail.
     *
     * @param setup     the setup type, such as <code>ServerSetup.IMAP</code>
     * @param mailProps additional mail properties.
     * @param debug sets JavaMail debug properties.
     * @return the JavaMail session.
     */
    public static Session getSession(final ServerSetup setup, Properties mailProps, boolean debug) {
        Properties props = setup.configureJavaMailSessionProperties(mailProps, debug);
        log.debug("Mail session properties are {}", props);
        return Session.getInstance(props, null);
    }

    /**
     * Sets a quota for a users.
     *
     * @param user  the user.
     * @param quota the quota.
     */
    public static void setQuota(final GreenMailUser user, final Quota quota) {
        Session session = GreenMailUtil.getSession(ServerSetupTest.IMAP);
        try {
            Store store = session.getStore("imap");
            store.connect(user.getEmail(), user.getPassword());
            try {
                ((QuotaAwareStore) store).setQuota(quota);
            } finally {
                store.close();
            }
        } catch (Exception ex) {
            throw new IllegalStateException("Can not set quota " + quota
                    + " for user " + user, ex);
        }
    }

    /**
     * Gets the quotas for the user.
     *
     * @param user      the user.
     * @param quotaRoot the quota root, eg 'INBOX'.
     * @return array of current quotas, or an empty array if not set.
     */
    public static Quota[] getQuota(final GreenMailUser user, final String quotaRoot) {
        Session session = GreenMailUtil.getSession(ServerSetupTest.IMAP);
        try {
            Store store = session.getStore("imap");
            store.connect(user.getEmail(), user.getPassword());
            try {
                return ((QuotaAwareStore) store).getQuota(quotaRoot);
            } finally {
                store.close();
            }
        } catch (Exception ex) {
            throw new IllegalStateException("Can not get quota for quota root "
                    + quotaRoot + " for user " + user, ex);
        }
    }
}