JavaMail通过SSL协议访问IMAP邮箱速度较慢 - 批量获取多封邮件

21

我目前正在尝试使用JavaMail从IMAP服务器(包括Gmail和其他邮件)获取电子邮件。基本上,我的代码是可用的:我确实可以获取邮件的头部、正文内容等等。我的问题是:在没有SSL的IMAP服务器上操作时,处理一条信息基本上需要1-2ms。而当我使用IMAPS服务器(例如Gmail)时,我会达到大约250毫秒/消息。我仅在处理邮件时测量时间(连接、握手等不计入时间)。

我知道,由于这是SSL,数据是加密的。但解密所需的时间应该不会那么重要,是吗?

我尝试设置更高的ServerCacheSize值、更高的连接池大小,但现在已经没有更多的想法了。有人遇到过这个问题吗?或许解决了呢?

我的担忧是,JavaMail API每次从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;
}

提前致谢。


注意:字符串SSL可以是“imap”或“imaps”。 此外,我已经阅读了https://dev59.com/8UzSa4cB1Zd3GeqPqNyK问题,但在一个不是Gmail的IMAPS服务器上尝试过,仍然得到相同的结果。 - Justmaker
我如何测量Thunderbird导入邮件所需的时间?(我们在毫秒级别...)。 它在不到20秒的时间内加载了所有文件夹(但我不知道它是否只获取了一些信息,并在我单击消息时获取其余部分)。 - Justmaker
嗯,这确实有点麻烦。我相信它有某种操作日志(默认关闭),但分辨率最多只能到秒级别。您可以配置TB下载完整的邮件(默认仅获取标题),然后测量整个收件箱;这至少可以显示它是否需要<1秒或多秒的时间。 - Piskvor left the building
Ubuntu 11.04 @Piskvor:仍在努力分析日志,以便给您一个准确的答案。 - Justmaker
好的,抱歉让您等待(Thunderbird日志默认不激活时间戳...)。对于在Thunderbird下获取邮件正文内容的一条消息,以下是时间戳: 2011-11-30 08:47:10.360004 UTC(开始获取) ... 2011-11-30 08:47:10.360922 UTC - -1989445888 [7f5b7e04a150]:7dcba000:imap.googlemail.com:S-INBOX:STREAM:CLOSE:Normal Message End Download Stream 2011-11-30 08:47:10.385466 UTC - -1989445888 [7f5b7e04a150]:ReadNextLine [stream=8cdc70e0 nb=56 needmore=0]因此,获取邮件正文内容要么需要25毫秒,要么需要1毫秒... - Justmaker
显示剩余3条评论
3个回答

28

经过大量的努力,以及JavaMail团队的帮助,这种“缓慢”的源头来自API中的FETCH行为。确实,就像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。

希望这能帮到您!


我在使用UID FETCH时遇到了一些问题,它没有获取到所有的邮件。 - Saša
在你的CustomProtocolCommand类中,你正在使用doCommand方法Contents.getContents(mm, i);我找不到需要导入哪个库才能使该方法正常工作,请在你的类中添加必要的导入,谢谢。 - vasilevich
我还注意到 body = (BODY) fetch.getItem(0); 可能会导致转换异常错误,因为在某些服务器中 getItem(0) 是 UID,请尝试使用 body = (BODY) fetch.getItem(BODY.class); 代替,这对我有效,并且如果您想要 UID,则使用 fetch.getItem(UID.class);。 - vasilevich
@Justmaker 非常感谢。这很不错。但是我需要进行一些小修改。所有的消息都被标记为已读,我认为是这行代码 Response[] r = protocol.command("FETCH", args); Response response = r[r.length - 1]; 的问题。我只需要在收到某个人的电子邮件时将该消息标记为已读。我在 Contents.getContents(mm, i) 方法中有这个逻辑。我尝试首先使用 message.setFlag(Flags.Flag.SEEN, false); 将消息设置为未读,然后如果满足条件则将消息标记为已读。但它不起作用。我需要做任何更新吗? - user525146
加载25条信息需要多少时间? - Rose
显示剩余3条评论

17

在迭代邮件之前,您需要向收件箱添加FetchProfile。Message 是一个延迟加载对象,它将为每个消息和每个未提供默认配置文件的字段返回到服务器。 例如:

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
    }

希望那能有所帮助。


FetchProfile 帮了一点忙,谢谢!但是现在我正在尝试批量获取多个消息的消息内容(当直接使用 JavaMail API 时不可能)。这将带来更大的性能提升,因为固定了大小限制以避免内存错误。 - Justmaker
使用FetchProfile接收25条消息需要多长时间?对我来说,大约需要4到5秒钟。 - Rose

1
总时间包括加密操作所需的时间。加密操作需要一个随机种子生成器。有不同的随机种子实现,可以提供用于密码学的随机位。默认情况下,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

指定此系统属性将覆盖java.security文件中的securerandom.source设置。你可以试试看。希望能有所帮助。

我确实在运行Ubuntu 11.04,我会尝试你建议的并及时向你反馈。 - Justmaker
很遗憾,我的java.security文件已经有以下行:securerandom.source=file:/dev/urandom - Justmaker
尝试将以下属性(使用3个///)显式添加到您的JVM中:-Djava.security.egd=file:///dev/urandom。上面的语法(使用1个/)不被识别。 - Drona

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接