一个邮件框架的重构记录

Email for Android的由来

2018年初自己写了一个小项目需要邮件发送功能,本想这个功能在服务端实现,奈何那时自己还不会服务端开发,所以只能把JavaMail的Jar包导入Android项目里面开发这个邮件发送功能,这个过程也踩了很多坑。后来到了2018年的暑假有了自己的Github账号,想放一些个人作品,最后选择了把这个发送邮件的功能封装成一个框架并上传到Github,方便他人使用。在重构框架的过程中,特别在3.0版本后解决了很多问题,所以特写此文进行记录。项目的Github地址如下:

https://github.com/mailhu/email

安装引入

步骤一、将JitPack存储库添加到根目录的build.gradle中:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

步骤二、在项目的app模块下的build.gradle里加:

dependencies {
    implementation 'com.github.mailhu:email:3.1.2'
}

注:因为该库内部使用了Java 8新特性,如果你的项目依赖该库在构建时失败,出现如下错误:

Invoke-customs are only supported starting with Android O (--min-api 26)

你可以在项目的app模块下的build.gradle里加添如下代码:

android {
    ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

在Android项目中的AndroidManifest.xml文件中添加联网权限。


使用示例

在发送邮件或读取邮件等操作都需要与邮箱服务提供商的服务器进行连接,所以需要在代码中配置邮箱服务器的主机地址和端口,该框架已经支持快速配置和自定义配置。通过创建一个Email类中的Config类的对象,然后设置发件人的邮箱和密码(授权码),还有邮件服务器的参数。

如何开启邮箱SMTP,IMAP,POP3服务和获取授权码?

快速配置,目前只支持QQ邮箱,Foxmail,网易邮箱

Email.Config config = new Email.Config()
        .setMailType(Email.MailType.QQ)     //选择邮箱类型
        .setAccount("[email protected]")          //发件人的邮箱
        .setPassword("password");           //发件人邮箱的密码或者授权码

自定义配置,自行填写你使用的邮箱的服务器host和port

Email.Config config = new Email.Config()
        .setSMTP("smtp.qq.com", 465)        //设置SMTP发件服务器主机地址和端口
        .setIMAP("imap.qq.com", 993)        //设置IMAP收件服务器主机地址和端口
        .setPOP3("pop.qq.com", 995)         //设置POP3收件服务器主机地址和端口
        .setAccount("[email protected]")          //发件人的邮箱
        .setPassword("password");           //发件人邮箱的密码或者授权码

发送邮件

前面已创建Config对象,把该对象传入getSendService方法,接着设置收件人的邮箱,发件人的昵称,邮件的主题,邮件的正文,然后调用send方法发送这封邮件,最后可以在回调接口中编写你的逻辑代码。还要注意的是setText方法用于发送文本邮件,setContent方法则是发送HTML邮件。

Email.getSendService(config)
        .setTo("[email protected]")       //收件人邮箱
        .setNickname("小学生")              //发件人的昵称
        .setSubject("这是一封测试邮件")      //邮件的主题
        .setText("Hello World !")           //邮件的内容
        .send(new Email.GetSendCallback() {
            @Override
            public void onSuccess() {
                Log.i(TAG, "发送成功");
            }

            @Override
            public void onFailure(String msg) {
                Log.i(TAG, "错误信息:" + msg);
            }
        });

发送成功后,很快对方的邮箱就会收到一封你发来的邮件。是不是很简单?

一个邮件框架的重构记录_第1张图片

读取邮箱中的邮件

使用IMAP协议读取邮箱中全部邮件,只需通过getIMAPService方法获取IMAPService的对象,然后调用该对象的receive方法即可,如果你想使用POP3协议来读取邮件,只需把getIMAPService方法改为getPOP3Service方法即可。receive方法的回调接口中的方法用途大致如下:

  • receiving:每读取一封邮件立即返回该封邮件的数据
  • onFinish:读取完全部邮件的数据后再返回全部邮件的数据
  • onFailure:获取错误信息
Email.getReceiveService(config)
        .getIMAPService()       //如果你想使用POP3协议,这里改为getPOP3Service()
        .receive(new Email.GetReceiveCallback() {
            @Override
            public void receiving(Message message) {
                Log.i(TAG, "发件人:" + message.getFrom());
                Log.i(TAG, "收件人:" + message.getTo());
                Log.i(TAG, "日期:" + message.getDate());
                Log.i(TAG, "主题:" + message.getSubject());
                Log.i(TAG, "内容:" + message.getContent());
            }

            @Override
            public void onFinish(List messageList) {
                Log.i(TAG, "邮箱中的邮件数量:" + messageList.size());
            }

            @Override
            public void onFailure(String msg) {
                Log.i(TAG, "错误信息:" + msg);
            }
        });

运行一下上面的示例代码,可以看到Logcat里面逐条打印每封邮件的数据。使用message对象的getContent方法可以获取到每封邮件的内容,如果邮件内容是HTML类型的,可以把这些内容保存到一个文件后缀名为“.html”的文件中,然后使用WebView读取这个文件即可看到邮件内容的原页面。我把我邮箱中的其中两份邮件内容保存在HTML类型的文件中,然后浏览器打开,效果如下图所示。

一个邮件框架的重构记录_第2张图片
一个邮件框架的重构记录_第3张图片
一个邮件框架的重构记录_第4张图片

上面示例中的receive方法可以读取邮箱中的全部邮件,但是每次都用这个方法来同步邮箱中的全部邮件到本地是不可取的,因为receive方法同步邮件的过程中会读取它里面的邮件数据,这个过程是十分耗时的。以我邮箱为例,邮箱里面有300多封邮件,每次使用receive方法来读取全部邮件需要耗时两分多钟。所以在Eamil 3.0版本增加了新的同步邮件API。通过获取邮件的UID(邮件序号)来同步邮件,以我的邮箱为例,每同步一次UID只需耗时十几秒。获取UID的代码如下:

Email.getReceiveService(config)
        .getIMAPService()
        .getUIDList(new Email.GetUIDListCallback() {
            @Override
            public void onSuccess(long[] uidList) {
                for (long i : uidList) {
                    Log.i(TAG, "uid = " + i);
                }
            }

            @Override
            public void onFailure(String msg) {
                Log.i(TAG, "错误信息:" + msg);
            }
        });

使用UID来同步邮件只能获取到最新的一组邮件序列号,你需要与之前已保存的UID作比较,找到新增的的UID(有新邮件),然后使用getMessage方法来获取邮件的数据。


Email.getReceiveService(config)
        .getIMAPService()
        .getMessage(15, new Email.GetMessageCallback() {
            @Override
            public void onSuccess(Message message) {
                Log.i(TAG, "发件人:" + message.getFrom());
                Log.i(TAG, "收件人:" + message.getTo());
                Log.i(TAG, "日期:" + message.getDate());
                Log.i(TAG, "主题:" + message.getSubject());
                Log.i(TAG, "内容:" + message.getContent());
            }

            @Override
            public void onFailure(String msg) {
                Log.i(TAG, "错误信息:" + msg);
            }
        });

框架主要的使用方法已介绍完了,还有获取邮件数量,未读邮件数量,邮件服务器连接检查等功能可以去Github上查看README文档。接下来写一下这个框架的代码重构的过程和遇到的坑,如果你对后面的内容不感兴趣的话,也可以选择结束本文阅读,哈哈!

代码重构和遇到的烦心事

这个框架在开发1.0版本时只有发送邮件这个功能,后来觉得既然有发送邮件功能为何不搞多个读取邮件的功能,由于1.0设计的时候没有考虑框架内部的类的划分,顺着1.0版本的代码接着写下去,导致2.0版本内部的代码写得一塌糊涂,外部的API接口设计也有很多不合理。最烦的是读取邮件时解析邮件内容的过程,程序会老崩溃,而我写的代码根本没问题,但收到一些用了这个框架的朋友邮件反馈时,我自己都无法给出解析,这个框架直到3.1.0版本终于修复了这个问题。

  • 邮件服务器配置类
    3.0版本前选择邮件协议并配置邮件服务器的主机地址和端口是分开填写的,这样有个弊端是如果你填主机地址,但忘了填端口号,这样就会连接不上邮件服务器。为了避免这种情况的出现,在3.0版本后,当你选择了对应邮件传输协议的API,你必需同时填入主机地址和端口号。配置服务器主机地址和端口不一定要同时使用这三个API,如果你只是需要发送邮件功能,只需填写SMTP的API。若你需要读取邮件,只需填写IMAP和POP3两个API中的中一个,不过我建议选IMAP。
一个邮件框架的重构记录_第5张图片
  • 线程切换与回调接口
    因为Android在4.0之后不允许在UI线程里进行网络请求,所以框架内部需要开启子线程进行网络请求,但在子线程中又无法更新UI。例如在3.0版本前提供的发送邮件的API在回调时可以选择是否切换回UI线程(就一个Activity参数来区分,我也忍这个参数很久了)。后来觉得既然邮件的发送完了,不切换回来还能干嘛!所以3.0版本的API在回调时全部都切换回UI线程,框架内部切换线程的实现也由runOnUiThread方法改为Handler。同时每个回调接口由以前独立写在.java文件中也全部改为写在Email类中,方便的统一管理。
一个邮件框架的重构记录_第6张图片
  • 解析邮件内容遇到的Bug
    这个邮件框架在3.1.0版本前读取邮件并解析邮件内容时,一直存在崩溃报错的现象,这种现象只存在于我把代码上传到JitPack,别人依赖进项目的时候才会出现(编译可通过,运行时报错),但把Github仓库的测试用例安装到模拟器和真机上运行都没问题,这就很奇怪了。有一些朋友用了这个框架读取邮件一直出现这个错误,还发邮件问我原因,很尬尴的是,我自己都解析不了这是什么情况,当时还一度想放弃这个仓库,删库跑路了。使用POP3协议和IMAP协议分别出现的报错内容大致如下:
AndroidRuntime: FATAL EXCEPTION: Thread-3
Process: com.example.demo, PID: 6672
java.lang.ClassCastException: javax.mail.util.SharedByteArrayInputStream cannot be cast to javax.mail.Multipart
    at com.smailnet.eamil.Converter$Content.toString(Converter.java:119)
AndroidRuntime: FATAL EXCEPTION: Thread-3
Process: com.example.demo, PID: 7455
java.lang.ClassCastException: com.sun.mail.imap.IMAPInputStream cannot be cast to javax.mail.Multipart
    at com.smailnet.eamil.Converter$Content.toString(Converter.java:119)

报错一直出现在框架内部的Converter类的第119行,说强制类型转换出错,但是我觉得我的代码没错啊!而且我后来还在类型转换前用关键字instanceof来核对一下类型,当框架依赖进别人的项目中,代码运行到类型转换那一行还是报错。多次尝试修复一直无果,我也很无奈。

一个邮件框架的重构记录_第7张图片

到了今年的四五六月,时不时收到关于这个框架的邮件反馈,可以解决的尽量都给解决。大多数都是这个问题,不处理留着也不是办法,于是我决定修复反馈的问题和重构以前的烂码。暑假回家后又在网上找了这个解析邮件内容的问题解的决方法很久,最终在JavaMail for Android的官方主页找到了答案。

一个邮件框架的重构记录_第8张图片

官方主页上的第二段英文大意是说:“Android平台没有提供一个Java兼容运行时,所以无法运行已分发的标准JavaMail。相反,(这里)有一个Android平台可用的特殊版本的JavaMail。这个特殊版本的JavaMail依赖于一个特殊版本的JavaBeans Activation Framework。”,难道是我一直用Java版的JavaMail的问题?然后我把项目原来的JavaMail的Jar包全部移除,重新添加Android版的JavaMail依赖,代码上传JitPack,在新项目中把Email框架依赖进去,重新测试读取邮件的API,发现竟然成功了,功夫不负有心人啊!

结语

这个框架对我来说的意义是我的Github上的第一个仓库;也是我自己封装的第一个框架(过了一把造轮子的瘾);见证自己编码水平的变化,在2.x版本时我知道内部代码写烂了(内部代码错综复杂,类文件有16个,太乱了),觉得反正没人看不管了,后来我觉得还是有重写代码的必要,3.x版把内部的类文件合理划分为8个并重新设计和新增API,给它增加新功能;它还有很多功能没有完善,我的目标是继续完善它。

对于他人来说,这个框架可以方便他人使用,让其可以很快写出与邮件客户端相似的功能。毕竟在百度上搜索关于Android发送邮件教程的代码,大多数文章写的都是导JavaMail的相关包和贴代码,但是里面还是有很多坑的,这个我深有体会。虽然这个框架是小众需求,但还是希望做到前人栽树,后人乘凉。

如果你对这个框架感兴趣的话,欢迎到Github上Star哦。

感谢阅读

你可能感兴趣的:(一个邮件框架的重构记录)