├── .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 | |![](image/config.png) | ![](image/main.png) | ![](image/menu.png) | 17 | | 写邮件 | 收件箱列表 | 查看邮件内容 | 18 | |![](image/write.png) | ![](image/inbox.png) | ![](image/details.png)| 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 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 15 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_write.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------