├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── github
│ │ └── artbits
│ │ └── androidmail
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── github
│ │ │ └── artbits
│ │ │ └── androidmail
│ │ │ ├── App.java
│ │ │ ├── Utils.java
│ │ │ ├── store
│ │ │ ├── Folder.java
│ │ │ ├── Message.java
│ │ │ └── UserInfo.java
│ │ │ └── view
│ │ │ ├── BaseActivity.java
│ │ │ ├── ConfigActivity.java
│ │ │ ├── DetailsActivity.java
│ │ │ ├── FolderActivity.java
│ │ │ ├── MainActivity.java
│ │ │ ├── SplashActivity.java
│ │ │ └── WriteActivity.java
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_config.xml
│ │ ├── activity_details.xml
│ │ ├── activity_folder.xml
│ │ ├── activity_main.xml
│ │ ├── activity_write.xml
│ │ └── item_message.xml
│ │ ├── menu
│ │ ├── menu_config.xml
│ │ ├── menu_main.xml
│ │ └── menu_write.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-night
│ │ └── themes.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ ├── data_extraction_rules.xml
│ │ └── network_security_config.xml
│ └── test
│ └── java
│ └── com
│ └── github
│ └── artbits
│ └── androidmail
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── image
├── config.png
├── details.png
├── inbox.png
├── main.png
├── menu.png
└── write.png
├── mailkit
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── github
│ │ └── artbits
│ │ └── mailkit
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── github
│ │ └── artbits
│ │ └── mailkit
│ │ ├── AuthService.java
│ │ ├── IMAPService.java
│ │ ├── MailFolder.java
│ │ ├── MailKit.java
│ │ ├── SMTPService.java
│ │ ├── Tools.java
│ │ └── UIDHandler.java
│ └── test
│ └── java
│ └── com
│ └── github
│ └── artbits
│ └── mailkit
│ └── ExampleUnitTest.java
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | /local.properties
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 | local.properties
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android-Mail
2 |
3 | Android-Mail 是基于 JavaMail 库设计与开发的邮箱 App 。2022年12月完成本项目的代码重构后,不再独立提供封装 JavaMail 的库,而是以整个 App 项目的形式来开源代码。
4 | 本项目工程包含两部分,分别是``app``模块和``mailkit``模块。app 模块含有邮箱客户端的界面和操作逻辑等代码;mailkit 模块主要封装了 JavaMail ,以 API 的形式
5 | 提供给 app 模块调用。Android-Mail 客户端目前支持的功能有:配置邮件服务器、发送简单邮件、获取文件夹列表、同步邮件、加载邮件、读取邮件。
6 | README 会列出 mailkit API 供大家查阅和参考,带有“ * ”的标题表示该 API 未被 app 模块使用。
7 | 因 JavaMail 内容博大精深,作者的水平和时间有限,所以计划本项目不长期维护,请大家见谅。
8 |
9 | 2023年4月,Android-Mail 原先使用的 SQLite 数据库(LitePal ORM)被替换为 QuickIO 数据库。[QuickIO](https://github.com/artbits/quickio) 是作者自研的高性能嵌入式 NoSQL 数据库,现以作为试验,引入到 Android-Mail 项目中使用。
10 |
11 | ## 效果图
12 |
13 | | | | |
14 | |:-------------------------:|:------------------------:|:------------------------:|
15 | | 服务器设置 | 邮箱文件夹列表 | 菜单弹窗 |
16 | | |  |  |
17 | | 写邮件 | 收件箱列表 | 查看邮件内容 |
18 | | |  | |
19 | | | | |
20 |
21 | ## MailKit API
22 |
23 | **配置邮件服务器**
24 | ```java
25 | MailKit.Config config = new MailKit.Config(c -> {
26 | c.account = "user@foxmail.com";
27 | c.password = "password";
28 | c.nickname = "Li Hua";
29 | c.SMTPHost = "smtp.qq.com";
30 | c.SMTPPort = 465;
31 | c.SMTPSSLEnable = true;
32 | c.IMAPHost = "imap.qq.com";
33 | c.IMAPPort = 993;
34 | c.IMAPSSLEnable = true;
35 | });
36 | ```
37 |
38 | **验证邮件服务器配置**
39 | ```java
40 | MailKit.auth(config, () -> {
41 | //验证成功,执行后续操作
42 | }, e -> {
43 | //验证失败
44 | Log.d(TAG, e.getMessage());
45 | });
46 | ```
47 |
48 | **发送邮件**
49 | ```java
50 | MailKit.Draft draft = new MailKit.Draft(d -> {
51 | d.to = new String[]{"to@foxmail.com"};
52 | d.subject = "Android-Mail Test";
53 | d.text = "Hello world";
54 | //d.cc = new String[]{"..."}; 抄送人地址
55 | //d.bcc = new String[]{"..."}; 密送人地址
56 | //d.html = "..." 发送富文本邮件内容
57 | });
58 |
59 | MailKit.SMTP smtp = new MailKit.SMTP(config);
60 | smtp.send(draft, () -> {
61 | //发送成功,执行后续操作
62 | }, e -> {
63 | //发送失败
64 | Log.d(TAG, e.getMessage());
65 | });
66 | ```
67 |
68 | **获取文件夹列表**
69 | ```java
70 | MailKit.IMAP imap = new MailKit.IMAP(config);
71 | imap.getDefaultFolders(strings -> {
72 | //获取成功,打印列表的文件夹名称
73 | strings.forEach(s -> Log.d(TAG, s));
74 | }, e -> {
75 | //获取失败
76 | Log.d(TAG, e.getMessage());
77 | });
78 | ```
79 |
80 | **获取指定的文件夹**
81 | ```java
82 | MailKit.IMAP imap = new MailKit.IMAP(config);
83 | //获取指定名称的文件夹
84 | MailKit.IMAP.Folder folder = imap.getFolder("INBOX");
85 | //获取收件箱文件夹
86 | MailKit.IMAP.Inbox inbox = imap.getInbox();
87 | //获取草稿箱文件夹
88 | MailKit.IMAP.DraftBox draftBox = imap.getDraftBox();
89 | ```
90 |
91 | **加载文件夹中的邮件头列表**
92 |
93 | 邮箱中文件夹中存在多封邮件,用load方法加载缓存到App本地,使用说明:
94 | + 若本地还没缓存过邮件消息时,minUID值传入一个小于0的值,例如-1,它将加载最新的20封邮件。
95 | + 若本地客已缓存过邮件消息时,则给minUID传入本地缓存的全部邮件中UID最小的那个值,它将加载比minUID值更小的20封邮件。
96 | + 每次加载的邮件的数量在[0, 20]之间。
97 | ```java
98 | MailKit.IMAP imap = new MailKit.IMAP(config);
99 | MailKit.IMAP.Folder folder = imap.getFolder("INBOX");
100 | folder.load(-1, msgList -> {
101 | //加载成功,执行后续操作
102 | msgList.forEach(msg -> {
103 | Log.d(TAG, String.valueOf(msg.uid));
104 | Log.d(TAG, String.valueOf(msg.sentDate));
105 | Log.d(TAG, String.valueOf(msg.flags.isSeen));
106 | Log.d(TAG, String.valueOf(msg.flags.isStar));
107 | Log.d(TAG, msg.subject);
108 | Log.d(TAG, msg.from.address);
109 | Log.d(TAG, msg.from.nickname);
110 | msg.toList.forEach(to -> {
111 | Log.d(TAG, to.address);
112 | Log.d(TAG, to.nickname);
113 | });
114 | msg.ccList.forEach(cc -> {
115 | Log.d(TAG, cc.address);
116 | Log.d(TAG, cc.nickname);
117 | });
118 | //Log.d(TAG, String.valueOf(msg.mainBody == null)); load方法不加载邮件正文内容
119 | });
120 | }, e -> {
121 | //加载失败
122 | Log.d(TAG, e.getMessage());
123 | });
124 | ```
125 |
126 | **同步文件夹中的邮件头**
127 |
128 | 本地每隔一段时间就应该与邮件服务器进行一次邮件同步,同步主要是查询邮件服务器是否有新邮件和本地已缓存过的邮件在服务器中是否有被删除,使用说明:
129 | + 参数localUIDArray为本地客户端已缓存的全部邮件UID。
130 | + 若发现服务端有邮件的UID比数组localUIDArray中的最大UID还要大,则拉取该邮件消息(新邮件)。
131 | + 若发现数组localUIDArray中的某一个UID值在服务端中不存在,则返回该UID。
132 | + 假设服务端[6, 5, 4, 2, 1],客户端[4, 3, 2],同步该服务端结果:新消息[6, 5],已删除[3]。
133 | ```java
134 | //本地已缓存邮件消息的uid
135 | long[] localUIDArray = new long[]{1, 2, 3, 4, 5, 6};
136 | //如果本地还没有缓存过邮件,传入一个空数组既不同步邮件,也不拉取邮件
137 | //long[] longs = new long[0];
138 |
139 | MailKit.IMAP imap = new MailKit.IMAP(config);
140 | MailKit.IMAP.Folder folder = imap.getFolder("INBOX");
141 | folder.sync(localUIDArray, (msgList, longs) -> {
142 | //同步成功,执行后续操作
143 | //获取新邮件
144 | msgList.forEach(msg -> {
145 | Log.d(TAG, String.valueOf(msg.uid));
146 | Log.d(TAG, String.valueOf(msg.sentDate));
147 | Log.d(TAG, String.valueOf(msg.flags.isSeen));
148 | Log.d(TAG, String.valueOf(msg.flags.isStar));
149 | Log.d(TAG, msg.subject);
150 | Log.d(TAG, msg.from.address);
151 | Log.d(TAG, msg.from.nickname);
152 | msg.toList.forEach(to -> {
153 | Log.d(TAG, to.address);
154 | Log.d(TAG, to.nickname);
155 | });
156 | msg.ccList.forEach(cc -> {
157 | Log.d(TAG, cc.address);
158 | Log.d(TAG, cc.nickname);
159 | });
160 | //Log.d(TAG, String.valueOf(msg.mainBody == null)); sync方法不加载邮件正文内容
161 | });
162 | //本地需要删除的邮件UID
163 | longs.forEach(uid -> Log.i(TAG, String.valueOf(uid)));
164 | }, e -> {
165 | //同步失败
166 | Log.d(TAG, e.getMessage());
167 | });
168 | ```
169 |
170 | **通过网络读取邮件详情**
171 | ```java
172 | MailKit.IMAP imap = new MailKit.IMAP(config);
173 | MailKit.IMAP.Folder folder = imap.getFolder("INBOX");
174 |
175 | //假设UID = 1967;不支持获取文件内容的附件
176 | folder.getMsg(1967, msg -> {
177 | //读取成功,执行后续操作
178 | Log.d(TAG, String.valueOf(msg.uid));
179 | Log.d(TAG, String.valueOf(msg.sentDate));
180 | Log.d(TAG, String.valueOf(msg.flags.isSeen));
181 | Log.d(TAG, String.valueOf(msg.flags.isStar));
182 | Log.d(TAG, msg.subject);
183 | Log.d(TAG, msg.from.address);
184 | Log.d(TAG, msg.from.nickname);
185 | msg.toList.forEach(to -> {
186 | Log.d(TAG, to.address);
187 | Log.d(TAG, to.nickname);
188 | });
189 | msg.ccList.forEach(cc -> {
190 | Log.d(TAG, cc.address);
191 | Log.d(TAG, cc.nickname);
192 | });
193 | Log.d(TAG, msg.mainBody.type);
194 | Log.d(TAG, msg.mainBody.content);
195 | }, e -> {
196 | //读取失败
197 | Log.d(TAG, e.getMessage());
198 | });
199 | ```
200 |
201 | **\* 标记或移除邮件的star**
202 | ```java
203 | MailKit.IMAP imap = new MailKit.IMAP(config);
204 | MailKit.IMAP.Inbox inbox = imap.getInbox();
205 |
206 | //传入需要标记或移除star的邮件UID数组,需要star则为true,否则为false
207 | inbox.star(new long[]{1967}, true, () -> {
208 | //操作完成,执行后续操作
209 | }, e -> {
210 | //操作失败
211 | Log.d(TAG, e.getMessage());
212 | });
213 | ```
214 |
215 | **\* 标记邮件状态是否已读**
216 | ```java
217 | MailKit.IMAP imap = new MailKit.IMAP(config);
218 | MailKit.IMAP.Inbox inbox = imap.getInbox();
219 |
220 | //传入需要标记邮件UID数组,标记已读则为true,否则为false
221 | inbox.readStatus(new long[]{1967}, false, () -> {
222 | //操作完成,执行后续操作
223 | }, e -> {
224 | //操作失败
225 | Log.d(TAG, e.getMessage());
226 | });
227 | ```
228 |
229 | **\* 移动邮件到另一文件夹**
230 | ```java
231 | MailKit.IMAP imap = new MailKit.IMAP(config);
232 | MailKit.IMAP.Inbox inbox = imap.getInbox();
233 |
234 | //设置需要把邮件移动到的文件夹名称,传入邮件UID数组
235 | inbox.move("Deleted Messages", new long[]{1876}, () -> {
236 | //操作完成,执行后续操作
237 | }, e -> {
238 | //操作失败
239 | Log.d(TAG, e.getMessage());
240 | });
241 | ```
242 |
243 | **\* 彻底删除文件夹中的邮件**
244 | ```java
245 | MailKit.IMAP imap = new MailKit.IMAP(config);
246 | MailKit.IMAP.Inbox inbox = imap.getInbox();
247 |
248 | //传入需要彻底删除的邮件UID数组
249 | inbox.delete(new long[]{1966}, () -> {
250 | //操作完成,执行后续操作
251 | }, e -> {
252 | //操作失败
253 | Log.d(TAG, e.getMessage());
254 | });
255 | ```
256 |
257 | **\* 统计文件夹中的全部邮件数量和未读邮件的数量**
258 | ```java
259 | MailKit.IMAP imap = new MailKit.IMAP(config);
260 | MailKit.IMAP.Inbox inbox = imap.getInbox();
261 |
262 | //统计文件夹中的全部邮件数量和未读邮件的数量
263 | inbox.count((total, unread) -> {
264 | //操作完成,执行后续操作
265 | }, e -> {
266 | //操作失败
267 | Log.d(TAG, e.getMessage());
268 | });
269 | ```
270 |
271 | **\* 把草稿保存到草稿箱**
272 | ```java
273 | MailKit.Draft draft = new MailKit.Draft(d -> {
274 | d.to = new String[]{"to@outlook.com"};
275 | d.subject = "MailKit test";
276 | d.text = "Hello world";
277 | });
278 |
279 | MailKit.IMAP imap = new MailKit.IMAP(config);
280 | MailKit.IMAP.DraftBox draftBox = imap.getDraftBox();
281 | draftBox.save(draft, () -> {
282 | //保存成功,执行后续操作
283 | }, e -> {
284 | //保存失败
285 | Log.d(TAG, e.getMessage());
286 | });
287 | ```
288 |
289 | **\* 按邮件主题搜索邮件内容(部分邮件服务器供应商不支持)**
290 | ```java
291 | MailKit.IMAP imap = new MailKit.IMAP(config);
292 | MailKit.IMAP.Inbox inbox = imap.getInbox();
293 |
294 | String subject = "需要搜索的邮件主题";
295 | inbox.searchBySubject(subject, msgList -> {
296 | //搜索成功,执行后续操作
297 | msgList.forEach(msg -> {
298 | Log.d(TAG, String.valueOf(msg.uid));
299 | Log.d(TAG, String.valueOf(msg.sentDate));
300 | Log.d(TAG, String.valueOf(msg.flags.isSeen));
301 | Log.d(TAG, String.valueOf(msg.flags.isStar));
302 | Log.d(TAG, msg.subject);
303 | Log.d(TAG, msg.from.address);
304 | Log.d(TAG, msg.from.nickname);
305 | msg.toList.forEach(to -> {
306 | Log.d(TAG, to.address);
307 | Log.d(TAG, to.nickname);
308 | });
309 | msg.ccList.forEach(cc -> {
310 | Log.d(TAG, cc.address);
311 | Log.d(TAG, cc.nickname);
312 | });
313 | //Log.d(TAG, String.valueOf(msg.mainBody == null)); 不支持获取邮件正文内容
314 | });
315 | }, e -> {
316 | //搜索失败
317 | Log.d(TAG, e.getMessage());
318 | });
319 | ```
320 |
321 | **\* 按发件人昵称搜索邮件内容(部分邮件服务器供应商不支持)**
322 | ```java
323 | MailKit.IMAP imap = new MailKit.IMAP(config);
324 | MailKit.IMAP.Inbox inbox = imap.getInbox();
325 |
326 | String nickname = "Lisa";
327 | inbox.searchByFrom(nickname, msgList -> {
328 | //搜索成功,执行后续操作
329 | }, e -> {
330 | //搜索失败
331 | Log.d(TAG, e.getMessage());
332 | });
333 | ```
334 |
335 | **\* 按收件人昵称搜索邮件内容(部分邮件服务器供应商不支持)**
336 | ```java
337 | MailKit.IMAP imap = new MailKit.IMAP(config);
338 | MailKit.IMAP.Inbox inbox = imap.getInbox();
339 |
340 | String nickname = "Li Hua";
341 | inbox.searchByTo(nickname, msgList -> {
342 | //搜索成功,执行后续操作
343 | }, e -> {
344 | //搜索失败
345 | Log.d(TAG, e.getMessage());
346 | });
347 | ```
348 |
349 | ## App中用到的开源项目
350 | + [QuickIO](https://github.com/artbits/quickio)
351 | + [SmartRefreshLayout](https://github.com/scwang90/SmartRefreshLayout)
352 | + [BaseRecyclerViewAdapterHelper](https://github.com/CymChad/BaseRecyclerViewAdapterHelper)
353 |
354 |
355 | ## License
356 | ```
357 | Copyright 2018 Zhang Guanhu
358 |
359 | Licensed under the Apache License, Version 2.0 (the "License");
360 | you may not use this file except in compliance with the License.
361 | You may obtain a copy of the License at
362 |
363 | http://www.apache.org/licenses/LICENSE-2.0
364 |
365 | Unless required by applicable law or agreed to in writing, software
366 | distributed under the License is distributed on an "AS IS" BASIS,
367 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
368 | See the License for the specific language governing permissions and
369 | limitations under the License.
370 | ```
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | namespace 'com.github.artbits.androidmail'
7 | compileSdk 33
8 |
9 | defaultConfig {
10 | applicationId "com.github.artbits.androidmail"
11 | minSdk 26
12 | targetSdk 33
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | buildFeatures {
30 | dataBinding = true
31 | }
32 | packagingOptions {
33 | pickFirst 'META-INF/LICENSE.md' // picks the JavaMail license file
34 | exclude 'META-INF/DEPENDENCIES'
35 | exclude 'META-INF/LICENSE'
36 | exclude 'META-INF/LICENSE.txt'
37 | exclude 'META-INF/license.txt'
38 | exclude 'META-INF/NOTICE'
39 | exclude 'META-INF/NOTICE.txt'
40 | exclude 'META-INF/NOTICE.md'
41 | exclude 'META-INF/notice.txt'
42 | exclude 'META-INF/ASL2.0'
43 | exclude("META-INF/*.kotlin_module")
44 | }
45 | configurations {
46 | all {
47 | exclude group: 'com.google.guava', module: 'listenablefuture'
48 | }
49 | }
50 | }
51 |
52 | dependencies {
53 | implementation project(path: ':mailkit')
54 | implementation 'androidx.appcompat:appcompat:1.4.1'
55 | implementation 'com.google.android.material:material:1.5.0'
56 |
57 | implementation 'com.github.artbits:quickio:1.3.4'
58 | implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
59 | implementation 'io.github.scwang90:refresh-layout-kernel:2.0.5'
60 | implementation 'io.github.scwang90:refresh-header-classics:2.0.5'
61 | implementation 'io.github.scwang90:refresh-footer-classics:2.0.5'
62 |
63 | testImplementation 'junit:junit:4.13.2'
64 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
65 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
66 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/github/artbits/androidmail/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 | assertEquals("com.github.artbit.email", appContext.getPackageName());
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/App.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail;
2 |
3 | import android.app.Application;
4 |
5 | import com.github.artbits.quickio.api.DB;
6 | import com.github.artbits.quickio.core.Config;
7 | import com.github.artbits.quickio.core.QuickIO;
8 |
9 | public class App extends Application {
10 |
11 | public static DB db;
12 |
13 |
14 | @Override
15 | public void onCreate() {
16 | super.onCreate();
17 | String dbName = "store";
18 | String basePath = getExternalFilesDir(null).getAbsolutePath();
19 | db = QuickIO.usingDB(Config.of(c -> c.path(basePath).name(dbName)));
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/Utils.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.icu.text.SimpleDateFormat;
6 | import android.widget.Toast;
7 |
8 | import java.util.Date;
9 |
10 | public class Utils {
11 |
12 | @SuppressLint("SimpleDateFormat")
13 | private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-M-dd HH:mm");
14 |
15 |
16 | public static boolean isNullOrEmpty(Object... args) {
17 | for (Object o : args) {
18 | if (o == null) {
19 | return true;
20 | }
21 | if (o instanceof String && ((String) o).isEmpty()) {
22 | return true;
23 | }
24 | }
25 | return false;
26 | }
27 |
28 |
29 | public static void toast(Context context, String s) {
30 | Toast.makeText(context, s, Toast.LENGTH_SHORT).show();
31 | }
32 |
33 |
34 | public static String getDate(long time) {
35 | return format.format(new Date(time));
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/store/Folder.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.store;
2 |
3 | import com.github.artbits.quickio.core.IOEntity;
4 |
5 | public final class Folder extends IOEntity {
6 | public String name;
7 |
8 | public Folder() { }
9 |
10 | public Folder(String name) {
11 | this.name = name;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/store/Message.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.store;
2 |
3 | import com.github.artbits.quickio.core.IOEntity;
4 |
5 | import java.util.function.Consumer;
6 |
7 | public final class Message extends IOEntity {
8 | public Long uid;
9 | public Long sentDate;
10 | public String folderName;
11 | public String subject;
12 | public String fromAddress;
13 | public String fromNickname;
14 | public String toAddress;
15 | public String toNickname;
16 | public String type;
17 | public String content;
18 |
19 | public static Message of(Consumer consumer) {
20 | Message message = new Message();
21 | consumer.accept(message);
22 | return message;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/store/UserInfo.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.store;
2 |
3 | import com.github.artbits.mailkit.MailKit;
4 | import com.github.artbits.quickio.core.IOEntity;
5 |
6 | import java.util.function.Consumer;
7 |
8 | public final class UserInfo extends IOEntity {
9 | public String account;
10 | public String password;
11 | public String nickname;
12 | public String SMTPHost;
13 | public String IMAPHost;
14 | public Integer SMTPPort;
15 | public Integer IMAPPort;
16 | public Boolean SMTPSSLEnable;
17 | public Boolean IMAPSSLEnable;
18 |
19 | public static UserInfo of(Consumer consumer) {
20 | UserInfo userInfo1 = new UserInfo();
21 | consumer.accept(userInfo1);
22 | return userInfo1;
23 | }
24 |
25 | public MailKit.Config toConfig() {
26 | return new MailKit.Config(c -> {
27 | c.account = account;
28 | c.password = password;
29 | c.nickname = nickname;
30 | c.SMTPHost = SMTPHost;
31 | c.SMTPPort = SMTPPort;
32 | c.IMAPHost = IMAPHost;
33 | c.IMAPPort = IMAPPort;
34 | c.SMTPSSLEnable = SMTPSSLEnable;
35 | c.IMAPSSLEnable = IMAPSSLEnable;
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/view/BaseActivity.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.view;
2 |
3 | import android.app.Activity;
4 | import android.app.AlertDialog;
5 | import android.app.ProgressDialog;
6 | import android.content.DialogInterface;
7 | import android.os.Bundle;
8 | import android.util.DisplayMetrics;
9 | import android.view.WindowManager;
10 |
11 | import androidx.annotation.NonNull;
12 | import androidx.appcompat.app.AppCompatActivity;
13 | import androidx.appcompat.widget.Toolbar;
14 | import androidx.databinding.DataBindingUtil;
15 | import androidx.databinding.ViewDataBinding;
16 |
17 | import java.util.Objects;
18 | import java.util.function.BiConsumer;
19 |
20 | public class BaseActivity extends AppCompatActivity {
21 |
22 | @Override
23 | protected void onCreate(Bundle savedInstanceState) {
24 | super.onCreate(savedInstanceState);
25 | }
26 |
27 |
28 | public T setContentView(@NonNull Activity activity, int layoutId) {
29 | return DataBindingUtil.setContentView(activity, layoutId);
30 | }
31 |
32 |
33 | public void setToolbar(Toolbar toolbar, boolean showBackButton) {
34 | setSupportActionBar(toolbar);
35 | Objects.requireNonNull(getSupportActionBar());
36 | getSupportActionBar().setDisplayHomeAsUpEnabled(showBackButton);
37 | getSupportActionBar().setHomeButtonEnabled(showBackButton);
38 | }
39 |
40 |
41 | public void setToolbar(Toolbar toolbar, String title, boolean showBackButton) {
42 | toolbar.setTitle(title);
43 | setSupportActionBar(toolbar);
44 | Objects.requireNonNull(getSupportActionBar());
45 | getSupportActionBar().setDisplayHomeAsUpEnabled(showBackButton);
46 | getSupportActionBar().setHomeButtonEnabled(showBackButton);
47 | }
48 |
49 |
50 | public void setToolbar(Toolbar toolbar, String title, String subtitle, boolean showBackButton) {
51 | toolbar.setTitle(title);
52 | toolbar.setSubtitle(subtitle);
53 | setSupportActionBar(toolbar);
54 | Objects.requireNonNull(getSupportActionBar());
55 | getSupportActionBar().setDisplayHomeAsUpEnabled(showBackButton);
56 | getSupportActionBar().setHomeButtonEnabled(showBackButton);
57 | }
58 |
59 |
60 | public class LoadingDialog {
61 |
62 | private final ProgressDialog dialog;
63 |
64 | public LoadingDialog() {
65 | dialog = new ProgressDialog(BaseActivity.this);
66 | dialog.setCancelable(false);
67 | }
68 |
69 | public LoadingDialog setTipWord(String s) {
70 | dialog.setMessage(s);
71 | return this;
72 | }
73 |
74 | public void show() {
75 | dialog.show();
76 | DisplayMetrics dm = new DisplayMetrics();
77 | WindowManager manager = BaseActivity.this.getWindowManager();
78 | manager.getDefaultDisplay().getMetrics(dm);
79 | WindowManager.LayoutParams params = Objects.requireNonNull(dialog.getWindow()).getAttributes();
80 | params.width = (int) (dm.widthPixels * 0.75);
81 | params.dimAmount = 0.4f;
82 | dialog.getWindow().setAttributes(params);
83 | }
84 |
85 | public void dismiss() {
86 | dialog.dismiss();
87 | }
88 |
89 | }
90 |
91 |
92 | public class MessageDialog {
93 |
94 | private final AlertDialog.Builder builder;
95 |
96 | public MessageDialog() {
97 | builder = new AlertDialog.Builder(BaseActivity.this);
98 | }
99 |
100 | public MessageDialog setTitle(String s) {
101 | builder.setTitle(s);
102 | return this;
103 | }
104 |
105 | public MessageDialog setMessage(String s) {
106 | builder.setMessage(s);
107 | return this;
108 | }
109 |
110 | public MessageDialog setPositiveButton(String s, BiConsumer consumer) {
111 | builder.setPositiveButton(s, consumer::accept);
112 | return this;
113 | }
114 |
115 | public MessageDialog setNegativeButton(String s, BiConsumer consumer) {
116 | builder.setNegativeButton(s, consumer::accept);
117 | return this;
118 | }
119 |
120 | public void show() {
121 | AlertDialog dialog = builder.create();
122 | dialog.show();
123 | WindowManager.LayoutParams params = Objects.requireNonNull(dialog.getWindow()).getAttributes();
124 | params.dimAmount = 0.4f;
125 | dialog.getWindow().setAttributes(params);
126 | }
127 |
128 | }
129 |
130 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/view/ConfigActivity.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.view;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.view.Menu;
6 | import android.view.MenuItem;
7 |
8 | import com.github.artbits.androidmail.App;
9 | import com.github.artbits.androidmail.R;
10 | import com.github.artbits.androidmail.Utils;
11 | import com.github.artbits.androidmail.databinding.ActivityConfigBinding;
12 | import com.github.artbits.androidmail.store.UserInfo;
13 | import com.github.artbits.mailkit.MailKit;
14 |
15 | public class ConfigActivity extends BaseActivity {
16 |
17 | private ActivityConfigBinding binding;
18 | private UserInfo userInfo;
19 |
20 |
21 | @Override
22 | protected void onCreate(Bundle savedInstanceState) {
23 | super.onCreate(savedInstanceState);
24 | binding = setContentView(this, R.layout.activity_config);
25 |
26 | userInfo = App.db.collection(UserInfo.class).findFirst();
27 | boolean isLogin = (userInfo != null);
28 | setToolbar(binding.toolbar, "服务器配置", isLogin);
29 | if (isLogin) {
30 | initData(userInfo);
31 | }
32 | }
33 |
34 |
35 | @Override
36 | public boolean onCreateOptionsMenu(Menu menu) {
37 | getMenuInflater().inflate(R.menu.menu_config, menu);
38 | return true;
39 | }
40 |
41 |
42 | @Override
43 | public boolean onOptionsItemSelected(MenuItem item) {
44 | if (item.getItemId() == android.R.id.home) {
45 | finish();
46 | }
47 | if (item.getItemId() == R.id.config_confirm) {
48 | auth();
49 | }
50 | return super.onOptionsItemSelected(item);
51 | }
52 |
53 |
54 | private void initData(UserInfo userInfo) {
55 | binding.accountText.setText(userInfo.account);
56 | binding.passwordText.setText(userInfo.password);
57 | binding.nicknameText.setText(userInfo.nickname);
58 | binding.smtpHostText.setText(userInfo.SMTPHost);
59 | binding.smtpPortText.setText(String.valueOf(userInfo.SMTPPort));
60 | binding.imapHostText.setText(userInfo.IMAPHost);
61 | binding.imapPortText.setText(String.valueOf(userInfo.IMAPPort));
62 | binding.smtpEncryptionSwt.setChecked(userInfo.SMTPSSLEnable);
63 | binding.imapEncryptionSwt.setChecked(userInfo.IMAPSSLEnable);
64 | }
65 |
66 |
67 | private void auth() {
68 | String account = binding.accountText.getText().toString();
69 | String password = binding.passwordText.getText().toString();
70 | String nickname = binding.nicknameText.getText().toString();
71 | String smtpHost = binding.smtpHostText.getText().toString();
72 | String smtpPort = binding.smtpPortText.getText().toString();
73 | String imapHost = binding.imapHostText.getText().toString();
74 | String imapPort = binding.imapPortText.getText().toString();
75 | if (Utils.isNullOrEmpty(account, password, nickname, smtpHost, smtpPort, imapHost, imapPort)) {
76 | Utils.toast(this, "配置参数都不能为空");
77 | return;
78 | }
79 |
80 | MailKit.Config config = new MailKit.Config(c -> {
81 | c.account = account;
82 | c.password = password;
83 | c.nickname = nickname;
84 | c.SMTPHost = smtpHost;
85 | c.IMAPHost = imapHost;
86 | c.SMTPPort = Integer.valueOf(smtpPort);
87 | c.IMAPPort = Integer.valueOf(imapPort);
88 | c.SMTPSSLEnable = binding.smtpEncryptionSwt.isChecked();
89 | c.IMAPSSLEnable = binding.imapEncryptionSwt.isChecked();
90 | });
91 |
92 | LoadingDialog dialog = new LoadingDialog();
93 | dialog.setTipWord("检查邮箱配置中...");
94 | dialog.show();
95 |
96 | MailKit.auth(config, () -> {
97 | if (userInfo == null) {
98 | App.db.collection(UserInfo.class).save(UserInfo.of(u -> {
99 | u.account = config.account;
100 | u.password = config.password;
101 | u.nickname = config.nickname;
102 | u.SMTPHost = config.SMTPHost;
103 | u.SMTPPort = config.SMTPPort;
104 | u.IMAPHost = config.IMAPHost;
105 | u.IMAPPort = config.IMAPPort;
106 | u.SMTPSSLEnable = config.SMTPSSLEnable;
107 | u.IMAPSSLEnable = config.IMAPSSLEnable;
108 | }));
109 | dialog.dismiss();
110 | startActivity(new Intent(this, MainActivity.class));
111 | finish();
112 | } else {
113 | userInfo.account = config.account;
114 | userInfo.password = config.password;
115 | userInfo.nickname = config.nickname;
116 | userInfo.SMTPHost = config.SMTPHost;
117 | userInfo.SMTPPort = config.SMTPPort;
118 | userInfo.IMAPHost = config.IMAPHost;
119 | userInfo.IMAPPort = config.IMAPPort;
120 | userInfo.SMTPSSLEnable = config.SMTPSSLEnable;
121 | userInfo.IMAPSSLEnable = config.IMAPSSLEnable;
122 | App.db.collection(UserInfo.class).save(userInfo);
123 | finish();
124 | }
125 | }, e -> {
126 | dialog.dismiss();
127 | Utils.toast(this, e.getMessage());
128 | });
129 | }
130 |
131 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/view/DetailsActivity.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.view;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.os.Bundle;
5 | import android.text.TextUtils;
6 | import android.view.MenuItem;
7 | import android.view.View;
8 | import android.webkit.WebChromeClient;
9 | import android.webkit.WebSettings;
10 | import android.webkit.WebView;
11 |
12 | import com.github.artbits.androidmail.App;
13 | import com.github.artbits.androidmail.R;
14 | import com.github.artbits.androidmail.databinding.ActivityDetailsBinding;
15 | import com.github.artbits.androidmail.Utils;
16 | import com.github.artbits.androidmail.store.Message;
17 | import com.github.artbits.androidmail.store.UserInfo;
18 | import com.github.artbits.mailkit.MailKit;
19 |
20 | import java.util.Objects;
21 |
22 | public class DetailsActivity extends BaseActivity {
23 |
24 | private ActivityDetailsBinding binding;
25 |
26 |
27 | @Override
28 | protected void onCreate(Bundle savedInstanceState) {
29 | super.onCreate(savedInstanceState);
30 | binding = setContentView(this, R.layout.activity_details);
31 | init();
32 | }
33 |
34 |
35 | @Override
36 | protected void onDestroy() {
37 | binding.webView.destroy();
38 | super.onDestroy();
39 | }
40 |
41 |
42 | @Override
43 | public boolean onOptionsItemSelected(MenuItem item) {
44 | if (item.getItemId() == android.R.id.home) {
45 | finish();
46 | }
47 | return super.onOptionsItemSelected(item);
48 | }
49 |
50 |
51 | @SuppressLint("SetJavaScriptEnabled")
52 | private void init() {
53 | long uid = getIntent().getLongExtra("uid", -1);
54 | String folderName = getIntent().getStringExtra("folderName");
55 | Message message = App.db.collection(Message.class).findOne(m -> {
56 | boolean b1 = Objects.equals(folderName, m.folderName);
57 | boolean b2 = Objects.equals(uid, m.uid);
58 | return b1 && b2;
59 | });
60 |
61 | setToolbar(binding.toolbar, "", true);
62 | binding.subjectText.setText(TextUtils.isEmpty(message.subject) ? "(无主题)" : message.subject);
63 | binding.fromNicknameText.setText(message.fromNickname);
64 | binding.fromAddressText.setText(message.fromAddress);
65 | binding.toNicknameText.setText(message.toNickname);
66 | binding.toAddressText.setText(message.toAddress);
67 | binding.dateText.setText(Utils.getDate(message.sentDate));
68 |
69 | WebSettings webSettings = binding.webView.getSettings();
70 | webSettings.setLoadsImagesAutomatically(true);
71 | webSettings.setJavaScriptEnabled(true);
72 | webSettings.setUseWideViewPort(true);
73 | webSettings.setLoadWithOverviewMode(true);
74 | webSettings.setSupportZoom(true);
75 | webSettings.setBuiltInZoomControls(true);
76 | webSettings.setDisplayZoomControls(false);
77 | binding.webView.setHorizontalScrollBarEnabled(false);
78 | binding.webView.setVerticalScrollBarEnabled(false);
79 | binding.webView.setInitialScale(25);
80 | binding.webView.setWebChromeClient(new WebChromeClient() {
81 | @Override
82 | public void onProgressChanged(WebView view, int newProgress) {
83 | super.onProgressChanged(view, newProgress);
84 | if (newProgress == 100 || message.content != null) {
85 | binding.progressBar.setVisibility(View.GONE);
86 | }
87 | }
88 | });
89 |
90 | if (message.content != null) {
91 | String content = message.content;
92 | String type = message.type;
93 | binding.webView.loadDataWithBaseURL(null, adaptScreen(content, type), "text/html", "utf-8", null);
94 | } else {
95 | UserInfo userInfo = App.db.collection(UserInfo.class).findFirst();
96 | if (userInfo == null) {
97 | return;
98 | }
99 | MailKit.IMAP imap = new MailKit.IMAP(userInfo.toConfig());
100 | MailKit.IMAP.Folder folder = imap.getFolder(folderName);
101 | folder.getMsg(uid, msg -> {
102 | if (msg.mainBody != null) {
103 | message.content = msg.mainBody.content;
104 | message.type = msg.mainBody.type;
105 | App.db.collection(Message.class).save(message);
106 | binding.webView.loadDataWithBaseURL(null, adaptScreen(message.content, message.type), "text/html", "utf-8", null);
107 | }
108 | }, e -> Utils.toast(this, e.getMessage()));
109 | }
110 | }
111 |
112 |
113 | private static String adaptScreen(String s, String type) {
114 | if (type.equals("text/html")) {
115 | return "\n" +
116 | "\n" +
117 | " \n" +
118 | "\n" +
119 | "\n" + s + "\n" +
120 | "";
121 | } else {
122 | return "\n" +
123 | "\n" +
124 | " \n" +
125 | "\n" +
126 | "\n" +
127 | "" + s + "\n" +
128 | "\n" +
129 | "";
130 | }
131 | }
132 |
133 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/view/FolderActivity.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.view;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.text.TextUtils;
6 | import android.view.MenuItem;
7 |
8 | import androidx.annotation.NonNull;
9 | import androidx.annotation.Nullable;
10 | import androidx.recyclerview.widget.LinearLayoutManager;
11 |
12 | import com.chad.library.adapter.base.BaseQuickAdapter;
13 | import com.chad.library.adapter.base.viewholder.BaseViewHolder;
14 | import com.github.artbits.androidmail.App;
15 | import com.github.artbits.androidmail.R;
16 | import com.github.artbits.androidmail.Utils;
17 | import com.github.artbits.androidmail.databinding.ActivityFolderBinding;
18 | import com.github.artbits.androidmail.store.Message;
19 | import com.github.artbits.androidmail.store.UserInfo;
20 | import com.github.artbits.mailkit.MailKit;
21 | import com.scwang.smart.refresh.layout.api.RefreshLayout;
22 | import com.scwang.smart.refresh.layout.listener.OnRefreshLoadMoreListener;
23 |
24 | import java.util.ArrayList;
25 | import java.util.List;
26 | import java.util.Map;
27 | import java.util.Objects;
28 | import java.util.stream.Collectors;
29 |
30 | public class FolderActivity extends BaseActivity {
31 |
32 | private ActivityFolderBinding binding;
33 | private MessageAdapter adapter;
34 | private String folderName;
35 |
36 | private long minUID;
37 | private boolean isEmpty;
38 |
39 | private MailKit.IMAP.Folder folder;
40 |
41 |
42 | @Override
43 | protected void onCreate(Bundle savedInstanceState) {
44 | super.onCreate(savedInstanceState);
45 | binding = setContentView(this, R.layout.activity_folder);
46 | init();
47 | }
48 |
49 |
50 | @Override
51 | public boolean onOptionsItemSelected(MenuItem item) {
52 | if (item.getItemId() == android.R.id.home) {
53 | finish();
54 | }
55 | return super.onOptionsItemSelected(item);
56 | }
57 |
58 |
59 | private void init() {
60 | folderName = getIntent().getStringExtra("folderName");
61 | setToolbar(binding.toolbar, folderName, true);
62 |
63 | binding.refreshLayout.setOnRefreshLoadMoreListener(new OnRefreshLoadMoreListener() {
64 | @Override
65 | public void onRefresh(@NonNull RefreshLayout refreshLayout) {
66 | refreshData(refreshLayout);
67 | }
68 |
69 | @Override
70 | public void onLoadMore(@NonNull RefreshLayout refreshLayout) {
71 | loadData(refreshLayout);
72 | }
73 | });
74 |
75 | adapter = new MessageAdapter(new ArrayList<>());
76 | LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
77 | binding.msgRecyclerView.setLayoutManager(linearLayoutManager);
78 | binding.msgRecyclerView.setAdapter(adapter);
79 | adapter.setOnItemClickListener((adapter, view, position) -> {
80 | Message message = (Message) adapter.getItem(position);
81 | Intent intent = new Intent(this, DetailsActivity.class)
82 | .putExtra("folderName", folderName)
83 | .putExtra("uid", message.uid);
84 | startActivity(intent);
85 | });
86 |
87 | List messages = getMessage(folderName);
88 | adapter.setNewData(messages);
89 | isEmpty = (messages.size() == 0);
90 | minUID = (isEmpty) ? -1 : messages.get(messages.size()-1).uid;
91 |
92 | UserInfo userInfo = App.db.collection(UserInfo.class).findFirst();
93 | if (userInfo != null) {
94 | MailKit.Config config = userInfo.toConfig();
95 | MailKit.IMAP imap = new MailKit.IMAP(config);
96 | folder = imap.getFolder(folderName);
97 | }
98 |
99 | binding.refreshLayout.autoRefresh();
100 | }
101 |
102 |
103 | private void refreshData(RefreshLayout refreshLayout) {
104 | if (folder == null) return;
105 |
106 | if (isEmpty) {
107 | folder.load(minUID, msgList -> {
108 | saveMessages(folderName, msgList);
109 | List messages = getMessage(folderName);
110 | adapter.setNewData(messages);
111 | isEmpty = false;
112 | minUID = (msgList.size() == 0) ? minUID : msgList.get(msgList.size()-1).uid;
113 | refreshLayout.finishRefresh();
114 | }, e -> {
115 | Utils.toast(this, e.getMessage());
116 | refreshLayout.finishRefresh();
117 | });
118 | } else {
119 | long[] localUIDArray = getLocalUIDArray(folderName);
120 | folder.sync(localUIDArray, (newMsgList, delUIDArray) -> {
121 | saveMessages(folderName, newMsgList);
122 | delMessages(folderName, delUIDArray);
123 | List messages = getMessage(folderName);
124 | adapter.setNewData(messages);
125 | refreshLayout.finishRefresh();
126 | }, e -> {
127 | Utils.toast(this, e.getMessage());
128 | refreshLayout.finishRefresh();
129 | });
130 | }
131 | }
132 |
133 |
134 | private void loadData(RefreshLayout refreshLayout) {
135 | if (folder == null) return;
136 |
137 | folder.load(minUID, msgList -> {
138 | List messages = saveMessages(folderName, msgList);
139 | adapter.addData(messages);
140 | minUID = (msgList.size() == 0) ? minUID : msgList.get(msgList.size()-1).uid;
141 | refreshLayout.finishLoadMore();
142 | }, e -> {
143 | Utils.toast(this, e.getMessage());
144 | refreshLayout.finishLoadMore();
145 | });
146 | }
147 |
148 |
149 | private List getMessage(String folderName) {
150 | return App.db.collection(Message.class).find(m -> Objects.equals(folderName, m.folderName), opt -> opt.sort("uid", -1));
151 | }
152 |
153 |
154 | private long[] getLocalUIDArray(String folderName) {
155 | List messages = App.db.collection(Message.class).find(m -> Objects.equals(folderName, m.folderName));
156 | long[] longs = new long[messages.size()];
157 | for (int i = 0, size = messages.size(); i < size; i++) {
158 | longs[i] = messages.get(i).uid;
159 | }
160 | return longs;
161 | }
162 |
163 |
164 | private void delMessages(String folderName, List uidList) {
165 | Map map = uidList.stream().collect(Collectors.toMap(uid -> uid, uid -> true));
166 | App.db.collection(Message.class).delete(m -> {
167 | boolean b1 = Objects.equals(folderName, m.folderName);
168 | boolean b2 = Boolean.TRUE.equals(map.getOrDefault(m.uid, false));
169 | return b1 && b2;
170 | });
171 | }
172 |
173 |
174 | private List saveMessages(String folderName, List msgList) {
175 | List messages = msgList.stream().map(msg -> Message.of(m -> {
176 | m.folderName = folderName;
177 | m.uid = msg.uid;
178 | m.sentDate = msg.sentDate;
179 | m.subject = msg.subject;
180 | m.fromAddress = msg.from.address;
181 | m.fromNickname = msg.from.nickname;
182 | m.toAddress = msg.toList.get(0).address;
183 | m.toNickname = msg.toList.get(0).nickname;
184 | })).collect(Collectors.toList());
185 | App.db.collection(Message.class).save(messages);
186 | return messages;
187 | }
188 |
189 |
190 | private static class MessageAdapter extends BaseQuickAdapter {
191 |
192 | public MessageAdapter(@Nullable List data) {
193 | super(R.layout.item_message, data);
194 | }
195 |
196 | @Override
197 | protected void convert(@NonNull BaseViewHolder holder, Message message) {
198 | holder.setText(R.id.nickname, message.fromNickname)
199 | .setText(R.id.subject, TextUtils.isEmpty(message.subject) ? "(无主题)" : message.subject)
200 | .setText(R.id.date, Utils.getDate(message.sentDate));
201 | }
202 |
203 | }
204 |
205 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/view/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.view;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.view.Menu;
6 | import android.view.MenuItem;
7 | import android.widget.ArrayAdapter;
8 |
9 | import com.github.artbits.androidmail.App;
10 | import com.github.artbits.androidmail.R;
11 | import com.github.artbits.androidmail.Utils;
12 | import com.github.artbits.androidmail.databinding.ActivityMainBinding;
13 | import com.github.artbits.androidmail.store.Folder;
14 | import com.github.artbits.androidmail.store.Message;
15 | import com.github.artbits.androidmail.store.UserInfo;
16 | import com.github.artbits.mailkit.MailKit;
17 |
18 | import java.util.ArrayList;
19 | import java.util.List;
20 |
21 | public class MainActivity extends BaseActivity {
22 |
23 | private ActivityMainBinding binding;
24 |
25 |
26 | @Override
27 | protected void onCreate(Bundle savedInstanceState) {
28 | super.onCreate(savedInstanceState);
29 | binding = setContentView(this, R.layout.activity_main);
30 | }
31 |
32 |
33 | @Override
34 | protected void onResume() {
35 | super.onResume();
36 | init();
37 | }
38 |
39 |
40 | @Override
41 | public boolean onCreateOptionsMenu(Menu menu) {
42 | getMenuInflater().inflate(R.menu.menu_main, menu);
43 | return true;
44 | }
45 |
46 |
47 | @Override
48 | public boolean onOptionsItemSelected(MenuItem item) {
49 | if (item.getItemId() == R.id.write) {
50 | startActivity(new Intent(this, WriteActivity.class));
51 | }
52 | if (item.getItemId() == R.id.settings) {
53 | startActivity(new Intent(this, ConfigActivity.class));
54 | }
55 | if (item.getItemId() == R.id.exit) {
56 | exit();
57 | }
58 | return super.onOptionsItemSelected(item);
59 | }
60 |
61 |
62 | private void init() {
63 | UserInfo userInfo = App.db.collection(UserInfo.class).findFirst();
64 | if (userInfo == null) {
65 | setToolbar(binding.toolbar, "Android-Mail", false);
66 | return;
67 | }
68 | setToolbar(binding.toolbar, userInfo.nickname, userInfo.account, false);
69 |
70 | List folders = App.db.collection(Folder.class).findAll();
71 | if (folders == null || folders.size() == 0) {
72 | MailKit.Config config = userInfo.toConfig();
73 | MailKit.IMAP imap = new MailKit.IMAP(config);
74 | imap.getDefaultFolders(strings -> {
75 | strings.forEach(s -> App.db.collection(Folder.class).save(new Folder(s)));
76 | ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, strings);
77 | binding.foldersListView.setAdapter(adapter);
78 | }, e -> Utils.toast(this, e.getMessage()));
79 | } else {
80 | List strings = new ArrayList<>();
81 | folders.forEach(folder -> strings.add(folder.name));
82 | ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, strings);
83 | binding.foldersListView.setAdapter(adapter);
84 | }
85 |
86 | binding.foldersListView.setOnItemClickListener((parent, view, position, id) -> {
87 | String folderName = parent.getAdapter().getItem(position).toString();
88 | Intent intent = new Intent(this, FolderActivity.class);
89 | intent.putExtra("folderName", folderName);
90 | startActivity(intent);
91 | });
92 | }
93 |
94 |
95 | private void exit() {
96 | new MessageDialog()
97 | .setTitle("退出帐户")
98 | .setMessage("退出帐户将会清除本地的帐户数据")
99 | .setNegativeButton("取消", (dialogInterface, integer) -> {})
100 | .setPositiveButton("退出", (dialogInterface, integer) -> {
101 | App.db.collection(UserInfo.class).deleteAll();
102 | App.db.collection(Folder.class).deleteAll();
103 | App.db.collection(Message.class).deleteAll();
104 | startActivity(new Intent(this, ConfigActivity.class));
105 | finish();
106 | }).show();
107 | }
108 |
109 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/view/SplashActivity.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.view;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 |
7 | import com.github.artbits.androidmail.App;
8 | import com.github.artbits.androidmail.store.UserInfo;
9 |
10 | @SuppressLint("CustomSplashScreen")
11 | public class SplashActivity extends BaseActivity {
12 |
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 | UserInfo userInfo = App.db.collection(UserInfo.class).findFirst();
17 | Class> cls = (userInfo != null) ? MainActivity.class : ConfigActivity.class;
18 | startActivity(new Intent(this, cls));
19 | finish();
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/artbits/androidmail/view/WriteActivity.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail.view;
2 |
3 | import android.os.Bundle;
4 | import android.view.Menu;
5 | import android.view.MenuItem;
6 |
7 | import com.github.artbits.androidmail.App;
8 | import com.github.artbits.androidmail.R;
9 | import com.github.artbits.androidmail.Utils;
10 | import com.github.artbits.androidmail.databinding.ActivityWriteBinding;
11 | import com.github.artbits.androidmail.store.UserInfo;
12 | import com.github.artbits.mailkit.MailKit;
13 |
14 | public class WriteActivity extends BaseActivity {
15 |
16 | private ActivityWriteBinding binding;
17 |
18 |
19 | @Override
20 | protected void onCreate(Bundle savedInstanceState) {
21 | super.onCreate(savedInstanceState);
22 | binding = setContentView(this, R.layout.activity_write);
23 | setToolbar(binding.toolbar, "写邮件", true);
24 | }
25 |
26 |
27 | @Override
28 | public boolean onCreateOptionsMenu(Menu menu) {
29 | getMenuInflater().inflate(R.menu.menu_write, menu);
30 | return true;
31 | }
32 |
33 |
34 | @Override
35 | public boolean onOptionsItemSelected(MenuItem item) {
36 | if (item.getItemId() == android.R.id.home) {
37 | finish();
38 | }
39 | if (item.getItemId() == R.id.send) {
40 | sendMail();
41 | }
42 | return super.onOptionsItemSelected(item);
43 | }
44 |
45 |
46 | private void sendMail() {
47 | String to = binding.addressText.getText().toString();
48 | String subject = binding.subjectText.getText().toString();
49 | String content = binding.contentText.getText().toString();
50 | if (Utils.isNullOrEmpty(to, subject, content)) {
51 | Utils.toast(this, "收件人地址、邮件主题或内容不能为空");
52 | return;
53 | }
54 |
55 | UserInfo userInfo = App.db.collection(UserInfo.class).findFirst();
56 | if (userInfo == null) {
57 | Utils.toast(this, "服务器配置异常,请重试");
58 | return;
59 | }
60 |
61 | LoadingDialog dialog = new LoadingDialog().setTipWord("发送中...");
62 | dialog.show();
63 |
64 | MailKit.Config config = userInfo.toConfig();
65 | MailKit.SMTP smtp = new MailKit.SMTP(config);
66 | smtp.send(new MailKit.Draft(d -> {
67 | d.to = new String[]{to};
68 | d.subject = subject;
69 | d.text = content;
70 | }), () -> {
71 | dialog.dismiss();
72 | Utils.toast(this, "发送成功");
73 | finish();
74 | }, e -> {
75 | dialog.dismiss();
76 | Utils.toast(this, e.getMessage());
77 | });
78 | }
79 |
80 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
14 |
15 |
18 |
19 |
23 |
24 |
33 |
34 |
39 |
40 |
47 |
48 |
59 |
60 |
61 |
62 |
66 |
67 |
74 |
75 |
86 |
87 |
88 |
89 |
93 |
94 |
101 |
102 |
112 |
113 |
114 |
115 |
124 |
125 |
130 |
131 |
138 |
139 |
149 |
150 |
151 |
152 |
156 |
157 |
164 |
165 |
176 |
177 |
178 |
179 |
183 |
184 |
191 |
192 |
199 |
200 |
201 |
202 |
211 |
212 |
217 |
218 |
225 |
226 |
236 |
237 |
238 |
239 |
243 |
244 |
251 |
252 |
263 |
264 |
265 |
266 |
270 |
271 |
278 |
279 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_details.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
14 |
15 |
19 |
20 |
24 |
25 |
30 |
31 |
41 |
42 |
51 |
52 |
63 |
64 |
74 |
75 |
84 |
85 |
96 |
97 |
107 |
108 |
117 |
118 |
129 |
130 |
131 |
132 |
139 |
140 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_folder.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
16 |
17 |
21 |
22 |
25 |
26 |
30 |
31 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_write.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
14 |
15 |
27 |
28 |
38 |
39 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_message.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
18 |
19 |
30 |
31 |
39 |
40 |
41 |
42 |
53 |
54 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_write.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Android-Mail
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/test/java/com/github/artbits/androidmail/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.androidmail;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id 'com.android.application' version '7.3.0' apply false
4 | id 'com.android.library' version '7.3.0' apply false
5 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Enables namespacing of each library's R class so that its R class includes only the
19 | # resources declared in the library itself and none from the library's dependencies,
20 | # thereby reducing the size of the R class for that library
21 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Oct 01 22:37:32 CST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/image/config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/image/config.png
--------------------------------------------------------------------------------
/image/details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/image/details.png
--------------------------------------------------------------------------------
/image/inbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/image/inbox.png
--------------------------------------------------------------------------------
/image/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/image/main.png
--------------------------------------------------------------------------------
/image/menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/image/menu.png
--------------------------------------------------------------------------------
/image/write.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/image/write.png
--------------------------------------------------------------------------------
/mailkit/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/mailkit/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | }
4 |
5 | android {
6 | namespace 'com.github.artbits.mailkit'
7 | compileSdk 33
8 |
9 | defaultConfig {
10 | minSdk 26
11 | targetSdk 33
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles "consumer-rules.pro"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | compileOptions {
24 | sourceCompatibility JavaVersion.VERSION_1_8
25 | targetCompatibility JavaVersion.VERSION_1_8
26 | }
27 | packagingOptions {
28 | pickFirst 'META-INF/LICENSE.md' // picks the JavaMail license file
29 | exclude 'META-INF/DEPENDENCIES'
30 | exclude 'META-INF/LICENSE'
31 | exclude 'META-INF/LICENSE.txt'
32 | exclude 'META-INF/license.txt'
33 | exclude 'META-INF/NOTICE'
34 | exclude 'META-INF/NOTICE.txt'
35 | exclude 'META-INF/NOTICE.md'
36 | exclude 'META-INF/notice.txt'
37 | exclude 'META-INF/ASL2.0'
38 | exclude("META-INF/*.kotlin_module")
39 | }
40 | }
41 |
42 | dependencies {
43 | testImplementation 'junit:junit:4.13.2'
44 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
45 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
46 |
47 | implementation 'com.sun.mail:android-mail:1.6.7'
48 | implementation 'com.sun.mail:android-activation:1.6.7'
49 | }
--------------------------------------------------------------------------------
/mailkit/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artbits/android-mail/31b2b592a80a3861c85da268fbf498c3e570c6a3/mailkit/consumer-rules.pro
--------------------------------------------------------------------------------
/mailkit/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/mailkit/src/androidTest/java/com/github/artbits/mailkit/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.mailkit;
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4;
4 |
5 | import org.junit.runner.RunWith;
6 |
7 | /**
8 | * Instrumented test, which will execute on an Android device.
9 | *
10 | * @see Testing documentation
11 | */
12 | @RunWith(AndroidJUnit4.class)
13 | public class ExampleInstrumentedTest {
14 |
15 | }
--------------------------------------------------------------------------------
/mailkit/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/mailkit/src/main/java/com/github/artbits/mailkit/AuthService.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.mailkit;
2 |
3 | import com.sun.mail.imap.IMAPStore;
4 |
5 | import java.util.function.Consumer;
6 |
7 | import javax.mail.MessagingException;
8 | import javax.mail.Transport;
9 |
10 | final class AuthService {
11 |
12 | private final MailKit.Config config;
13 |
14 |
15 | AuthService(MailKit.Config config) {
16 | this.config = config;
17 | }
18 |
19 |
20 | void auth(Runnable runnable, Consumer consumer) {
21 | MailKit.thread.execute(() -> {
22 | int count = 0;
23 | if (config.SMTPHost != null && config.SMTPPort != null) {
24 | try(Transport transport = Tools.getTransport(config)) {
25 | count++;
26 | } catch (MessagingException e) {
27 | MailKit.handler.post(() -> consumer.accept(e));
28 | }
29 | }
30 | if (config.IMAPHost != null && config.IMAPPort != null) {
31 | try(IMAPStore store = Tools.getStore(config)) {
32 | count++;
33 | } catch (MessagingException e) {
34 | MailKit.handler.post(() -> consumer.accept(e));
35 | }
36 | }
37 | if (count == 2) {
38 | MailKit.handler.post(runnable);
39 | }
40 | });
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/mailkit/src/main/java/com/github/artbits/mailkit/IMAPService.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.mailkit;
2 |
3 | import com.sun.mail.imap.IMAPStore;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.function.Consumer;
8 |
9 | import javax.mail.Folder;
10 | import javax.mail.MessagingException;
11 |
12 | class IMAPService {
13 |
14 | private final MailKit.Config config;
15 |
16 |
17 | IMAPService(MailKit.Config config) {
18 | this.config = config;
19 | }
20 |
21 |
22 | public void getDefaultFolders(Consumer> consumer1, Consumer consumer2) {
23 | MailKit.thread.execute(() -> {
24 | try(IMAPStore store = Tools.getStore(config)) {
25 | List folders = new ArrayList<>();
26 | for (Folder folder : store.getDefaultFolder().list()) {
27 | if (folder.list().length == 0) {
28 | folders.add(folder.getFullName());
29 | }
30 | }
31 | MailKit.handler.post(() -> consumer1.accept(folders));
32 | } catch (MessagingException e) {
33 | MailKit.handler.post(() -> consumer2.accept(e));
34 | }
35 | });
36 | }
37 |
38 |
39 | public MailKit.IMAP.Folder getFolder(String folderName) {
40 | return new MailKit.IMAP.Folder(config, folderName);
41 | }
42 |
43 |
44 | public MailKit.IMAP.Inbox getInbox() {
45 | return new MailKit.IMAP.Inbox(config, "INBOX");
46 | }
47 |
48 |
49 | public MailKit.IMAP.DraftBox getDraftBox() {
50 | return new MailKit.IMAP.DraftBox(config, "Drafts");
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/mailkit/src/main/java/com/github/artbits/mailkit/MailFolder.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.mailkit;
2 |
3 | import com.sun.mail.imap.IMAPFolder;
4 | import com.sun.mail.imap.IMAPMessage;
5 | import com.sun.mail.imap.IMAPStore;
6 |
7 | import java.net.MalformedURLException;
8 | import java.util.ArrayList;
9 | import java.util.List;
10 | import java.util.function.BiConsumer;
11 | import java.util.function.Consumer;
12 |
13 | import javax.mail.FetchProfile;
14 | import javax.mail.Flags;
15 | import javax.mail.Message;
16 | import javax.mail.MessagingException;
17 | import javax.mail.internet.MimeMessage;
18 | import javax.mail.search.FromStringTerm;
19 | import javax.mail.search.RecipientStringTerm;
20 | import javax.mail.search.SubjectTerm;
21 |
22 | class MailFolder {
23 |
24 | private final MailKit.Config config;
25 | private final String folderName;
26 |
27 |
28 | MailFolder(MailKit.Config config, String folderName) {
29 | this.config = config;
30 | this.folderName = folderName;
31 | }
32 |
33 |
34 | public void sync(long[] localUIDArray, BiConsumer, List> consumer1, Consumer consumer2) {
35 | MailKit.thread.execute(() -> {
36 | synchronized (MailFolder.this) {
37 | try(IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
38 | UIDHandler.Result result = UIDHandler.syncUIDArray(folder, localUIDArray);
39 | long[] newArray = result.newArray;
40 | long[] delArray = result.delArray;
41 | List newMsgList = new ArrayList<>();
42 | List delUIDList = new ArrayList<>();
43 | if (newArray.length > 0) {
44 | Message[] messages = folder.getMessagesByUID(newArray);
45 | FetchProfile fetchProfile = new FetchProfile();
46 | fetchProfile.add(FetchProfile.Item.ENVELOPE);
47 | fetchProfile.add(FetchProfile.Item.FLAGS);
48 | folder.fetch(messages, fetchProfile);
49 | for (Message message : messages) {
50 | IMAPMessage imapMessage = (IMAPMessage) message;
51 | long uid = folder.getUID(imapMessage);
52 | MailKit.Msg msg = Tools.getMsgHead(uid, imapMessage);
53 | if (msg != null) {
54 | newMsgList.add(msg);
55 | }
56 | }
57 | }
58 | for (long uid : delArray) {
59 | delUIDList.add(uid);
60 | }
61 | MailKit.handler.post(() -> consumer1.accept(newMsgList, delUIDList));
62 | } catch (MessagingException e) {
63 | MailKit.handler.post(() -> consumer2.accept(e));
64 | }
65 | }
66 | });
67 | }
68 |
69 |
70 | public void load(long minUID, Consumer> consumer1, Consumer consumer2) {
71 | MailKit.thread.execute(() -> {
72 | synchronized (MailFolder.this) {
73 | try(IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
74 | long[] uidList = UIDHandler.nextUIDArray(folder, minUID);
75 | Message[] messages = folder.getMessagesByUID(uidList);
76 | FetchProfile fetchProfile = new FetchProfile();
77 | fetchProfile.add(FetchProfile.Item.ENVELOPE);
78 | fetchProfile.add(FetchProfile.Item.FLAGS);
79 | folder.fetch(messages, fetchProfile);
80 | List msgList = new ArrayList<>();
81 | for (Message message: messages){
82 | IMAPMessage imapMessage = (IMAPMessage) message;
83 | long uid = folder.getUID(imapMessage);
84 | MailKit.Msg msg = Tools.getMsgHead(uid, imapMessage);
85 | if (msg != null) {
86 | msgList.add(msg);
87 | }
88 | }
89 | MailKit.handler.post(() -> consumer1.accept(msgList));
90 | } catch (MessagingException e) {
91 | MailKit.handler.post(() -> consumer2.accept(e));
92 | }
93 | }
94 | });
95 | }
96 |
97 |
98 | public void getMsg(long uid, Consumer consumer1, Consumer consumer2) {
99 | MailKit.thread.execute(() -> {
100 | try (IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
101 | IMAPMessage imapMessage = (IMAPMessage) folder.getMessageByUID(uid);
102 | if (imapMessage != null) {
103 | MailKit.Msg msg = Tools.toMsg(uid, imapMessage);
104 | MailKit.handler.post(() -> consumer1.accept(msg));
105 | }
106 | } catch (Exception e) {
107 | MailKit.handler.post(() -> consumer2.accept(e));
108 | }
109 | });
110 | }
111 |
112 |
113 | public void count(BiConsumer consumer1, Consumer consumer2) {
114 | MailKit.thread.execute(() -> {
115 | try (IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
116 | int total = folder.getMessageCount();
117 | int unreadCount = folder.getUnreadMessageCount();
118 | MailKit.handler.post(() -> consumer1.accept(total, unreadCount));
119 | } catch (MessagingException e) {
120 | MailKit.handler.post(() -> consumer2.accept(e));
121 | }
122 | });
123 | }
124 |
125 |
126 | public void move(String targetFolderName, long[] uidList, Runnable runnable, Consumer consumer) {
127 | MailKit.thread.execute(() -> {
128 | try (IMAPStore store = Tools.getStore(config);
129 | IMAPFolder originalFolder = Tools.getFolder(store, folderName, config);
130 | IMAPFolder targetFolder = Tools.getFolder(store, targetFolderName, config)) {
131 | Message[] msgList = originalFolder.getMessagesByUID(uidList);
132 | originalFolder.copyMessages(msgList, targetFolder);
133 | originalFolder.setFlags(msgList, new Flags(Flags.Flag.DELETED), true);
134 | MailKit.handler.post(runnable);
135 | } catch (MessagingException e) {
136 | MailKit.handler.post(() -> consumer.accept(e));
137 | }
138 | });
139 | }
140 |
141 |
142 | public void delete(long[] uidList, Runnable runnable, Consumer consumer) {
143 | MailKit.thread.execute(() -> {
144 | try (IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
145 | Message[] msgList = folder.getMessagesByUID(uidList);
146 | folder.setFlags(msgList, new Flags(Flags.Flag.DELETED), true);
147 | MailKit.handler.post(runnable);
148 | } catch (MessagingException e) {
149 | MailKit.handler.post(() -> consumer.accept(e));
150 | }
151 | });
152 | }
153 |
154 |
155 | public void star(long[] uidList, boolean status, Runnable runnable, Consumer consumer) {
156 | MailKit.thread.execute(() -> {
157 | try (IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
158 | Message[] msgList = folder.getMessagesByUID(uidList);
159 | folder.setFlags(msgList, new Flags(Flags.Flag.FLAGGED), status);
160 | MailKit.handler.post(runnable);
161 | } catch (MessagingException e) {
162 | MailKit.handler.post(() -> consumer.accept(e));
163 | }
164 | });
165 | }
166 |
167 |
168 | public void readStatus(long[] uidList, boolean status, Runnable runnable, Consumer consumer) {
169 | MailKit.thread.execute(() -> {
170 | try (IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
171 | Message[] msgList = folder.getMessagesByUID(uidList);
172 | folder.setFlags(msgList, new Flags(Flags.Flag.SEEN), status);
173 | MailKit.handler.post(runnable);
174 | } catch (MessagingException e) {
175 | MailKit.handler.post(() -> consumer.accept(e));
176 | }
177 | });
178 | }
179 |
180 |
181 | public void searchBySubject(String subject, Consumer> consumer1, Consumer consumer2) {
182 | MailKit.thread.execute(() -> {
183 | try (IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
184 | SubjectTerm subjectTerm = new SubjectTerm(subject);
185 | Message[] messages = folder.search(subjectTerm);
186 | FetchProfile fp = new FetchProfile();
187 | fp.add(FetchProfile.Item.ENVELOPE);
188 | folder.fetch(messages, fp);
189 | List msgList = Tools.getMsgHeads(folder, messages);
190 | MailKit.handler.post(() -> consumer1.accept(msgList));
191 | } catch (MessagingException e) {
192 | MailKit.handler.post(() -> consumer2.accept(e));
193 | }
194 | });
195 | }
196 |
197 |
198 | public void searchByFrom(String nickname, Consumer> consumer1, Consumer consumer2) {
199 | MailKit.thread.execute(() -> {
200 | try (IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
201 | FromStringTerm fromStringTerm = new FromStringTerm(nickname);
202 | Message[] messages = folder.search(fromStringTerm);
203 | FetchProfile fp = new FetchProfile();
204 | fp.add(FetchProfile.Item.ENVELOPE);
205 | folder.fetch(messages, fp);
206 | List msgList = Tools.getMsgHeads(folder, messages);
207 | MailKit.handler.post(() -> consumer1.accept(msgList));
208 | } catch (MessagingException e) {
209 | MailKit.handler.post(() -> consumer2.accept(e));
210 | }
211 | });
212 | }
213 |
214 |
215 | public void searchByTo(String nickname, Consumer> consumer1, Consumer consumer2) {
216 | MailKit.thread.execute(() -> {
217 | try (IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
218 | RecipientStringTerm stringTerm = new RecipientStringTerm(MimeMessage.RecipientType.TO, nickname);
219 | Message[] messages = folder.search(stringTerm);
220 | FetchProfile fp = new FetchProfile();
221 | fp.add(FetchProfile.Item.ENVELOPE);
222 | folder.fetch(messages, fp);
223 | List msgList = Tools.getMsgHeads(folder, messages);
224 | MailKit.handler.post(() -> consumer1.accept(msgList));
225 | } catch (MessagingException e) {
226 | MailKit.handler.post(() -> consumer2.accept(e));
227 | }
228 | });
229 | }
230 |
231 |
232 | void save(MailKit.Draft draft, Runnable runnable, Consumer consumer) {
233 | MailKit.thread.execute(() -> {
234 | try (IMAPStore store = Tools.getStore(config); IMAPFolder folder = Tools.getFolder(store, folderName, config)) {
235 | MimeMessage message = Tools.toMimeMessage(config, draft);
236 | folder.appendMessages(new MimeMessage[]{message});
237 | MailKit.handler.post(runnable);
238 | } catch (MalformedURLException | MessagingException e) {
239 | MailKit.handler.post(() -> consumer.accept(e));
240 | }
241 | });
242 | }
243 |
244 | }
--------------------------------------------------------------------------------
/mailkit/src/main/java/com/github/artbits/mailkit/MailKit.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.mailkit;
2 |
3 | import android.os.Handler;
4 | import android.os.Looper;
5 |
6 | import java.util.List;
7 | import java.util.concurrent.ExecutorService;
8 | import java.util.concurrent.Executors;
9 | import java.util.function.Consumer;
10 |
11 | public final class MailKit {
12 |
13 | static Handler handler = new Handler(Looper.getMainLooper());
14 | static ExecutorService thread = Executors.newCachedThreadPool();
15 |
16 |
17 | static {
18 | Runtime.getRuntime().addShutdownHook(new Thread(() -> thread.shutdownNow()));
19 | }
20 |
21 |
22 | public static class Config {
23 | public String account;
24 | public String password;
25 | public String nickname;
26 | public String SMTPHost;
27 | public String IMAPHost;
28 | public Integer SMTPPort;
29 | public Integer IMAPPort;
30 | public boolean SMTPSSLEnable;
31 | public boolean IMAPSSLEnable;
32 |
33 | public Config(Consumer consumer) {
34 | consumer.accept(this);
35 | }
36 | }
37 |
38 |
39 | public static class SMTP extends SMTPService {
40 | public SMTP(Config config) {
41 | super(config);
42 | }
43 | }
44 |
45 |
46 | public static class IMAP extends IMAPService {
47 | public IMAP(Config config) {
48 | super(config);
49 | }
50 |
51 | public static class Folder extends MailFolder {
52 | protected Folder(Config config, String folderName) {
53 | super(config, folderName);
54 | }
55 | }
56 |
57 | public static class Inbox extends MailFolder {
58 | protected Inbox(Config config, String folderName) {
59 | super(config, folderName);
60 | }
61 | }
62 |
63 | public static class DraftBox extends MailFolder {
64 | protected DraftBox(Config config, String folderName) {
65 | super(config, folderName);
66 | }
67 |
68 | @Override
69 | public void save(Draft draft, Runnable runnable, Consumer consumer) {
70 | super.save(draft, runnable, consumer);
71 | }
72 | }
73 | }
74 |
75 |
76 | public static class Draft {
77 | public String[] to;
78 | public String[] cc;
79 | public String[] bcc;
80 | public String subject;
81 | public String text;
82 | public String html;
83 |
84 | public Draft(Consumer consumer) {
85 | consumer.accept(this);
86 | }
87 | }
88 |
89 |
90 | public static class Msg {
91 | public long uid;
92 | public long sentDate;
93 | public String subject;
94 | public Flags flags;
95 | public From from;
96 | public List toList;
97 | public List ccList;
98 | public MainBody mainBody;
99 |
100 | protected Msg(Consumer consumer) {
101 | consumer.accept(this);
102 | }
103 |
104 | public static class From {
105 | public String address;
106 | public String nickname;
107 |
108 | From(Consumer consumer) {
109 | consumer.accept(this);
110 | }
111 | }
112 |
113 | public static class To {
114 | public String address;
115 | public String nickname;
116 |
117 | To(Consumer consumer) {
118 | consumer.accept(this);
119 | }
120 | }
121 |
122 | public static class Cc {
123 | public String address;
124 | public String nickname;
125 |
126 | Cc(Consumer consumer) {
127 | consumer.accept(this);
128 | }
129 | }
130 |
131 | public static class Flags {
132 | public boolean isSeen;
133 | public boolean isStar;
134 |
135 | Flags(Consumer consumer) {
136 | consumer.accept(this);
137 | }
138 | }
139 |
140 | public static class MainBody {
141 | public String type;
142 | public String content;
143 |
144 | MainBody(Consumer consumer) {
145 | consumer.accept(this);
146 | }
147 | }
148 | }
149 |
150 |
151 | public static void auth(Config config, Runnable runnable, Consumer consumer) {
152 | new AuthService(config).auth(runnable, consumer);
153 | }
154 |
155 | }
--------------------------------------------------------------------------------
/mailkit/src/main/java/com/github/artbits/mailkit/SMTPService.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.mailkit;
2 |
3 | import java.net.MalformedURLException;
4 | import java.util.function.Consumer;
5 |
6 | import javax.mail.Message;
7 | import javax.mail.MessagingException;
8 | import javax.mail.Transport;
9 | import javax.mail.internet.MimeMessage;
10 |
11 | class SMTPService {
12 |
13 | private final MailKit.Config config;
14 |
15 |
16 | SMTPService(MailKit.Config config) {
17 | this.config = config;
18 | }
19 |
20 |
21 | public void send(MailKit.Draft draft, Runnable runnable, Consumer consumer) {
22 | MailKit.thread.execute(() -> {
23 | try(Transport transport = Tools.getTransport(config)) {
24 | MimeMessage message = Tools.toMimeMessage(config, draft);
25 | transport.sendMessage(message, message.getRecipients(Message.RecipientType.TO));
26 | if (draft.cc != null && draft.cc.length != 0) {
27 | transport.sendMessage(message, message.getRecipients(Message.RecipientType.CC));
28 | }
29 | if (draft.bcc != null && draft.bcc.length != 0) {
30 | transport.sendMessage(message, message.getRecipients(Message.RecipientType.BCC));
31 | }
32 | MailKit.handler.post(runnable);
33 | } catch (MessagingException | MalformedURLException e) {
34 | MailKit.handler.post(() -> consumer.accept(e));
35 | }
36 | });
37 | }
38 |
39 |
40 | private void reply(MailKit.Draft draft, String folderName, long originUID, Runnable runnable, Consumer consumer) {
41 | MailKit.IMAP imap = new MailKit.IMAP(config);
42 | MailKit.IMAP.Folder folder = imap.getFolder(folderName);
43 | folder.getMsg(originUID, msg -> {
44 | draft.text = draft.text + msg.mainBody.content;
45 | send(draft, runnable, consumer);
46 | }, consumer);
47 | }
48 |
49 |
50 | private void forward(MailKit.Draft draft, String folderName, long originUID, Runnable runnable, Consumer consumer) {
51 | MailKit.IMAP imap = new MailKit.IMAP(config);
52 | MailKit.IMAP.Folder folder = imap.getFolder(folderName);
53 | folder.getMsg(originUID, msg -> {
54 | draft.text = draft.text + msg.mainBody.content;
55 | send(draft, runnable, consumer);
56 | }, consumer);
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/mailkit/src/main/java/com/github/artbits/mailkit/Tools.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.mailkit;
2 |
3 | import com.sun.mail.imap.IMAPFolder;
4 | import com.sun.mail.imap.IMAPMessage;
5 | import com.sun.mail.imap.IMAPStore;
6 |
7 | import java.io.IOException;
8 | import java.net.MalformedURLException;
9 | import java.util.ArrayList;
10 | import java.util.Date;
11 | import java.util.HashMap;
12 | import java.util.List;
13 | import java.util.Properties;
14 |
15 | import javax.mail.Address;
16 | import javax.mail.Flags;
17 | import javax.mail.Folder;
18 | import javax.mail.Message;
19 | import javax.mail.MessagingException;
20 | import javax.mail.Multipart;
21 | import javax.mail.Part;
22 | import javax.mail.Session;
23 | import javax.mail.Transport;
24 | import javax.mail.internet.AddressException;
25 | import javax.mail.internet.InternetAddress;
26 | import javax.mail.internet.MimeMessage;
27 |
28 | final class Tools {
29 |
30 | static Session toSession(MailKit.Config config) {
31 | Properties properties = new Properties();
32 | if (config.SMTPHost != null && config.SMTPPort != null) {
33 | properties.put("mail.smtp.auth", true);
34 | properties.put("mail.smtp.host", config.SMTPHost);
35 | properties.put("mail.smtp.port", config.SMTPPort);
36 | if (config.account.contains("@outlook.com") || config.account.contains("@office365.com")) {
37 | properties.put("mail.smtp.starttls.enable", config.SMTPSSLEnable);
38 | properties.put("mail.smtp.starttls.required", true);
39 | } else {
40 | properties.put("mail.smtp.ssl.enable", config.SMTPSSLEnable);
41 | }
42 | }
43 | if (config.IMAPHost != null && config.IMAPPort != null) {
44 | properties.put("mail.imap.auth", true);
45 | properties.put("mail.imap.host", config.IMAPHost);
46 | properties.put("mail.imap.port", config.IMAPPort);
47 | properties.put("mail.imap.ssl.enable", config.IMAPSSLEnable);
48 | properties.setProperty("mail.imap.partialfetch", "false");
49 | properties.setProperty("mail.imaps.partialfetch", "false");
50 | }
51 | return Session.getInstance(properties);
52 | }
53 |
54 |
55 | static Address[] toAddresses(String[] addresses) throws AddressException {
56 | Address[] internetAddresses = new InternetAddress[addresses.length];
57 | for (int i = 0, length = addresses.length; i < length; i++) {
58 | internetAddresses[i] = new InternetAddress(addresses[i]);
59 | }
60 | return internetAddresses;
61 | }
62 |
63 |
64 | static MimeMessage toMimeMessage(MailKit.Config config, MailKit.Draft draft)
65 | throws MessagingException, MalformedURLException {
66 | Session session = toSession(config);
67 | MimeMessage message = new MimeMessage(session);
68 | message.addRecipients(MimeMessage.RecipientType.TO, toAddresses(draft.to));
69 | if (draft.cc != null) {
70 | message.addRecipients(MimeMessage.RecipientType.CC, toAddresses(draft.cc));
71 | }
72 | if (draft.bcc != null) {
73 | message.addRecipients(MimeMessage.RecipientType.BCC, toAddresses(draft.bcc));
74 | }
75 | message.setFrom(new InternetAddress(config.nickname + "<" + config.account + ">"));
76 | message.setSubject(draft.subject, "UTF-8");
77 | message.setSentDate(new Date());
78 | if (draft.html != null) {
79 | message.setContent(draft.html, "text/html; charset=UTF-8");
80 | }
81 | if (draft.html == null && draft.text != null) {
82 | message.setText(draft.text, "UTF-8");
83 | }
84 | message.setFlag(Flags.Flag.RECENT, true);
85 | message.saveChanges();
86 | return message;
87 | }
88 |
89 |
90 | static MailKit.Msg toMsg(long uid, IMAPMessage imapMessage) throws MessagingException, IOException {
91 | long sentTime = imapMessage.getSentDate().getTime();
92 | String subject = imapMessage.getSubject();
93 | MailKit.Msg.Flags flags = getFlags(imapMessage.getFlags());
94 | MailKit.Msg.From from = getFrom(imapMessage.getFrom());
95 | List toList = getToList(imapMessage.getRecipients(MimeMessage.RecipientType.TO));
96 | List ccList = getCcList(imapMessage.getRecipients(MimeMessage.RecipientType.CC));
97 | MailKit.Msg.MainBody mainBody = getMainBody(imapMessage);
98 | return new MailKit.Msg(m -> {
99 | m.uid = uid;
100 | m.subject = subject;
101 | m.sentDate = sentTime;
102 | m.flags = flags;
103 | m.from = from;
104 | m.toList = toList;
105 | m.ccList = ccList;
106 | m.mainBody = mainBody;
107 | });
108 | }
109 |
110 |
111 | static MailKit.Msg getMsgHead(long uid, IMAPMessage imapMessage) {
112 | try {
113 | long sentDate = imapMessage.getSentDate().getTime();
114 | String subject = imapMessage.getSubject();
115 | MailKit.Msg.Flags flags = getFlags(imapMessage.getFlags());
116 | MailKit.Msg.From from = Tools.getFrom(imapMessage.getFrom());
117 | List toList = Tools.getToList(imapMessage.getRecipients(MimeMessage.RecipientType.TO));
118 | List ccList = Tools.getCcList(imapMessage.getRecipients(MimeMessage.RecipientType.CC));
119 | return new MailKit.Msg(msg -> {
120 | msg.uid = uid;
121 | msg.sentDate = sentDate;
122 | msg.subject = subject;
123 | msg.flags = flags;
124 | msg.from = from;
125 | msg.toList = toList;
126 | msg.ccList = ccList;
127 | });
128 | } catch (Exception e) {
129 | return null;
130 | }
131 | }
132 |
133 |
134 | static List getMsgHeads(IMAPFolder folder, Message[] messages) {
135 | List msgHeads = new ArrayList<>();
136 | for (Message message : messages) {
137 | try {
138 | IMAPMessage imapMessage = (IMAPMessage) message;
139 | long uid = folder.getUID(imapMessage);
140 | long sentDate = imapMessage.getSentDate().getTime();
141 | String subject = imapMessage.getSubject();
142 | MailKit.Msg.Flags flags = getFlags(imapMessage.getFlags());
143 | MailKit.Msg.From from = Tools.getFrom(imapMessage.getFrom());
144 | List toList = Tools.getToList(imapMessage.getRecipients(MimeMessage.RecipientType.TO));
145 | List ccList = Tools.getCcList(imapMessage.getRecipients(MimeMessage.RecipientType.CC));
146 | msgHeads.add(new MailKit.Msg(msg -> {
147 | msg.uid = uid;
148 | msg.sentDate = sentDate;
149 | msg.subject = subject;
150 | msg.flags = flags;
151 | msg.from = from;
152 | msg.toList = toList;
153 | msg.ccList = ccList;
154 | }));
155 | } catch (Exception ignored) { }
156 | }
157 | return msgHeads;
158 | }
159 |
160 |
161 | static MailKit.Msg.From getFrom(Address[] addresses) {
162 | if (addresses != null && addresses.length != 0) {
163 | InternetAddress address = (InternetAddress) addresses[0];
164 | return new MailKit.Msg.From(f -> {
165 | f.address = address.getAddress();
166 | f.nickname = address.getPersonal();
167 | });
168 | }
169 | return null;
170 | }
171 |
172 |
173 | static List getToList(Address[] addresses) {
174 | if (addresses != null && addresses.length != 0) {
175 | List toList = new ArrayList<>();
176 | for (Address address : addresses) {
177 | InternetAddress internetAddress = (InternetAddress) address;
178 | toList.add(new MailKit.Msg.To(t -> {
179 | t.address = internetAddress.getAddress();
180 | t.nickname = internetAddress.getPersonal();
181 | }));
182 | }
183 | return toList;
184 | }
185 | return null;
186 | }
187 |
188 |
189 | static List getCcList(Address[] addresses) {
190 | if (addresses != null && addresses.length != 0) {
191 | List ccList = new ArrayList<>();
192 | for (Address address : addresses) {
193 | InternetAddress internetAddress = (InternetAddress) address;
194 | ccList.add(new MailKit.Msg.Cc(c -> {
195 | c.address = internetAddress.getAddress();
196 | c.nickname = internetAddress.getPersonal();
197 | }));
198 | }
199 | return ccList;
200 | }
201 | return null;
202 | }
203 |
204 |
205 | static MailKit.Msg.Flags getFlags(Flags internetFlags) {
206 | return new MailKit.Msg.Flags(f -> {
207 | f.isSeen = internetFlags.contains(Flags.Flag.SEEN);
208 | f.isStar = internetFlags.contains(Flags.Flag.FLAGGED);
209 | });
210 | }
211 |
212 |
213 | static HashMap getMainBodyMap(Part part, HashMap map)
214 | throws MessagingException, IOException {
215 | StringBuilder text = new StringBuilder();
216 | StringBuilder html = new StringBuilder();
217 | if (part.isMimeType("text/plain")){
218 | map.put("text/plain", text.append(part.getContent()));
219 | } else if (part.isMimeType("text/html")) {
220 | map.put("text/html", html.append(part.getContent()));
221 | } else if (part.isMimeType("multipart/*")) {
222 | Multipart multipart = (Multipart) part.getContent();
223 | for (int i = 0, count = multipart.getCount(); i < count; i++) {
224 | getMainBodyMap(multipart.getBodyPart(i), map);
225 | }
226 | }
227 | return map;
228 | }
229 |
230 |
231 | static MailKit.Msg.MainBody getMainBody(IMAPMessage imapMessage) throws IOException, MessagingException {
232 | HashMap map = getMainBodyMap(imapMessage, new HashMap<>());
233 | imapMessage.setFlag(Flags.Flag.SEEN, true);
234 | if (map.get("text/html") != null) {
235 | return new MailKit.Msg.MainBody(m -> {
236 | m.type = "text/html";
237 | m.content = String.valueOf(map.get("text/html"));
238 | });
239 | } else if (map.get("text/plain") != null) {
240 | return new MailKit.Msg.MainBody(m -> {
241 | m.type = "text/plain";
242 | m.content = String.valueOf(map.get("text/plain"));
243 | });
244 | } else {
245 | return null;
246 | }
247 | }
248 |
249 |
250 | static Transport getTransport(MailKit.Config config) throws MessagingException {
251 | Session session = toSession(config);
252 | Transport transport = session.getTransport("smtp");
253 | transport.connect(config.SMTPHost, config.account, config.password);
254 | return transport;
255 | }
256 |
257 |
258 | static IMAPStore getStore(MailKit.Config config) throws MessagingException {
259 | Session session = toSession(config);
260 | IMAPStore store = (IMAPStore) session.getStore("imap");
261 | store.connect(config.IMAPHost, config.account, config.password);
262 | return store;
263 | }
264 |
265 |
266 | static IMAPFolder getFolder(IMAPStore store, String folderName, MailKit.Config config) throws MessagingException {
267 | IMAPFolder folder = (IMAPFolder) store.getFolder(folderName);
268 | boolean b1 = config.account.contains("@163.com");
269 | boolean b2 = config.account.contains("@126.com");
270 | boolean b3 = config.account.contains("@yeah.net");
271 | if (b1 || b2 || b3) {
272 | folder.doCommand(protocol -> {
273 | protocol.id("FUTONG");
274 | return null;
275 | });
276 | }
277 | folder.open(Folder.READ_WRITE);
278 | return folder;
279 | }
280 |
281 | }
--------------------------------------------------------------------------------
/mailkit/src/main/java/com/github/artbits/mailkit/UIDHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.mailkit;
2 |
3 | import com.sun.mail.imap.IMAPFolder;
4 |
5 | import java.util.Arrays;
6 |
7 | import javax.mail.Message;
8 | import javax.mail.MessagingException;
9 |
10 | class UIDHandler {
11 |
12 | //加载下一组uid的算法
13 | static long[] nextUIDArray(IMAPFolder folder, long lastUID) throws MessagingException {
14 | Message[] msgList = folder.getMessages();
15 | long[] uidArray = new long[0];
16 | if (msgList.length == 0){
17 | return uidArray;
18 | } else if (lastUID < 0) {
19 | for (int i = msgList.length-1, count = 1; i >= 0 && count <= 20; --i, ++count)
20 | uidArray = Basis.insertUID(uidArray, folder.getUID(msgList[i]));
21 | return uidArray;
22 | } else {
23 | int index = Basis.searchIndex(folder, msgList, lastUID);
24 | for (int i = index, count = 1; i >= 0 && count <= 20; --i, ++count)
25 | uidArray = Basis.insertUID(uidArray, folder.getUID(msgList[i]));
26 | return uidArray;
27 | }
28 | }
29 |
30 |
31 | //对本地已存在的uid进行同步的算法
32 | static Result syncUIDArray(IMAPFolder folder, long[] localUIDArray) throws MessagingException {
33 | //获取message数组
34 | Message[] msgList = folder.getMessages();
35 |
36 | //获取本地消息和服务器消息的数组长度
37 | int localLength = localUIDArray.length;
38 | int netLength = msgList.length;
39 |
40 | //初始化数组
41 | long[] newArray = new long[0];
42 | long[] delArray = new long[0];
43 |
44 | //如果本地一封邮件都没有,则框架什么也做,不会拉取新数据
45 | if (localLength == 0) {
46 | return new Result(newArray, delArray);
47 | }
48 |
49 | //服务器没有一封邮件(服务器上的邮件已经被全部删除),则把本地已存储的邮件已删除
50 | if (netLength == 0) {
51 | return new Result(newArray, localUIDArray);
52 | }
53 |
54 | //排序本地的uid,由小到大
55 | Arrays.sort(localUIDArray);
56 |
57 | //服务端的消息已全部同步到本地
58 | if (localLength != netLength || localUIDArray[localLength - 1] != folder.getUID(msgList[netLength - 1])) {
59 | //判断邮件服务器是否有新邮件
60 | long uid;
61 | long localMaxUID = localUIDArray[localLength - 1];
62 | for (int i = netLength - 1; (uid = folder.getUID(msgList[i])) > localMaxUID; --i) {
63 | newArray = Basis.insertUID(newArray, uid);
64 | }
65 | //判断邮件服务器是否有已删除的邮件
66 | for (long localUID : localUIDArray) {
67 | if (Basis.binarySearch(folder, msgList, localUID) < 0) {
68 | delArray = Basis.insertUID(delArray, localUID);
69 | }
70 | }
71 | }
72 | return new Result(newArray, delArray);
73 | }
74 |
75 |
76 | private static class Basis {
77 |
78 | static long[] insertUID(long[] srcArray, long value) {
79 | int srcLength = srcArray.length;
80 | long[] destArrays = new long[srcLength+1];
81 | System.arraycopy(srcArray, 0, destArrays, 0, srcLength);
82 | destArrays[srcLength] = value;
83 | return destArrays;
84 | }
85 |
86 | static long[] deleteUID(long[] srcArray, long value) {
87 | int delPost = Arrays.binarySearch(srcArray, value);
88 | int srcLength = srcArray.length;
89 | long[] destArray = new long[srcLength-1];
90 | if (delPost > 0) {
91 | System.arraycopy(srcArray, 0, destArray, 0, delPost);
92 | System.arraycopy(srcArray, delPost+1, destArray, delPost, srcLength-delPost-1);
93 | return destArray;
94 | } else if (delPost == 0){
95 | System.arraycopy(srcArray, 1, destArray, 0, srcLength-1);
96 | return destArray;
97 | } else {
98 | return srcArray;
99 | }
100 | }
101 |
102 | static int binarySearch(IMAPFolder folder, Message[] msgList, long uid) throws MessagingException {
103 | for (int min = 0, max = msgList.length - 1, mid; min <= max; ) {
104 | mid = (min + max) / 2;
105 | if (folder.getUID(msgList[mid]) > uid) {
106 | max = mid - 1;
107 | } else if (folder.getUID(msgList[mid]) < uid) {
108 | min = mid + 1;
109 | } else {
110 | return mid;
111 | }
112 | }
113 | return -1;
114 | }
115 |
116 | /**
117 | * 算法功能:在元素值递增的数组中查找刚比目标uid小的uid的下标
118 | * uid表 = [1, 2, 3, 4, 5, 6, 8, 9, 10]
119 | * 下标值: [0, 1, 2, 3, 4, 5, 6, 7, 8 ]
120 | *
121 | * 假设目标uid = 11,刚比目标uid小的uid = 10,该uid的index = 8
122 | * 假设目标uid = 10,刚比目标uid小的uid = 9,该uid的index = 7
123 | * 假设目标uid = 7,刚比目标uid小的uid = 6,该uid的index = 5
124 | * 假设目标uid = 0,刚比目标uid小的uid不存在,返回值 = -1
125 | */
126 | static int searchIndex(IMAPFolder folder, Message[] msgList, long uid) throws MessagingException {
127 | for (int low = 0, high = msgList.length - 1, last = high, mid; low <= high; ) {
128 | mid = (low + high) / 2;
129 | if (folder.getUID(msgList[mid]) > uid) {
130 | high = mid - 1;
131 | if (high >= 0 && folder.getUID(msgList[high]) < uid)
132 | return high;
133 | } else if (folder.getUID(msgList[mid]) < uid) {
134 | low = mid + 1;
135 | if (low == last && folder.getUID(msgList[low]) < uid)
136 | return low;
137 | } else {
138 | return mid - 1;
139 | }
140 | }
141 | return -1;
142 | }
143 |
144 | }
145 |
146 |
147 | static class Result {
148 |
149 | long[] newArray;
150 | long[] delArray;
151 |
152 | Result(long[] newArray, long[] delArray) {
153 | this.newArray = newArray;
154 | this.delArray = delArray;
155 | }
156 |
157 | }
158 |
159 | }
160 |
--------------------------------------------------------------------------------
/mailkit/src/test/java/com/github/artbits/mailkit/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.github.artbits.mailkit;
2 |
3 | /**
4 | * Example local unit test, which will execute on the development machine (host).
5 | *
6 | * @see Testing documentation
7 | */
8 | public class ExampleUnitTest {
9 |
10 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | maven { url 'https://maven.aliyun.com/repository/google' }
4 | maven { url 'https://maven.aliyun.com/repository/public' }
5 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
6 | maven { url 'https://www.jitpack.io' }
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | maven { url 'https://maven.aliyun.com/repository/google' }
13 | maven { url 'https://maven.aliyun.com/repository/public' }
14 | maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
15 | maven { url 'https://www.jitpack.io' }
16 | }
17 | }
18 | rootProject.name = "android-mail"
19 | include ':app'
20 | include ':mailkit'
21 |
--------------------------------------------------------------------------------