前言
电子邮件(email)是一种传统的互联网沟通和交流方式,目前仍在办公场所广泛使用。最近在负责一款邮件客户端APP的开发工作,接触到了一些邮件相关的代码和知识,抽时间就整理了一下邮件开发相关的开源库和资源。
正文
首先明确一下,邮箱从使用协议/类别上大致可以分为两种:
- 标准的IMAP/SMTP邮箱,如GMail、Outlook、Yahoo、QQ、163等邮箱。
- Exchange,即Microsoft Exchange Server,是微软公司推出的邮件服务器,如Exchange 2010。应用程序可以使用EWS(Exchange Web Service)来访问Exchange服务器上的邮件、日程、联系人等。
AppAuth
AppAuth is a client SDK for communicating with OAuth 2.0 and OpenId Connect Providers.
mailcore2
MailCore2是一个用C语言编写的邮件协议封装库,能够运行在iOS、Android、Mac、Windows和Linux平台上,支持SMTP、IMAP、POP3、MIME和HTML消息渲染。MailCore2 API的调用都是异步的,即操作都在子线程中进行,不会阻塞当前主线程。
MailCore 2 provide a simple and asynchronous API to work with e-mail protocols IMAP, POP and SMTP. The API has been redesigned from ground up.
GitHub: github.com/MailCore/ma…
很多第三方邮件客户端收发邮件都是基于MailCore2开发的,如小巧精致,超高颜值的开源项目Mailspring
编译
- 编译环境:macOS 10.13.6,Intel处理器,Android NDK 17.2.4988734
- 安装autoconf
curl -OL http://ftpmirror.gnu.org/autoconf/autoconf-2.69.tar.gz
tar -xzf autoconf-2.69.tar.gz
cd autoconf-2.69
./configure && make && sudo make install
- 安装automake
curl -OL http://ftpmirror.gnu.org/automake/automake-1.14.tar.gz
tar -xzf automake-1.14.tar.gz
cd automake-1.14
./configure && make && sudo make install
- 安装libtool
curl -OL http://ftpmirror.gnu.org/libtool/libtool-2.4.2.tar.gz
tar -xzf libtool-2.4.2.tar.gz
cd libtool-2.4.2
./configure && make && sudo make install
- 安装pkg-config
curl -OL http://pkgconfig.freedesktop.org/releases/pkg-config-0.29.2.tar.gz
tar -xzf pkg-config-0.29.2.tar.gz
cd pkg-config-0.29.2
./configure --with-internal-glib && make && sudo make install
- 安装cmake
在cmake官网下载cmake-3.22.2-macos-universal.dmg,安装,然后执行以下命令:
sudo "/Applications/CMake.app/Contents/bin/cmake-gui" --install
//查看cmake版本
cmake --version
- 参考官方文档-Build for Android,编译MailCore2源代码,生成mailcore2-android-4.aar,大小有30M左右😀😔😱 编译好的aar文件已经上传GitHub,地址:github.com/kongpf8848/…
使用
- 将生成的mailcore2-android-4.aar放入在项目模块的libs目录下
- 在模块的build.gradle文件中添加以下代码:
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
api(name: 'mailcore2-android-4', ext: 'aar')
}
添加邮箱->拉取邮件的整体流程如下:
- 检查邮箱是否可用 --> 获取文件夹信息 --> 获取指定文件夹UID列表
- 获取邮件Header --> 获取邮件Structure --> 获取邮件Body和附件信息 --> 下载邮件附件
构建IMAPSession
使用API的第一步就是构建Session对象,其包含邮箱用户名,密码,主机名,端口等信息。
IMAPSession imapSession = new IMAPSession();
imapSession.setUsername("xxx@163.com");
imapSession.setPassword("授权码");
imapSession.setHostname("imap.163.com");
imapSession.setPort(993);
imapSession.setConnectionType(ConnectionType.ConnectionTypeTLS);
IMAPIdentity imapIdentity = imapSession.clientIdentity();
imapIdentity.setVendor("xxx");
imapIdentity.setName("xxx");
imapIdentity.setVendor("xxx");
如不需要校验证书,则添加以下代码:
imapSession.setCheckCertificateEnabled(false);
如需要走OAuth2
认证,如GMail,Outlook等邮箱,则需要添加以下代码:
imapSession.setOAuth2Token("token");
imapSession.setAuthType(AuthType.AuthTypeXOAuth2Outlook); //Outlook邮箱
//imapSession.setAuthType(AuthType.AuthTypeXOAuth2); //非Outlook邮箱
检查邮箱IMAP服务是否可用
IMAPOperation imapOperation = imapSession.checkAccountOperation();
if (imapOperation == null) {
return;
}
imapOperation.start(new OperationCallback() {
@Override
public void succeeded() {
Log.d("MailCore2", "check imap succeeded()");
}
@Override
public void failed(MailException e) {
Log.d("MailCore2", "check imap failed() with: e = [" + e + "]");
}
});
构建SMTPSession
smtpSession = new SMTPSession();
smtpSession.setUsername("xxx@163.com");
smtpSession.setPassword("授权码");
smtpSession.setHostname("smtp.163.com");
smtpSession.setPort(465);
smtpSession.setConnectionType(ConnectionType.ConnectionTypeTLS);
如不需要校验证书,则添加以下代码:
smtpSession.setCheckCertificateEnabled(false);
如需要走OAuth2
认证,如GMail,Outlook等邮箱,则需要添加以下代码:
smtpSession.setOAuth2Token("token");
smtpSession.setAuthType(AuthType.AuthTypeXOAuth2Outlook); //Outlook邮箱
//smtpSession.setAuthType(AuthType.AuthTypeXOAuth2); //非Outlook邮箱
检查邮箱SMTP服务是否可用
Address address = new Address();
address.setMailbox(smtpSession.username());
SMTPOperation smtpOperation = smtpSession.checkAccountOperation(address);
if (smtpOperation == null) {
return;
}
smtpOperation.start(new OperationCallback() {
@Override
public void succeeded() {
Log.d("MailCore2", "check smtp succeeded()");
}
@Override
public void failed(MailException e) {
Log.d("MailCore2", "check smtp failed() with: e = [" + e + "]");
}
});
获取文件夹信息
//获取defaultNamespace
IMAPNamespace defaultNamespace = imapSession.defaultNamespace();
if (defaultNamespace == null) {
IMAPFetchNamespaceOperation imapFetchNamespaceOperation = imapSession.fetchNamespaceOperation();
imapFetchNamespaceOperation.start(new OperationCallback() {
@Override
public void succeeded() {
getFolderHasNamespace(imapSession);
}
@Override
public void failed(MailException e) {
getFolderHasNamespace(imapSession);
}
});
} else {
getFolderHasNamespace(imapSession);
}
private void getFolderHasNamespace(IMAPSession imapSession) {
IMAPFetchFoldersOperation imapFetchFoldersOperation = imapSession.fetchAllFoldersOperation();
imapFetchFoldersOperation.start(new OperationCallback() {
@Override
public void succeeded() {
List<IMAPFolder> folderList = imapFetchFoldersOperation.folders();
if (!ListUtil.isEmpty(folderList)) {
for (IMAPFolder folder : folderList) {
Log.d("MailCore2", "succeeded() called with path=" + folder.path() + ",name=" + getFolderNameForMailCore(imapSession, folder));
}
}
}
@Override
public void failed(MailException e) {
Log.d("MailCore2", "failed() called with: e = [" + e + "]");
}
});
}
//获取文件夹名称,将path转化成name
private String getFolderNameForMailCore(IMAPSession imapSession, IMAPFolder folder) {
List<String> componentNameList = imapSession.defaultNamespace().componentsFromPath(folder.path());
if (!ListUtil.isEmpty(componentNameList)) {
return componentNameList.get(componentNameList.size() - 1);
} else {
List<String> pathList = new ArrayList<>();
pathList.add(folder.path());
String namespacePath = imapSession.defaultNamespace().pathForComponents(pathList);
List<String> nameList = imapSession.defaultNamespace().componentsFromPath(namespacePath);
if (ListUtil.isEmpty(nameList)) {
String[] defaultNameArray = {"inbox", "sent messages", "sent", "junk", "deleted"};
if (Arrays.asList(defaultNameArray).contains(folder.path().toLowerCase())) {
return folder.path();
} else {
return "Unknown";
}
} else {
return nameList.get(nameList.size() - 1);
}
}
}
这里需要注意:
- 获取文件夹之前最好先执行获取defaultNamespace操作,后面将path转换为name的时候需要用到defaultNamespace, 不执行则defaultNamespace()方法返回的值为null
- IMAPFolder对象中的path为一串杂乱的字符串,需要执行上面的
getFolderNameForMailCore
方法才能转化为可读的名称
获取指定文件夹UID列表
根据文件夹名称获取指定文件夹消息数量,然后获取UID列表
//获取收件箱UID
String path = "INBOX";
IMAPFolderInfoOperation imapFolderInfoOperation = imapSession.folderInfoOperation(path);
imapFolderInfoOperation.start(new OperationCallback() {
@Override
public void succeeded() {
IMAPFolderInfo folderInfo = imapFolderInfoOperation.info();
int messageCount = folderInfo.messageCount();
if (messageCount > 0) {
Range range = new Range(1, messageCount - 1);
IndexSet indexSet = new IndexSet();
indexSet.addRange(range);
IMAPFetchMessagesOperation imapFetchMessagesOperation = imapSession.fetchMessagesByNumberOperation(path, IMAPMessagesRequestKind.IMAPMessagesRequestKindUid, indexSet);
imapFetchMessagesOperation.start(new OperationCallback() {
@Override
public void succeeded() {
List<IMAPMessage> messages = imapFetchMessagesOperation.messages();
if (messages != null && messages.size() > 0) {
for (IMAPMessage message : messages) {
Log.d("MailCore2", "succeeded() called,uid:" + message.uid());
}
}
}
@Override
public void failed(MailException e) {
Log.d("MailCore2", "failed() called with: e = [" + e + "]");
}
});
}
}
@Override
public void failed(MailException e) {
Log.d("MailCore2", "failed() called with: e = [" + e + "]");
}
});
获取邮件Header
根据UID列表获取邮件Header列表
String path = "INBOX"; //path
IndexSet indexSet = new IndexSet();
indexSet.addIndex(1318015733); //添加UID
int requestKind = IMAPMessagesRequestKind.IMAPMessagesRequestKindFlags | IMAPMessagesRequestKind.IMAPMessagesRequestKindInternalDate | IMAPMessagesRequestKind.IMAPMessagesRequestKindFullHeaders;
IMAPFetchMessagesOperation imapFetchMessagesOperation = imapSession.fetchMessagesByUIDOperation(path, requestKind, indexSet);
imapFetchMessagesOperation.start(new OperationCallback() {
@Override
public void succeeded() {
List<IMAPMessage> messages = imapFetchMessagesOperation.messages();
if (messages != null && messages.size() > 0) {
for (IMAPMessage message : messages) {
MessageHeader header = message.header();
if (header != null) {
Log.d("MailCore2", "succeeded() called,subject:" + header.subject());
Address from = header.from();
if (from != null) {
Log.d("MailCore2", "succeeded() called,from:" + from.mailbox() + "," + from.displayName());
}
List<Address> to = header.to();
if (to != null) {
for (Address address : to) {
Log.d("MailCore2", "succeeded() called,to:" + address.mailbox() + "," + address.displayName());
}
}
}
}
}
}
@Override
public void failed(MailException e) {
Log.d("MailCore2", "failed() called with: e = [" + e + "]");
}
});
获取邮件Body
先获取邮件Structure信息,然后再获取邮件正文信息
String path = "INBOX";
IndexSet indexSet = new IndexSet();
indexSet.addIndex(1318015852);
int requestKind = IMAPMessagesRequestKind.IMAPMessagesRequestKindStructure;
IMAPFetchMessagesOperation imapFetchMessagesOperation = imapSession.fetchMessagesByUIDOperation(path, requestKind, indexSet);
imapFetchMessagesOperation.start(new OperationCallback() {
@Override
public void succeeded() {
List<IMAPMessage> messages = imapFetchMessagesOperation.messages();
if (messages != null && messages.size() > 0) {
IMAPMessage message = messages.get(0);
IMAPMessageRenderingOperation imapMessageRenderingOperation = imapSession.htmlBodyRenderingOperation(message, path);
imapMessageRenderingOperation.start(new OperationCallback() {
@Override
public void succeeded() {
String body = imapMessageRenderingOperation.result();
Log.d(TAG, "succeeded() called,body:" + body);
//内联附件信息
List<AbstractPart> inlineAttachments = message.htmlInlineAttachments();
if (inlineAttachments != null && inlineAttachments.size() > 0) {
for (AbstractPart part : inlineAttachments) {
IMAPPart imapPart = (IMAPPart) part;
Log.d("MailCore2", "succeeded() called,inline-attachment:id=" + imapPart.uniqueID()
+ ",filename:" + imapPart.filename() + ",contentId:" + imapPart.contentID()
+ ",mimeType:" + imapPart.mimeType() + ",isInLine:"
+ imapPart.isInlineAttachment() + ",size:" + imapPart.decodedSize()
+ ",partId:" + imapPart.partID() + ",encoding:" + imapPart.encoding());
}
}
//非内联附件信息
List<AbstractPart> attachments = message.attachments();
if (attachments != null && attachments.size() > 0) {
for (AbstractPart part : attachments) {
IMAPPart imapPart = (IMAPPart) part;
Log.d("MailCore2", "succeeded() called,attachment:id=" + imapPart.uniqueID()
+ ",filename:" + imapPart.filename() + ",contentId:" + imapPart.contentID()
+ ",mimeType:" + imapPart.mimeType() + ",isInLine:"
+ imapPart.isInlineAttachment() + ",size:" + imapPart.decodedSize()
+ ",partId:" + imapPart.partID() + ",encoding:" + imapPart.encoding());
}
}
}
@Override
public void failed(MailException e) {
Log.d("MailCore2", "failed() called with: e = [" + e + "]");
}
});
}
}
@Override
public void failed(MailException e) {
Log.d("MailCore2", "failed() called with: e = [" + e + "]");
}
});
下载附件
在上一步获取邮件Body的时候我们已经得到邮件附件信息,一个附件的主要信息如下:
字段 | 类型 | 说明 |
---|---|---|
uniqueID | String | 附件ID |
filename | String | 附件名称,如sun.png |
contentId | String | 内容Id,附件为内联附件时有值,如1fa7edd3.png |
mimeType | String | 附件类型,如image/png,application/octet-stream |
isInLine | boolean | 是否为内联附件,值为true或false |
size | long | 附件大小,单位为字节 |
partId | String | ID,如1,2 |
encoding | int | 附件编码方式,0=Encoding7Bit,1=Encoding8Bit,2=EncodingBinary,3=EncodingBase64,4=EncodingQuotedPrintable |
下载附件对应的代码如下:
private void downloadAttachment(IMAPSession imapSession,String folder, long uid, String partId, int encoding) {
IMAPFetchContentOperation imapFetchContentOperation = imapSession.fetchMessageAttachmentByUIDOperation(folder, uid, partId, encoding);
imapFetchContentOperation.setProgressListener((current, max) -> {
if (max > 0) {
int percent = (int) (current * 1000 / max);
if (percent != downloadPercent) {
downloadPercent = percent;
LogUtil.d("MailCore2", "download percent:" + (percent / 10) + "%");
}
}
});
imapFetchContentOperation.start(new OperationCallback() {
@Override
public void succeeded() {
byte[] data = imapFetchContentOperation.data();
String filePath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + File.separator + "xx.png";
writeFile(filePath, data);
}
@Override
public void failed(MailException e) {
Log.d("MailCore2", "failed() called with: e = [" + e + "]");
}
});
}
private void writeFile(String path, byte[] bytes) {
File file = new File(path);
if (file.exists()) {
file.delete();
}
try (FileOutputStream outputStream = new FileOutputStream(file)) {
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
bufferedOutputStream.write(bytes);
bufferedOutputStream.flush();
bufferedOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
完整的demo可以参考github.com/kongpf8848/…
ews-java-api
微软官方提供的java类库,用于访问Exchange Web Services。后续会专门写文章分析ews类库😄😄😄
The Exchange Web Services (EWS) Java API provides a managed interface for developing Java applications that use EWS. By using the EWS Java API, you can access almost all the information stored in an Office 365, Exchange Online, or Exchange Server mailbox.
GitHub: github.com/OfficeDev/e…
ews-cpp
C++11 header-only library for Microsoft Exchange Web Services. GitHub: github.com/otris/ews-c…
ical4j
A Java library for parsing and building iCalendar data models GitHub: github.com/ical4j/ical…
JavaMail
The JavaMail API provides a platform-independent and protocol-independent framework to build mail and messaging applications. The JavaMail API is available as an optional package for use with the Java SE platform and is also included in the Java EE platform. GitHub: github.com/javaee/java…
官方网站: javaee.github.io/javamail
使用
针对Java Maven程序,在pom.xml文件中添加以下代码:
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
针对Java Gradle程序,在build.gradle文件中添加以下代码:
implementation 'com.sun.mail:javax.mail:1.6.2'
针对Android程序,在build.gradle文件中添加以下代码:
android {
packagingOptions {
pickFirst 'META-INF/LICENSE.txt'
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/NOTICE'
exclude 'META-INF/NOTICE.md'
exclude 'META-INF/NOTICE.txt'
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'com.sun.mail:android-mail:1.6.7'
implementation 'com.sun.mail:android-activation:1.6.7'
}
k9-mail
Open Source Email App for Android
GitHub: github.com/k9mail/k-9
官方网站: k9mail.app
Maily
Flutter Mail app for iOS, Android and hopefully more platforms in the future.
GitHub: github.com/Enough-Soft…
其他资源
Yahoo
Outlook
Office365
Microsoft OpenID Connect 服务的发现文档
Other
名称 | 链接 | 图片 |
---|---|---|
GMail | mail.google.com | |
Outlook | outlook.live.com/owa | |
QQ邮箱 | mail.qq.com | |
网易邮箱大师 | dashi.163.com | |
Spike | www.spikenow.com | |
Spark | sparkmailapp.com | |
Hey | www.hey.com | |
MailTime | mailtime.com/zh_hk | |
mail.ru | mail.ru/ | |
商务密邮 | www.10sain.com |