首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 讀取郵件實戰:JavaMail API 支援 POP3/IMAP、多部分解析、附件下載與 OAuth2 現代認證

     編輯:Java 相關知識

企業級應用常需整合郵件系統 (自動化工單、帳單解析、安全審計)。本文基於 Jakarta Mail (原 JavaMail) 完整示範 POP3/IMAP 連接、郵件結構樹遞歸解析、文本/HTML/附件提取、大附件流式存儲、OAuth2 現代認證 (Gmail/Outlook) 與連接池化最佳實踐。

依賴坐標 (Jakarta EE 9+)




    com.sun.mail
    jakarta.mail
    2.0.1



    com.google.auth
    google-auth-library-oauth2-http
    1.19.0

核心配置類


@ConfigurationProperties(prefix = "mail")
public class MailConfig {
    private String host = "imap.gmail.com";
    private int port = 993;
    private String protocol = "imaps"; // pop3s / imaps
    private String username;
    private String password;           // 應用專用密碼或 OAuth2 token
    private boolean sslEnable = true;
    private int connectionTimeout = 30000;
    private int readTimeout = 60000;
    // getter/setter 省略
}

郵件讀取服務


@Service
public class MailReaderService {
    private final MailConfig config;
    private final Session session;

    public MailReaderService(MailConfig config) {
        this.config = config;
        Properties props = new Properties();
        props.put("mail.store.protocol", config.getProtocol());
        props.put("mail." + config.getProtocol() + ".host", config.getHost());
        props.put("mail." + config.getProtocol() + ".port", config.getPort());
        props.put("mail." + config.getProtocol() + ".ssl.enable", config.isSslEnable());
        props.put("mail." + config.getProtocol() + ".connectiontimeout", config.getConnectionTimeout());
        props.put("mail." + config.getProtocol() + ".timeout", config.getReadTimeout());
        // IMAP 特有:支援 IDLE 推送、部分獲取
        if ("imaps".equals(config.getProtocol())) {
            props.put("mail.imap.partialfetch", true);
            props.put("mail.imap.fetchsize", 1024 * 1024); // 1MB 分塊獲取
        }
        this.session = Session.getInstance(props);
    }

    public List fetchUnread(int maxCount) throws MessagingException {
        try (Store store = session.getStore(config.getProtocol())) {
            store.connect(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
            try (Folder folder = store.getFolder("INBOX")) {
                folder.open(Folder.READ_WRITE); // 需標記已讀
                // 搜尋未讀郵件
                FlagTerm unreadTerm = new FlagTerm(new Flags(Flags.Flag.SEEN), false);
                Message[] messages = folder.search(unreadTerm);
                return Arrays.stream(messages)
                        .limit(maxCount)
                        .map(this::parseMessage)
                        .collect(Collectors.toList());
            }
        }
    }

    private EmailMessage parseMessage(Message msg) throws MessagingException, IOException {
        EmailMessage email = new EmailMessage();
        email.setMessageId(msg.getMessageID());
        email.setSubject(MimeUtility.decodeText(msg.getSubject()));
        email.setFrom(Arrays.stream(msg.getFrom()).map(Object::toString).collect(Collectors.joining(", ")));
        email.setSentDate(msg.getSentDate());
        email.setFlags(Arrays.stream(msg.getFlags().getSystemFlags()).map(Flags.Flag::toString).collect(Collectors.toSet()));

        // 遞歸解析多部分結構
        parsePart(msg, email);
        return email;
    }

    private void parsePart(Part part, EmailMessage email) throws MessagingException, IOException {
        String contentType = part.getContentType();
        String disposition = part.getDisposition();

        if (part.isMimeType("text/plain")) {
            email.setTextBody((String) part.getContent());
        } else if (part.isMimeType("text/html")) {
            email.setHtmlBody((String) part.getContent());
        } else if (Part.ATTACHMENT.equalsIgnoreCase(disposition) || part.getFileName() != null) {
            // 附件處理
            String fileName = MimeUtility.decodeText(part.getFileName());
            try (InputStream is = part.getInputStream()) {
                // 大附件直接流式寫入磁碟/對象存儲,避免載入記憶體
                Path tempFile = Files.createTempFile("mail_attach_", "_" + fileName);
                Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING);
                email.addAttachment(new Attachment(fileName, part.getContentType(), tempFile.toFile()));
            }
        } else if (contentType != null && contentType.startsWith("multipart/")) {
            // 遞歸處理 multipart/mixed, multipart/alternative, multipart/related
            Multipart mp = (Multipart) part.getContent();
            for (int i = 0; i < mp.getCount(); i++) {
                parsePart(mp.getBodyPart(i), email);
            }
        }
    }
}

OAuth2 現代認證 (Gmail/Outlook)


public class OAuth2Authenticator {
    private static final String GMAIL_TOKEN_URL = "https://oauth2.googleapis.com/token";

    public static String getAccessToken(String clientId, String clientSecret, String refreshToken) throws IOException {
        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(GMAIL_TOKEN_URL))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(
                        "client_id=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8) +
                        "&client_secret=" + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) +
                        "&refresh_token=" + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) +
                        "&grant_type=refresh_token"))
                .build();
        String resp = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body();
        // 解析 JSON 取 access_token
        return JsonParser.parseString(resp).getAsJsonObject().get("access_token").getAsString();
    }

    // 連接時使用 XOAUTH2
    public static void connectWithOAuth2(Store store, String email, String accessToken) throws MessagingException {
        store.connect(email, accessToken); // Jakarta Mail 2.0+ 支援 XOAUTH2
        // 或手動 SASL: props.put("mail.imap.auth.mechanisms", "XOAUTH2");
    }
}

郵件實體類


@Data @Builder
public class EmailMessage {
    private String messageId;
    private String subject;
    private String from;
    private String textBody;
    private String htmlBody;
    private Date sentDate;
    private Set flags;
    private List attachments = new ArrayList<>();
}

@Data @AllArgsConstructor
public class Attachment {
    private String fileName;
    private String contentType;
    private File file; // 或 byte[] / InputStream / S3 路徑
}

生產級優化清單

優化點配置/代碼效果
連接池Apache Commons Pool2 包裝 Store,maxTotal=20避免頻繁建立 SSL 連接開銷
增量同步IMAP UIDNEXT/UIDVALIDITY 記錄同步游標僅抓取新郵件,避免全量掃描
大附件流式存儲Files.copy(is, path) 而非 getContent()防止 Heap OOM
異步處理@Async + 線程池解析郵件不阻塞 API 響應
錯誤重試Resilience4j Retry (指數退避)應對網路抖動、郵件伺服器暫時不可用

避坑指南

  • ❌ POP3 不支援多設備同步、不支援標記已讀同步 → **生產必用 IMAP**
  • ❌ msg.getContent() 直接強轉 String → 可能是 Multipart 拋 ClassCastException,必須 isMimeType() 判斷
  • ❌ 忽略 MimeUtility.decodeText() → 中文主題/附件名亂碼
  • ❌ 密碼明文配置 → 必須用 Vault/Secret Manager + 應用專用密碼/OAuth2
  • ✅ 設置 mail.imap.fetchsize 分塊獲取大郵件
  • ✅ 定期 folder.expunge() 清理已刪除郵件

結語

Java 讀取郵件的核心難點在於**多部分 MIME 結構遞歸解析**、**大附件流式處理**與**現代 OAuth2 認證遷移**。掌握 Jakarta Mail API 的 Part 樹狀模型、IMAP 部分獲取與 IDLE 推送機制,配合連接池化與異步解析,即可構建企業級郵件整合中台。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved