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

JavaMail API 實戰:電子郵件協議原理與發送接收完整指南

     編輯:Java 相關知識

前言

JavaMail 是 Sun/Oracle 提供的郵件處理標準 API,支援 SMTP/POP3/IMAP 等協議。本文系統講解郵件系統架構、核心協議原理、JavaMail 核心類體系、發送接收實戰代碼、附件處理、HTML 郵件、異步發送及生產環境優化。

郵件系統架構與核心協議

1. 核心組件

  • MUA (Mail User Agent):用戶代理 (Outlook、Foxmail、手機郵件客戶端)
  • MTA (Mail Transfer Agent):郵件傳輸代理 (Postfix、Sendmail、Exchange) - 處理郵件路由轉發
  • MDA (Mail Delivery Agent):郵件投遞代理 - 將郵件投入用戶郵箱
  • 郵箱:郵件存儲容器 (Maildir、mbox、數據庫)

2. 郵件流轉流程

發件人 MUA 
    ↓ SMTP (提交)
發件方 MTA (發件郵件服務器)
    ↓ SMTP (轉發, 可能經過多級 MTA)
收件方 MTA (收件郵件服務器)
    ↓ MDA (投遞)
收件人郵箱
    ↓ POP3/IMAP (讀取)
收件人 MUA

3. 核心協議對比

協議全稱端口用途特點
SMTP Simple Mail Transfer Protocol 25/465/587 發送/轉發 單向、推送模式、需認證
POP3 Post Office Protocol v3 110/995 接收/下載 下載到本地、默認刪除服務器、離線讀取
IMAP Internet Message Access Protocol 143/993 接收/同步 服務器同步、多設備一致、支援文件夾、旗標

4. SSL/TLS 加密端口

  • SMTP:465 (隱式 SSL)、587 (STARTTLS 顯式 TLS)
  • POP3:995 (SSL)
  • IMAP:993 (SSL)

JavaMail 核心 API 體系

1. 四大核心類

類別角色核心方法
Session 環境上下文、配置管理、連接工廠 getInstance(), getTransport(), getStore()
Message 郵件對象 (MimeMessage) setFrom(), setRecipients(), setSubject(), setContent()
Transport 發送服務 (SMTP) connect(), sendMessage(), close()
Store 接收服務 (POP3/IMAP) connect(), getFolder(), close()

2. MimeMessage 結構

MimeMessage
├── Headers (From, To, Subject, Date, Content-Type...)
├── Content
│   ├── Simple: text/plain
│   ├── HTML: text/html
│   ├── Attachment: multipart/mixed
│   │   ├── Part 1: text/html (正文)
│   │   ├── Part 2: application/pdf (附件1)
│   │   └── Part 3: image/png (附件2)
│   └── Inline Image: multipart/related
│       ├── Part 1: text/html (引用 cid:xxx)
│       └── Part 2: image/png (Content-ID: <xxx>)
└── ...

發送郵件實戰

1. 基礎文本郵件

public class MailSender {
    private final Session session;
    private final String username;
    private final String password;

    public MailSender(String host, int port, String username, String password, boolean ssl) {
        Properties props = new Properties();
        props.put("mail.transport.protocol", "smtp");
        props.put("mail.smtp.host", host);
        props.put("mail.smtp.port", port);
        props.put("mail.smtp.auth", "true");
        
        if (ssl) {
            props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
            props.put("mail.smtp.socketFactory.port", port);
        } else {
            props.put("mail.smtp.starttls.enable", "true"); // STARTTLS
        }
        
        this.session = Session.getInstance(props);
        this.session.setDebug(true); // 開發調試
        this.username = username;
        this.password = password;
    }

    public void sendText(String from, String to, String subject, String text) throws MessagingException {
        MimeMessage msg = new MimeMessage(session);
        msg.setFrom(new InternetAddress(from));
        msg.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
        msg.setSubject(subject, "UTF-8");
        msg.setText(text, "UTF-8");
        msg.setSentDate(new Date());
        
        send(msg);
    }

    private void send(MimeMessage msg) throws MessagingException {
        try (Transport transport = session.getTransport()) {
            transport.connect(username, password);
            transport.sendMessage(msg, msg.getAllRecipients());
        }
    }
}

2. HTML 郵件與內嵌圖片

public void sendHtmlWithInlineImage(String from, String to, String subject, 
                                    String htmlContent, Map<String, File> inlineImages) 
                                    throws MessagingException {
    MimeMessage msg = new MimeMessage(session);
    msg.setFrom(new InternetAddress(from));
    msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
    msg.setSubject(subject, "UTF-8");
    
    // 根容器:related (正文 + 內嵌資源)
    MimeMultipart root = new MimeMultipart("related");
    
    // 正文部分
    MimeBodyPart htmlPart = new MimeBodyPart();
    htmlPart.setContent(htmlContent, "text/html;charset=UTF-8");
    root.addBodyPart(htmlPart);
    
    // 內嵌圖片
    for (Map.Entry<String, File> entry : inlineImages.entrySet()) {
        MimeBodyPart imgPart = new MimeBodyPart();
        imgPart.attachFile(entry.getValue());
        imgPart.setContentID("<" + entry.getKey() + ">"); // CID
        imgPart.setDisposition(MimeBodyPart.INLINE);
        root.addBodyPart(imgPart);
    }
    
    msg.setContent(root);
    send(msg);
}

// HTML 中引用: 對應 Content-ID: 

3. 附件郵件 (混合模式)

public void sendWithAttachments(String from, String to, String subject, 
                                String htmlContent, List<File> attachments) 
                                throws MessagingException {
    MimeMessage msg = new MimeMessage(session);
    msg.setFrom(new InternetAddress(from));
    msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
    msg.setSubject(subject, "UTF-8");
    
    // 外層:mixed (正文 + 附件)
    MimeMultipart mixed = new MimeMultipart("mixed");
    
    // 內層:alternative (純文本 + HTML) 或 related (HTML + 內嵌圖)
    MimeMultipart alternative = new MimeMultipart("alternative");
    
    MimeBodyPart textPart = new MimeBodyPart();
    textPart.setText("請使用支持 HTML 的郵件客戶端查看", "UTF-8");
    alternative.addBodyPart(textPart);
    
    MimeBodyPart htmlPart = new MimeBodyPart();
    htmlPart.setContent(htmlContent, "text/html;charset=UTF-8");
    alternative.addBodyPart(htmlPart);
    
    // 將 alternative 加入 mixed
    MimeBodyPart contentPart = new MimeBodyPart();
    contentPart.setContent(alternative);
    mixed.addBodyPart(contentPart);
    
    // 添加附件
    for (File file : attachments) {
        MimeBodyPart attachPart = new MimeBodyPart();
        attachPart.attachFile(file);
        attachPart.setFileName(MimeUtility.encodeText(file.getName(), "UTF-8", "B"));
        mixed.addBodyPart(attachPart);
    }
    
    msg.setContent(mixed);
    send(msg);
}

4. 群發與抄送

msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse("a@x.com, b@x.com"));
msg.setRecipients(Message.RecipientType.CC, InternetAddress.parse("cc@x.com"));
msg.setRecipients(Message.RecipientType.BCC, InternetAddress.parse("bcc@x.com")); // 密送

接收郵件實戰

1. POP3 接收

public List<MailMessage> receivePop3(String host, int port, boolean ssl, 
                                     String username, String password) throws MessagingException {
    Properties props = new Properties();
    props.put("mail.store.protocol", "pop3");
    props.put("mail.pop3.host", host);
    props.put("mail.pop3.port", port);
    if (ssl) {
        props.put("mail.pop3.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
    }
    
    Session session = Session.getInstance(props);
    List<MailMessage> mails = new ArrayList<>();
    
    try (Store store = session.getStore("pop3")) {
        store.connect(username, password);
        
        Folder inbox = store.getFolder("INBOX");
        inbox.open(Folder.READ_ONLY);
        
        Message[] messages = inbox.getMessages();
        for (int i = messages.length - 1; i >= Math.max(0, messages.length - 20); i--) {
            mails.add(parseMessage(messages[i]));
        }
        
        inbox.close(false);
    }
    return mails;
}

2. IMAP 接收 (支援搜索、標記)

public List<MailMessage> receiveImap(String host, int port, boolean ssl,
                                     String username, String password, String folderName) 
                                     throws MessagingException {
    Properties props = new Properties();
    props.put("mail.store.protocol", "imap");
    props.put("mail.imap.host", host);
    props.put("mail.imap.port", port);
    if (ssl) props.put("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
    
    Session session = Session.getInstance(props);
    
    try (Store store = session.getStore("imap")) {
        store.connect(username, password);
        
        Folder folder = store.getFolder(folderName);
        folder.open(Folder.READ_WRITE); // 可寫入標記
        
        // 搜索未讀郵件
        FlagTerm unreadTerm = new FlagTerm(new Flags(Flags.Flag.SEEN), false);
        Message[] messages = folder.search(unreadTerm);
        
        List<MailMessage> mails = new ArrayList<>();
        for (Message msg : messages) {
            mails.add(parseMessage(msg));
            // 標記為已讀
            msg.setFlag(Flags.Flag.SEEN, true);
        }
        
        folder.close(false);
        return mails;
    }
}

3. 郵件解析通用方法

private MailMessage parseMessage(Message msg) throws MessagingException, IOException {
    MailMessage mail = new MailMessage();
    mail.setSubject(MimeUtility.decodeText(msg.getSubject()));
    mail.setFrom(InternetAddress.toString(msg.getFrom()));
    mail.setSentDate(msg.getSentDate());
    mail.setContent(parsePart(msg));
    return mail;
}

private String parsePart(Part part) throws MessagingException, IOException {
    if (part.isMimeType("text/plain")) {
        return (String) part.getContent();
    } else if (part.isMimeType("text/html")) {
        return (String) part.getContent();
    } else if (part.isMimeType("multipart/*")) {
        Multipart mp = (Multipart) part.getContent();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < mp.getCount(); i++) {
            sb.append(parsePart(mp.getBodyPart(i)));
        }
        return sb.toString();
    }
    return "";
}

生產環境優化與最佳實踐

1. 連接池管理

// 使用 Apache Commons Pool 2 管理 Transport/Store 連接
public class MailSessionPool {
    private final GenericObjectPool transportPool;
    private final GenericObjectPool storePool;
    
    // 初始化池配置...
    
    public Transport borrowTransport() throws Exception {
        return transportPool.borrowObject();
    }
    
    public void returnTransport(Transport transport) {
        transportPool.returnObject(transport);
    }
}

2. 異步發送 (生產者-消費者)

@Service
public class AsyncMailService {
    private final BlockingQueue queue = new LinkedBlockingQueue<>(10000);
    private final ExecutorService senderPool = Executors.newFixedThreadPool(4);
    
    @PostConstruct
    public void init() {
        for (int i = 0; i < 4; i++) {
            senderPool.submit(this::consume);
        }
    }
    
    public void sendAsync(MailTask task) {
        queue.offer(task); // 非阻塞
    }
    
    private void consume() {
        while (true) {
            try {
                MailTask task = queue.take();
                mailSender.send(task.getMessage());
            } catch (Exception e) {
                log.error("郵件發送失敗", e);
                // 重試邏輯、死信隊列...
            }
        }
    }
}

3. 重試與熔斷

@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
public void sendWithRetry(MimeMessage msg) throws MessagingException {
    try (Transport transport = session.getTransport()) {
        transport.connect(username, password);
        transport.sendMessage(msg, msg.getAllRecipients());
    } catch (MessagingException e) {
        // 記錄失敗、觸發熔斷...
        throw e;
    }
}

4. 安全配置清單

  • ✅ 必須使用 SSL/TLS (465/587/993/995)
  • ✅ 使用授權碼而非登錄密碼 (Gmail、QQ、163 等)
  • ✅ 驗證發件人域名 SPF/DKIM/DMARC 記錄
  • ✅ 限制發送頻率,避免被判定為垃圾郵件
  • ✅ 敏感信息 (密碼/授權碼) 存放在配置中心/密保箱,不入代碼倉庫

常見問題排查

異常原因解決
AuthenticationFailedException 密碼錯誤/未開啟 SMTP/需授權碼 檢查郵箱設置、使用授權碼、開啟 SMTP 服務
MessagingException: Could not connect to SMTP host 網絡不通/端口錯誤/防火牆 telnet host port 測試、檢查 SSL/TLS 端口
javax.mail.SendFailedException: Invalid Addresses 郵箱格式錯誤/編碼問題 使用 InternetAddress.parse()、MimeUtility.encodeText()
附件中文名亂碼 文件名編碼缺失 MimeUtility.encodeText(filename, "UTF-8", "B")
大附件發送失敗 超過郵件服務器限制 (通常 25-50MB) 分卷壓縮、雲存儲鏈接、分片上傳

總結

  • Session:線程安全,全局單例,配置協議/主機/端口/認證
  • MimeMessage:構建郵件結構,支援文本/HTML/附件/內嵌圖片/多部分
  • Transport:SMTP 發送,連接認證發送關閉
  • Store/Folder:POP3/IMAP 接收,支援搜索/標記/文件夾管理
  • 生產級:連接池、異步隊列、重試熔斷、監控告警、安全合規
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved