初步實現 Mail 插件 —— 收取郵件

本文是《輕量級 Java Web 框架架構設計》的系列博文。 java

上篇中描述了發送郵件的主要過程,今天我想和你們分享一下 Smart Mail 插件的另一個功能 —— 收取郵件,可能沒有發送郵件那麼經常使用。 正則表達式

在具體描述如何實現收取郵件以前,有必要對發送郵件與收取郵件各定義一個接口,爲了功能更加清晰。 shell

好比,對於發送郵件,咱們能夠這樣定義: session

public interface MailSender {

    void addCc(String[] cc);

    void addBcc(String[] bcc);

    void addAttachment(String path);

    void send();
}

對該接口提供一個抽象實現類,也就是上篇說到的使用模板方法的那個類了。 數據結構

public abstract class AbstractMailSender implements MailSender {
    ...
}

該抽象類對開發人員是透明的,開發人員只須要知道 MailSender 接口,以及它的兩個具體實現類 TextMailSender、HtmlMailSender 便可。 架構

同理,也須要對收取郵件定義一個接口,收取郵件的實現過程正是開始框架

第一步:定義一個郵件收取接口 ide

public interface MailFetcher {

    List<MailInfo> fetch(int count);

    MailInfo fetchLatest();
}

以上定義了兩個接口方法:收取指定數量的郵件;收取最新一封郵件。經過 MailInfo 類將郵件信息作一個封裝,它的數據結構是怎樣的呢? 工具

第二步:定義一個 JavaBean 以封裝郵件信息 測試

public class MailInfo extends BaseBean {

    private String subject;
    private String content;
    private String from;
    private String[] to;
    private String[] cc;
    private String[] bcc;
    private String date;

    // getter/setter...
}

想必以上這些字段你們都能理解,或許這裏還缺乏了 attachment(附件),目前暫未實現,若是未來有業務需求,可考慮之後進行擴展。

第三步:實現郵件收取接口

目前主要有兩種收取郵件的協議,分別是:POP3 與 IMAP,前者使用普遍,後者功能強大。無論使用哪一種協議,對於 JavaMail 而言,都有相應的支持。惋惜 Apache Commons Email 組件並無對收取郵件提供一個優雅的實現方案,咱們只能有限地使用它,更多地仍是擴展 JavaMail 了。其實也只能使用 Apache Commons Email 的 MimeMessageParser 了,用於解析郵件內容

下面的代碼稍微有些多,我將分塊進行描述。

public class DefaultMailFetcher implements MailFetcher {

    private static final Logger logger = Logger.getLogger(DefaultMailFetcher.class);

    // 獲取協議名(pop3 或 imap)
    private static final String PROTOCOL = MailConstant.Fetcher.PROTOCOL;

    private final String username;
    private final String password;

    public DefaultMailFetcher(String username, String password) {
        this.username = username;
        this.password = password;
    }
...

因爲是收取郵件,那麼就須要提供某個帳號的登陸方式,好比 username 與 password,這樣才能收取該帳號的郵件,因此這裏提供了兩個必填字段,而且在構造器中進行初始化。此外,還從常量類中獲取了指定的協議名,其實都是在配置文件裏進行管理的,本文最後將統一給出。

隨後須要的是實現接口裏的那兩個方法:

...
    @Override
    public List<MailInfo> fetch(int count) {
        // 建立 Session
        Session session = createSession();
        // 建立 MailInfo 列表
        List<MailInfo> mailInfoList = new ArrayList<MailInfo>();
        // 收取郵件
        Store store = null;
        Folder folder = null;
        try {
            // 獲取 Store,並鏈接 Store(登陸)
            store = session.getStore(PROTOCOL);
            store.connect(username, password);
            // 獲取 Folder(收件箱)
            folder = store.getFolder(MailConstant.Fetcher.FOLDER);
            // 判斷是 只讀方式 仍是 讀寫方式 打開收件箱
            if (MailConstant.Fetcher.FOLDER_READONLY) {
                folder.open(Folder.READ_ONLY);
            } else {
                folder.open(Folder.READ_WRITE);
            }
            // 獲取郵件總數
            int size = folder.getMessageCount();
            // 獲取並遍歷郵件列表
            Message[] messages = folder.getMessages();
            if (ArrayUtil.isNotEmpty(messages)) {
                for (int i = size - 1; i > size - count - 1; i--) {
                    // 建立並累加 MailInfo
                    Message message = messages[i];
                    if (message instanceof MimeMessage) {
                        MailInfo mailInfo = createMailInfo((MimeMessage) message);
                        mailInfoList.add(mailInfo);
                    }
                }
            }
        } catch (Exception e) {
            logger.error("錯誤:收取郵件出錯!", e);
        } finally {
            try {
                // 關閉收件箱
                if (folder != null) {
                    folder.close(false);
                }
                // 註銷
                if (store != null) {
                    store.close();
                }
            } catch (MessagingException e) {
                logger.error("錯誤:釋放資源出錯!", e);
            }
        }
        return mailInfoList;
    }

    @Override
    public MailInfo fetchLatest() {
        List<MailInfo> mailInfoList = fetch(1);
        return CollectionUtil.isNotEmpty(mailInfoList) ? mailInfoList.get(0) : null;
    }
...

可見,實現部分是將 JavaMail API 的一個封裝,獲取指定數量的郵件實際上是根據發送日期進行了一個倒序排列(注意 for 循環中的 i 是從後往前遞減的),而獲取最新一封郵件其實是前者的一個特例。

以上用到了一些私有方法,現描述以下:

...
    private Session createSession() {
        // 初始化 Session 配置項
        Properties props = new Properties();
        // 判斷是否支持 SSL 鏈接
        if (MailConstant.Fetcher.IS_SSL) {
            props.put("mail." + PROTOCOL + ".ssl.enable", true);
        }
        // 設置 主機名 與 端口號
        props.put("mail." + PROTOCOL + ".host", MailConstant.Fetcher.HOST);
        props.put("mail." + PROTOCOL + ".port", MailConstant.Fetcher.PORT);
        // 建立 Session
        Session session = Session.getDefaultInstance(props);
        // 判斷是否開啓 debug 模式
        if (MailConstant.IS_DEBUG) {
            session.setDebug(true);
        }
        return session;
    }

    private String[] parseTo(MimeMessageParser parser) throws Exception {
        return doParse(parser.getTo());
    }

    private String[] parseCc(MimeMessageParser parser) throws Exception {
        return doParse(parser.getCc());
    }

    private String[] parseBcc(MimeMessageParser parser) throws Exception {
        return doParse(parser.getBcc());
    }

    private String[] doParse(List<Address> addressList) {
        List<String> list = new ArrayList<String>();
        if (CollectionUtil.isNotEmpty(addressList)) {
            for (Address address : addressList) {
                list.add(MailUtil.decodeEmailAddress(address.toString()));
            }
        }
        return list.toArray(new String[0]);
    }

    private MailInfo createMailInfo(MimeMessage message) throws Exception {
        // 建立 MailInfo
        MailInfo mailInfo = new MailInfo();
        // 解析郵件內容
        MimeMessageParser parser = new MimeMessageParser(message).parse();
        // 設置 MailInfo 相關屬性
        mailInfo.setSubject(parser.getSubject());
        if (parser.hasHtmlContent()) {
            mailInfo.setContent(parser.getHtmlContent());
        } else if (parser.hasPlainContent()) {
            mailInfo.setContent(parser.getPlainContent());
        }
        mailInfo.setFrom(parser.getFrom());
        mailInfo.setTo(parseTo(parser));
        mailInfo.setCc(parseCc(parser));
        mailInfo.setBcc(parseBcc(parser));
        mailInfo.setDate(DateUtil.formatDatetime(message.getSentDate().getTime()));
        return mailInfo;
    }
}

在編寫代碼時,建議你們保持方法的簡短,將能夠重用的代碼或業務獨立的代碼抽取爲私有方法,這也是《重構-改善既有代碼的設計》這本書裏一再強調的地方。

如何使用 MailFetcher 這個接口呢?

第四步:收取郵件測試

public class FetchMailTest {

    private static final String username = "hy_think@163.com";
    private static final String password = "xxx";
    private static final MailFetcher mailFetcher = new DefaultMailFetcher(username, password);

    @Test
    public void fetchTest() {
        List<MailInfo> mailInfoList = mailFetcher.fetch(5);
        for (MailInfo mailInfo : mailInfoList) {
            System.out.println(mailInfo.getSubject());
        }
    }

    @Test
    public void fetchLatestTest() {
        MailInfo mailInfo = mailFetcher.fetchLatest();
        System.out.println(mailInfo.getSubject());
    }
}

可見,只須要提供 username 與 password,就可使用 MailFetcher 了。

通過這兩篇文章,大體描述了一下發送與收取郵件的主要開發過程,固然上面的都是主角,還有寫配角也提供了重要的做用,現描述以下:

config-mail.properties

經過一個 properties 文件提供郵件相關配置。

mail.is_debug=false

sender.protocol=smtp
sender.protocol.ssl=true
sender.protocol.host=smtp.163.com
sender.protocol.port=465
sender.from=管理員<huang_yong_2006@163.com>
sender.auth=true
sender.auth.username=huang_yong_2006@163.com
sender.auth.password=xxx

fetcher.protocol=pop3
fetcher.protocol.ssl=true
fetcher.protocol.host=pop.163.com
fetcher.protocol.port=995
fetcher.folder=INBOX
fetcher.folder.readonly=true

MailConstant.java

經過一個常量類(其實是一個接口),獲取 config-mail.properties 文件中相關配置項,方便在代碼中使用。

public interface MailConstant {

    Properties config = FileUtil.loadPropFile("config-mail.properties");

    boolean IS_DEBUG = CastUtil.castBoolean(config.getProperty("mail.is_debug"));

    interface Sender {

        String PROTOCOL = config.getProperty("sender.protocol");
        boolean IS_SSL = CastUtil.castBoolean(config.getProperty("sender.protocol.ssl"));
        String HOST = config.getProperty("sender.protocol.host");
        int PORT = CastUtil.castInt(config.getProperty("sender.protocol.port"));
        String FROM = config.getProperty("sender.from");
        boolean IS_AUTH = CastUtil.castBoolean(config.getProperty("sender.auth"));
        String AUTH_USERNAME = config.getProperty("sender.auth.username");
        String AUTH_PASSWORD = config.getProperty("sender.auth.password");
    }

    interface Fetcher {

        String PROTOCOL = config.getProperty("fetcher.protocol");
        boolean IS_SSL = CastUtil.castBoolean(config.getProperty("fetcher.protocol.ssl"));
        String HOST = config.getProperty("fetcher.protocol.host");
        int PORT = CastUtil.castInt(config.getProperty("fetcher.protocol.port"));
        String FOLDER = config.getProperty("fetcher.folder");
        boolean FOLDER_READONLY = CastUtil.castBoolean(config.getProperty("fetcher.folder.readonly"));
    }
}

MailUtil.java

經過一個工具類,將代碼中比較通用的功能進行封裝,這裏主要提供了郵箱地址的編碼與解碼方法。因爲考慮到郵箱地址中若是出現中文,可能會致使亂碼。

public class MailUtil {

    private static final Logger logger = Logger.getLogger(MailUtil.class);

    // 定義一個郵箱地址的正則表達式:姓名<郵箱>
    private static final Pattern pattern = Pattern.compile("(.+)(<.+@.+..+>)");

    private static enum CodecType {
        ENCODE, DECODE
    }

    // 編碼郵箱地址
    public static String encodeAddress(String address) {
        return codec(CodecType.ENCODE, address);
    }

    // 解碼郵箱地址
    public static String decodeAddress(String address) {
        return codec(CodecType.DECODE, address);
    }

    private static String codec(CodecType codecType, String address) {
        // 須要對知足匹配條件的郵箱地址進行 UTF-8 編碼,不然姓名將出現中文亂碼
        Matcher addressMatcher = pattern.matcher(address);
        if (addressMatcher.find()) {
            try {
                if (codecType == CodecType.ENCODE) {
                    address = MimeUtility.encodeText(addressMatcher.group(1), "UTF-8", "B") + addressMatcher.group(2);
                } else {
                    address = MimeUtility.decodeText(addressMatcher.group(1)) + addressMatcher.group(2);
                }
            } catch (UnsupportedEncodingException e) {
                logger.error("錯誤:郵箱地址編解碼出錯!", e);
            }
        }
        return address;
    }
}

到目前爲止,Smart Email 插件的開發過程已所有結束,歡迎您的點評,並期待您的建議!

相關文章
相關標籤/搜索