本文是《轻量级 Java Web 框架架构设计》的系列博文。
在上篇中描述了发送邮件的主要过程,今天我想和大家分享一下 Smart Mail 插件的另外一个功能 —— 收取邮件,可能没有发送邮件那么常用。
在具体描述如何实现收取邮件之前,有必要对发送邮件与收取邮件各定义一个接口,为了功能更加清晰。
比如,对于发送邮件,我们可以这样定义:
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 即可。
同理,也需要对收取邮件定义一个接口,收取邮件的实现过程正是开始。
第一步:定义一个邮件收取接口
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 插件的开发过程已全部结束,欢迎您的点评,并期待您的建议!