邮件客户端开发相关开源库/资源整理

·  阅读 2031
邮件客户端开发相关开源库/资源整理

前言

电子邮件(email)是一种传统的互联网沟通和交流方式,目前仍在办公场所广泛使用。最近在负责一款邮件客户端APP的开发工作,接触到了一些邮件相关的代码和知识,抽时间就整理了一下邮件开发相关的开源库和资源。

正文

首先明确一下,邮箱从使用协议/类别上大致可以分为两种:

  • 标准的IMAP/SMTP邮箱,如GMail、Outlook、Yahoo、QQ、163等邮箱。
  • Exchange,即Microsoft Exchange Server,是微软公司推出的邮件服务器,如Exchange 2010。应用程序可以使用EWS(Exchange Web Service)来访问Exchange服务器上的邮件、日程、联系人等。

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

p429394382.webp

p429394383.webp

编译

  • 编译环境: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
复制代码

使用

  • 将生成的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);
        }
    }
}
复制代码

image.png 这里需要注意:

  • 获取文件夹之前最好先执行获取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的时候我们已经得到邮件附件信息,一个附件的主要信息如下:

字段类型说明
uniqueIDString附件ID
filenameString附件名称,如sun.png
contentIdString内容Id,附件为内联附件时有值,如1fa7edd3.png
mimeTypeString附件类型,如image/png,application/octet-stream
isInLineboolean是否为内联附件,值为true或false
sizelong附件大小,单位为字节
partIdStringID,如1,2
encodingint附件编码方式,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…

image.png

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

其他资源

名称链接图片
GMailmail.google.comunnamed.webp
Outlookoutlook.live.com/owaimage.png
QQ邮箱mail.qq.comimage.png
网易邮箱大师dashi.163.comimage.png
Spikewww.spikenow.comimage.png
Sparksparkmailapp.comimage.png
Heywww.hey.comimage.png
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改