本文是《輕量級 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 插件的開發過程已所有結束,歡迎您的點評,並期待您的建議!