You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中JakartaMail API获取邮件及信封内容问题求助

问题:Spring Boot中使用JakartaMail API获取IMAP邮件并返回客户端的问题

核心需求

  • 在Spring Boot应用中通过JakartaMail API,将IMAP服务器中的邮件(含信封项与内容)存入数组,返回给客户端展示
  • 当前仅能获取发件人、收件人、主题等信封项,且请求频繁超时

当前代码与问题

初始实现代码

@Async
public CompletableFuture<List<EmailResponseDto>> fetchEmails() {
    List<EmailResponseDto> emailResponseDtoList = new ArrayList<>();
    try {
        Properties prop = new Properties();
        prop.put("mail.store.protocol", "imaps");
        prop.put("mail.imaps.host", "imap.gmail.com");
        prop.put("mail.imaps.port", "993");
        prop.put("mail.imaps.ssl.enable", "true");
        prop.put("mail.imaps.ssl.protocols", "TLSv1.2");
        prop.put("mail.imaps.ssl.trust", "*");

        Session emailSession = Session.getInstance(prop, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication("username", "password");
            }
        });

        FetchProfile profile = new FetchProfile();
        profile.add(FetchProfile.Item.ENVELOPE);

        Store store = emailSession.getStore("imaps");
        store.connect("username", "password");

        Folder emailFolder = store.getFolder("INBOX");
        emailFolder.open(Folder.READ_ONLY);

        emailFolder.fetch(emailFolder.getMessages(), profile);

        for (Message message : emailFolder.getMessages()) {
            EmailResponseDto emailResponseDto = new EmailResponseDto();
            emailResponseDto.setSubject(message.getSubject());
            emailResponseDto.setFrom(message.getFrom()[0].toString());
            emailResponseDto.setSentDate(message.getSentDate());
            emailResponseDtoList.add(emailResponseDto);
        }

        emailFolder.close(false);
        store.close();

    } catch (MessagingException e) {
        e.printStackTrace();
    }
    return CompletableFuture.completedFuture(emailResponseDtoList);
}

问题:能收到响应但返回401状态码,API客户端提示“无响应体返回”;已使用异步函数,但即使限制邮件数量仍无法获取正常响应,不确定实现逻辑是否正确,是否需要将邮件内容写入文件。

补充异步获取发件人代码

@Async
public CompletableFuture<List<String>> getEmails(String username, String password) {
    SecurityContext context = SecurityContextHolder.getContext();
    return CompletableFuture.supplyAsync(() -> {
        SecurityContextHolder.setContext(context);

        EmailProtocol emailProtocol = new EmailProtocol(username, password);
        List<String> fromList = new ArrayList<>();
        Session imapSession = emailProtocol.ImapSession();

        try  {
            Store store = imapSession.getStore("imaps");
            store.connect(username, password);
            Folder inboxFolder = store.getFolder("INBOX");
            inboxFolder.open(Folder.READ_WRITE);

            Message[] messages = inboxFolder.getMessages();
            for (Message m : messages) {
                Address[] fromAddresses = m.getFrom();
                if (fromAddresses != null) {
                    for (Address address : fromAddresses) {
                        fromList.add(address.toString());
                    }
                }
            }
            inboxFolder.close(false);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return fromList;
    });
}

问题:使用@Async注解时无法获取邮件发件人地址。


问题分析与解决方案

1. 401无响应体问题处理

  • 认证失败排查:Gmail现在需要使用应用专用密码(开启2FA时),或切换至OAuth2认证方式,禁止直接使用账号密码(低安全性应用访问已被默认禁用)。
  • 异步响应处理:Controller层需正确接收CompletableFuture返回值,确保Spring能解析响应体,示例:
    @GetMapping
    public CompletableFuture<ResponseEntity<List<EmailResponseDto>>> getEmails() {
        return emailService.fetchEmails()
                .thenApply(ResponseEntity::ok)
                .exceptionally(ex -> ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
    }
    
  • 异常反馈:当前代码仅打印异常,需在catch块返回失败的CompletableFuture,让客户端收到错误信息:
    catch (MessagingException e) {
        e.printStackTrace();
        return CompletableFuture.failedFuture(e);
    }
    

2. 超时问题优化

  • 分页获取邮件:避免一次性拉取所有邮件,使用inboxFolder.getMessages(start, end)分页,比如每次取50封:
    int total = emailFolder.getMessageCount();
    int start = Math.max(1, total - page * size);
    int end = Math.max(1, total - (page-1)*size);
    Message[] messages = emailFolder.getMessages(start, end);
    
  • 添加超时配置:在Properties中设置连接、读取超时:
    prop.put("mail.imaps.connectiontimeout", "5000"); // 连接超时5秒
    prop.put("mail.imaps.timeout", "10000"); // 读取超时10秒
    prop.put("mail.imaps.writetimeout", "10000"); // 写入超时10秒
    
  • 连接复用:不要每次请求都创建新Session和Store,可引入IMAP连接池减少连接开销。

3. @Async注解失效问题

  • 开启异步支持:启动类必须添加@EnableAsync注解,否则异步逻辑不生效。
  • 跨类调用:异步方法不能与调用方在同一个类中,Spring AOP代理仅对跨类调用生效。
  • 安全上下文处理:无需手动传递SecurityContext,若需要线程间共享,使用DelegatingSecurityContextAsyncTaskExecutor自动处理,避免线程安全问题。

4. 获取邮件内容的正确流程

无需将邮件内容写入文件,直接解析存入DTO即可:

  • 扩展FetchProfile:添加CONTENT_INFO获取内容元数据,或直接调用message.getContent()解析:
    profile.add(FetchProfile.Item.CONTENT_INFO);
    // 解析内容
    if (message.isMimeType("text/plain")) {
        emailResponseDto.setContent((String) message.getContent());
    } else if (message.isMimeType("multipart/*")) {
        Multipart multipart = (Multipart) message.getContent();
        StringBuilder content = new StringBuilder();
        for (int i = 0; i < multipart.getCount(); i++) {
            BodyPart part = multipart.getBodyPart(i);
            if (part.isMimeType("text/plain")) {
                content.append(part.getContent().toString());
            }
        }
        emailResponseDto.setContent(content.toString());
    }
    

修正后的异步服务示例

@Service
public class EmailService {

    @Async
    public CompletableFuture<List<EmailResponseDto>> fetchEmails(String username, String password, int page, int size) {
        List<EmailResponseDto> dtoList = new ArrayList<>();
        Properties prop = new Properties();
        prop.put("mail.store.protocol", "imaps");
        prop.put("mail.imaps.host", "imap.gmail.com");
        prop.put("mail.imaps.port", "993");
        prop.put("mail.imaps.ssl.enable", "true");
        prop.put("mail.imaps.ssl.protocols", "TLSv1.2");
        prop.put("mail.imaps.ssl.trust", "*");
        prop.put("mail.imaps.connectiontimeout", "5000");
        prop.put("mail.imaps.timeout", "10000");

        Session session = Session.getInstance(prop, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(username, password);
            }
        });

        try (Store store = session.getStore("imaps")) {
            store.connect(username, password);
            try (Folder inbox = store.getFolder("INBOX")) {
                inbox.open(Folder.READ_ONLY);
                int total = inbox.getMessageCount();
                int start = Math.max(1, total - page * size);
                int end = Math.max(1, total - (page-1)*size);
                if (start > end) return CompletableFuture.completedFuture(dtoList);

                Message[] messages = inbox.getMessages(start, end);
                FetchProfile profile = new FetchProfile();
                profile.add(FetchProfile.Item.ENVELOPE);
                profile.add(FetchProfile.Item.CONTENT_INFO);
                inbox.fetch(messages, profile);

                for (Message msg : messages) {
                    EmailResponseDto dto = new EmailResponseDto();
                    dto.setSubject(msg.getSubject());
                    dto.setFrom(msg.getFrom()[0].toString());
                    dto.setSentDate(msg.getSentDate());

                    // 解析邮件内容
                    if (msg.isMimeType("text/plain")) {
                        dto.setContent((String) msg.getContent());
                    } else if (msg.isMimeType("multipart/*")) {
                        Multipart multipart = (Multipart) msg.getContent();
                        StringBuilder content = new StringBuilder();
                        for (int i = 0; i < multipart.getCount(); i++) {
                            BodyPart part = multipart.getBodyPart(i);
                            if (part.isMimeType("text/plain")) {
                                content.append(part.getContent().toString());
                            }
                        }
                        dto.setContent(content.toString());
                    }
                    dtoList.add(dto);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return CompletableFuture.failedFuture(e);
        }
        return CompletableFuture.completedFuture(dtoList);
    }
}

内容的提问来源于stack exchange,提问作者Kamrul Hassan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 16:55:07