Подтвердить что ты не робот

JavaMail IMAP через SSL довольно медленно - Массовая выборка нескольких сообщений

В настоящее время я пытаюсь использовать JavaMail для получения электронной почты с серверов IMAP (Gmail и других). В принципе, мой код работает: я действительно могу получить заголовки, содержимое тела и так далее. Моя проблема заключается в следующем: при работе на сервере IMAP (без SSL) в основном требуется 1-2 мс для обработки сообщения. Когда я иду на сервер IMAPS (следовательно, с помощью SSL, например Gmail), я достигаю 250 м/сообщение. Я ТОЛЬКО измеряю время обработки сообщений (соединение, рукопожатие и т.д. НЕ принимаются во внимание).

Я знаю, что, поскольку это SSL, данные шифруются. Однако время для расшифровки не должно быть таким важным, не так ли?

Я попытался установить более высокое значение ServerCacheSize, более высокий connectionpoolsize, но у меня действительно заканчиваются идеи. Кто-нибудь столкнулся с этой проблемой? Решил ли это, что можно надеяться?

Я боюсь, что API JavaMail использует другое соединение каждый раз, когда он извлекает почту с сервера IMAPS (с использованием накладных расходов для рукопожатия...). Если да, существует ли способ отменить это поведение?

Вот мой код (хотя вполне стандартный), вызванный из класса Main():

 public static int connectTest(String SSL, String user, String pwd, String host) throws IOException,
                                                                               ProtocolException,
                                                                               GeneralSecurityException {

    Properties props = System.getProperties();
    props.setProperty("mail.store.protocol", SSL);
    props.setProperty("mail.imaps.ssl.trust", host);
    props.setProperty("mail.imaps.connectionpoolsize", "10");

    try {


        Session session = Session.getDefaultInstance(props, null);

        // session.setDebug(true);

        Store store = session.getStore(SSL);
        store.connect(host, user, pwd);      
        Folder inbox = store.getFolder("INBOX");

        inbox.open(Folder.READ_ONLY);                
        int numMess = inbox.getMessageCount();
        Message[] messages = inbox.getMessages();

        for (Message m : messages) {

            m.getAllHeaders();
            m.getContent();
        }

        inbox.close(false);
        store.close();
        return numMess;
    } catch (MessagingException e) {
        e.printStackTrace();
        System.exit(2);
    }
    return 0;
}

Спасибо заранее.

4b9b3361

Ответ 1

после большой работы и помощи со стороны людей в JavaMail, источник этой "медленности" - это поведение FETCH в API. В самом деле, как сказал pjaol, мы возвращаемся на сервер каждый раз, когда нам нужна информация (заголовок или содержимое сообщения) для сообщения.

Если FetchProfile позволяет нам распространять информацию заголовка или флаги для многих сообщений, получение содержимого нескольких сообщений НЕ возможно.

К счастью, мы можем написать свою собственную команду IMAP, чтобы избежать этого "ограничения" (это было сделано таким образом, чтобы избежать ошибок в памяти: выборка каждой почты в памяти в одной команде может быть довольно тяжелой).

Вот мой код:

import com.sun.mail.iap.Argument;
import com.sun.mail.iap.ProtocolException;
import com.sun.mail.iap.Response;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.protocol.BODY;
import com.sun.mail.imap.protocol.FetchResponse;
import com.sun.mail.imap.protocol.IMAPProtocol;
import com.sun.mail.imap.protocol.UID;

public class CustomProtocolCommand implements IMAPFolder.ProtocolCommand {
    /** Index on server of first mail to fetch **/
    int start;

    /** Index on server of last mail to fetch **/
    int end;

    public CustomProtocolCommand(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
        Argument args = new Argument();
        args.writeString(Integer.toString(start) + ":" + Integer.toString(end));
        args.writeString("BODY[]");
        Response[] r = protocol.command("FETCH", args);
        Response response = r[r.length - 1];
        if (response.isOK()) {
            Properties props = new Properties();
            props.setProperty("mail.store.protocol", "imap");
            props.setProperty("mail.mime.base64.ignoreerrors", "true");
            props.setProperty("mail.imap.partialfetch", "false");
            props.setProperty("mail.imaps.partialfetch", "false");
            Session session = Session.getInstance(props, null);

            FetchResponse fetch;
            BODY body;
            MimeMessage mm;
            ByteArrayInputStream is = null;

            // last response is only result summary: not contents
            for (int i = 0; i < r.length - 1; i++) {
                if (r[i] instanceof IMAPResponse) {
                    fetch = (FetchResponse) r[i];
                    body = (BODY) fetch.getItem(0);
                    is = body.getByteArrayInputStream();
                    try {
                        mm = new MimeMessage(session, is);
                        Contents.getContents(mm, i);
                    } catch (MessagingException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        // dispatch remaining untagged responses
        protocol.notifyResponseHandlers(r);
        protocol.handleResult(response);

        return "" + (r.length - 1);
    }
}

Функция getContents (MimeMessage mm, int i) является классической функцией, которая рекурсивно печатает содержимое сообщения в файл (многие примеры доступны в сети).

Чтобы избежать ошибок в памяти, я просто устанавливаю ограничение maxDocs и maxSize (это было сделано произвольно и, вероятно, возможно улучшено!), используется следующим образом:

public int efficientGetContents(IMAPFolder inbox, Message[] messages)
        throws MessagingException {
    FetchProfile fp = new FetchProfile();
    fp.add(FetchProfile.Item.FLAGS);
    fp.add(FetchProfile.Item.ENVELOPE);
    inbox.fetch(messages, fp);
    int index = 0;
    int nbMessages = messages.length;
    final int maxDoc = 5000;
    final long maxSize = 100000000; // 100Mo

    // Message numbers limit to fetch
    int start;
    int end;

    while (index < nbMessages) {
        start = messages[index].getMessageNumber();
        int docs = 0;
        int totalSize = 0;
        boolean noskip = true; // There are no jumps in the message numbers
                                           // list
        boolean notend = true;
        // Until we reach one of the limits
        while (docs < maxDoc && totalSize < maxSize && noskip && notend) {
            docs++;
            totalSize += messages[index].getSize();
            index++;
            if (notend = (index < nbMessages)) {
                noskip = (messages[index - 1].getMessageNumber() + 1 == messages[index]
                        .getMessageNumber());
            }
        }

        end = messages[index - 1].getMessageNumber();
        inbox.doCommand(new CustomProtocolCommand(start, end));

        System.out.println("Fetching contents for " + start + ":" + end);
        System.out.println("Size fetched = " + (totalSize / 1000000)
                + " Mo");

    }

    return nbMessages;
}

Не то, чтобы здесь я использую номера сообщений, которые нестабильны (они меняются, если сообщения удаляются с сервера). Лучшим методом было бы использовать UID! Затем вы измените команду FETCH на UID FETCH.

Надеюсь, что это поможет!

Ответ 2

Вам нужно добавить FetchProfile в папку "Входящие", прежде чем выполнять пересылку сообщений. Сообщение - ленивый объект загрузки, он будет возвращаться на сервер для каждого сообщения и для каждого которое не получает профиль по умолчанию. например.

for (Message message: messages) {
  message.getSubject(); //-> goes to the imap server to fetch the subject line
}

Если вы хотите отображать как список входящих сообщений, например, "От", "Тема", "Отправлено", "Прикрепить" и т.д., вы должны использовать что-то вроде следующего

    inbox.open(Folder.READ_ONLY);
    Message[] messages = inbox.getMessages(start + 1, total);

    FetchProfile fp = new FetchProfile();
    fp.add(FetchProfile.Item.ENVELOPE);
    fp.add(FetchProfileItem.FLAGS);
    fp.add(FetchProfileItem.CONTENT_INFO);

    fp.add("X-mailer");
    inbox.fetch(messages, fp); // Load the profile of the messages in 1 fetch.
    for (Message message: messages) {
       message.getSubject(); //Subject is already local, no additional fetch required
    }

Надеюсь, что это поможет.

Ответ 3

Общее время включает время, необходимое для криптографических операций. Криптографическим операциям нужна случайная сеялка. Существуют различные случайные реализации посева, которые предоставляют случайные биты для использования в криптографии. По умолчанию Java использует /dev/urandom, и это указано в вашем java.security, как показано ниже:

securerandom.source=file:/dev/urandom

В Windows java использует семантические функции Microsoft CryptoAPI, которые обычно не имеют проблем. Однако в unix и linux Java по умолчанию использует /dev/random для случайного посева. И операции чтения в /dev/random иногда блокируются и занимают много времени. Если вы используете платформы * nix, время, затраченное на это, будет подсчитываться в течение общего времени.

Так как я не знаю, какую платформу вы используете, я не могу точно сказать, что это может быть вашей проблемой. Но если вы, то это может быть одной из причин, по которым ваши операции занимают много времени. Одним из решений этого может быть использование /dev/urandom вместо/dev/random в качестве вашей случайной сеялки, которая не блокируется. Это можно указать с помощью системного свойства "java.security.egd". Например,

  -Djava.security.egd=file:/dev/urandom

Указание этого системного свойства переопределит параметр securerandom.source в файле java.security. Вы можете попробовать. Надеюсь, что это поможет.