SSL上的JavaMail IMAP非常慢 - 批量获取多条消息

Posted

技术标签:

【中文标题】SSL上的JavaMail IMAP非常慢 - 批量获取多条消息【英文标题】:JavaMail IMAP over SSL quite slow - Bulk fetching multiple messages 【发布时间】:2012-01-09 12:07:51 【问题描述】:

我目前正在尝试使用 JavaMail 从 IMAP 服务器(Gmail 和其他服务器)获取电子邮件。基本上,我的代码有效:我确实可以获得标题、正文内容等。我的问题如下:在 IMAP 服务器(无 SSL)上工作时,处理一条消息基本上需要 1-2 毫秒。当我使用 IMAPS 服务器(因此使用 SSL,例如 Gmail)时,我达到大约 250m/条消息。我只测量处理消息的时间(不考虑连接、握手等)。

我知道因为这是 SSL,所以数据是加密的。不过,解密的时间应该没那么重要吧?

我尝试设置更高的 ServerCacheSize 值、更高的 connectionpoolsize,但我的想法严重不足。有人遇到过这个问题吗?希望解决它?

我担心 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”。另外,我已经阅读了***.com/questions/2538481/javamail-performance 的问题,但在不是 Gmail 的 IMAPS 服务器上进行了尝试,并且仍然得到相同的结果。 同一 IMAP/IMAPS 服务器上的其他客户端(Thunderbird、Outlook、what-have-you)是否也会发生这种情况?在这种情况下,这不是你的代码的错,而是服务器的问题。 如何测量 Thunderbird 导入消息所需的时间? (我们在ms区...)。它在 20 秒内对所有文件夹收费(但我不知道它是否只获得了一些信息,当我点击消息时获得其余信息)。 嗯,这有点麻烦,是的。我相信它有某种操作日志(默认关闭),但分辨率最多以秒为单位。您可以配置 TB 下载完整的邮件(默认只获取邮件头),然后测量整个收件箱;这至少应该告诉你它需要 你的底层操作系统是什么? 【参考方案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 上运行,我会尝试您的建议并及时发布。 不幸的是,我的 java.security 文件已经包含以下行:securerandom.source=file:/dev/urandom 尝试将以下属性(带有 3 ///)显式添加到您的 JVM -Djava.security.egd=file:///dev/urandom。无法识别上面的语法(带 1 /)。【参考方案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
    

希望对您有所帮助。

【讨论】:

FetchProfile 有点帮助,谢谢!但是,我现在正在尝试批量获取多条消息的消息内容(直接使用 JavaMail API 时这是不可能的)。鉴于已修复大小限制以避免内存不足错误,这将带来更大的性能提升。 使用 FetchProfile 接收 25 条消息需要多长时间?对我来说,这需要将近 4 到 5 秒。【参考方案3】:

经过大量工作以及 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 时遇到了一些问题,它只是没有获取所有邮件。 @Justmaker:这个很好的答案是(IMO)或多或少正是我需要解决我的抓取问题的解决方案:***.com/questions/28166182/…。但是,我有一个问题:我看到您使用 `args.writeString("BODY[]");' 来获取每条消息的整个正文部分。例如,如果我想拥有 uid 为 16 的消息的正文部分 1.3 加上 uid 为 17 的消息的正文部分 2.1,那么论点应该是什么样子...... 在您的 CustomProtocolCommand 类中,doCommand 您正在使用方法 Contents.getContents(mm, i); ,我找不到,要导入什么库才能使此方法起作用,请将必需的导入添加到您的类中,谢谢。 我还注意到 body = (BODY) fetch.getItem(0);可能会导致转换异常错误,因为似乎在某些服务器中 getItem(0) 是 UID,尝试将 body = (BODY) fetch.getItem(BODY.class);相反,对我有用,如果你想要 UID,那么 fetch.getItem(UID.class); 我已经添加了必要的导入,它们应该在 JavaMail 库中。但是 Contents.getContents 实际上是您需要实现的方法。网上有很多例子,大多数是测试类型知道如何处理内容:***.com/questions/11240368/…。是的,你对 getItem 的看法是对的,我没有更新我现在有点旧的答案;)

以上是关于SSL上的JavaMail IMAP非常慢 - 批量获取多条消息的主要内容,如果未能解决你的问题,请参考以下文章

Javamail,Transport.send() 非常慢

IMAP 读取含有附件邮件超慢问题

IMAP:(JavaMail)UIDVALIDTY 值总是更改某些文件夹

JavaMail 使用 IMAP 读取最近的未读邮件

Javamail发信和收信机制(smtppop3imap)

javamail接收邮件报错