本文是《轻量级 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 插件的开发过程已所有结束,欢迎您的点评,并期待您的建议!