初步实现 Mail 插件 —— 收取邮件

原创
2013/11/25 22:51
阅读数 1.8K

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

展开阅读全文
加载中
点击加入讨论🔥(3) 发布并加入讨论🔥
打赏
3 评论
13 收藏
5
分享
返回顶部
顶部