Simple email application for Android. Original source code: https://framagit.org/dystopia-project/simple-email

503 lines
19 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. package eu.faircode.email;
  2. /*
  3. This file is part of FairEmail.
  4. FairEmail is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. NetGuard is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
  14. Copyright 2018 by Marcel Bokhorst (M66B)
  15. */
  16. import android.content.Context;
  17. import android.text.TextUtils;
  18. import android.util.Base64;
  19. import android.util.Log;
  20. import android.webkit.MimeTypeMap;
  21. import org.jsoup.Jsoup;
  22. import java.io.ByteArrayInputStream;
  23. import java.io.ByteArrayOutputStream;
  24. import java.io.File;
  25. import java.io.IOException;
  26. import java.io.InputStream;
  27. import java.io.UnsupportedEncodingException;
  28. import java.nio.charset.Charset;
  29. import java.util.ArrayList;
  30. import java.util.Date;
  31. import java.util.List;
  32. import java.util.Properties;
  33. import javax.activation.DataHandler;
  34. import javax.activation.FileDataSource;
  35. import javax.activation.FileTypeMap;
  36. import javax.mail.Address;
  37. import javax.mail.BodyPart;
  38. import javax.mail.Flags;
  39. import javax.mail.Message;
  40. import javax.mail.MessagingException;
  41. import javax.mail.Multipart;
  42. import javax.mail.Part;
  43. import javax.mail.Session;
  44. import javax.mail.internet.ContentType;
  45. import javax.mail.internet.InternetAddress;
  46. import javax.mail.internet.MimeBodyPart;
  47. import javax.mail.internet.MimeMessage;
  48. import javax.mail.internet.MimeMultipart;
  49. import javax.mail.internet.ParseException;
  50. public class MessageHelper {
  51. private MimeMessage imessage;
  52. private String raw = null;
  53. final static int NETWORK_TIMEOUT = 60 * 1000; // milliseconds
  54. static Properties getSessionProperties(int auth_type) {
  55. Properties props = new Properties();
  56. // https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#properties
  57. props.put("mail.imaps.ssl.checkserveridentity", "true");
  58. props.put("mail.imaps.ssl.trust", "*");
  59. props.put("mail.imaps.starttls.enable", "false");
  60. // TODO: make timeouts configurable?
  61. props.put("mail.imaps.connectiontimeout", Integer.toString(NETWORK_TIMEOUT));
  62. props.put("mail.imaps.timeout", Integer.toString(NETWORK_TIMEOUT));
  63. props.put("mail.imaps.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
  64. props.put("mail.imaps.connectionpool.debug", "true");
  65. props.put("mail.imaps.connectionpooltimeout", Integer.toString(3 * 60 * 1000)); // default: 45 sec
  66. // "mail.imaps.finalizecleanclose"
  67. // https://tools.ietf.org/html/rfc4978
  68. // https://docs.oracle.com/javase/8/docs/api/java/util/zip/Deflater.html
  69. if (true) {
  70. Log.i(Helper.TAG, "IMAP compress enabled");
  71. props.put("mail.imaps.compress.enable", "true");
  72. //props.put("mail.imaps.compress.level", "-1");
  73. //props.put("mail.imaps.compress.strategy", "0");
  74. }
  75. props.put("mail.imaps.fetchsize", Integer.toString(48 * 1024)); // default 16K
  76. props.put("mail.imaps.peek", "true");
  77. // https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html#properties
  78. props.put("mail.smtps.ssl.checkserveridentity", "true");
  79. props.put("mail.smtps.ssl.trust", "*");
  80. props.put("mail.smtps.starttls.enable", "false");
  81. props.put("mail.smtps.starttls.required", "false");
  82. props.put("mail.smtps.auth", "true");
  83. props.put("mail.smtps.connectiontimeout", Integer.toString(NETWORK_TIMEOUT));
  84. props.put("mail.smtps.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
  85. props.put("mail.smtps.timeout", Integer.toString(NETWORK_TIMEOUT));
  86. props.put("mail.smtp.ssl.checkserveridentity", "true");
  87. props.put("mail.smtp.ssl.trust", "*");
  88. props.put("mail.smtp.starttls.enable", "true");
  89. props.put("mail.smtp.starttls.required", "true");
  90. props.put("mail.smtp.auth", "true");
  91. props.put("mail.smtp.connectiontimeout", Integer.toString(NETWORK_TIMEOUT));
  92. props.put("mail.smtp.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
  93. props.put("mail.smtp.timeout", Integer.toString(NETWORK_TIMEOUT));
  94. props.put("mail.mime.address.strict", "false");
  95. props.put("mail.mime.decodetext.strict", "false");
  96. props.put("mail.mime.ignoreunknownencoding", "true"); // Content-Transfer-Encoding
  97. props.put("mail.mime.decodefilename", "true");
  98. props.put("mail.mime.encodefilename", "true");
  99. // https://docs.oracle.com/javaee/6/api/javax/mail/internet/MimeMultipart.html
  100. props.put("mail.mime.multipart.ignoremissingboundaryparameter", "true"); // javax.mail.internet.ParseException: In parameter list
  101. props.put("mail.mime.multipart.ignoreexistingboundaryparameter", "true");
  102. // The documentation is unclear/inconsistent whether this are system or session properties:
  103. System.setProperty("mail.mime.address.strict", "false");
  104. System.setProperty("mail.mime.decodetext.strict", "false");
  105. System.setProperty("mail.mime.ignoreunknownencoding", "true"); // Content-Transfer-Encoding
  106. System.setProperty("mail.mime.decodefilename", "true");
  107. System.setProperty("mail.mime.encodefilename", "true");
  108. System.setProperty("mail.mime.multipart.ignoremissingboundaryparameter", "true"); // javax.mail.internet.ParseException: In parameter list
  109. System.setProperty("mail.mime.multipart.ignoreexistingboundaryparameter", "true");
  110. if (false) {
  111. Log.i(Helper.TAG, "Prefering IPv4");
  112. System.setProperty("java.net.preferIPv4Stack", "true");
  113. }
  114. // https://javaee.github.io/javamail/OAuth2
  115. Log.i(Helper.TAG, "Auth type=" + auth_type);
  116. if (auth_type == Helper.AUTH_TYPE_GMAIL) {
  117. props.put("mail.imaps.auth.mechanisms", "XOAUTH2");
  118. props.put("mail.smtps.auth.mechanisms", "XOAUTH2");
  119. props.put("mail.smtp.auth.mechanisms", "XOAUTH2");
  120. }
  121. return props;
  122. }
  123. static MimeMessageEx from(Context context, EntityMessage message, EntityMessage reply, List<EntityAttachment> attachments, Session isession) throws MessagingException, IOException {
  124. MimeMessageEx imessage = new MimeMessageEx(isession, message.msgid);
  125. if (reply == null)
  126. imessage.addHeader("References", message.msgid);
  127. else {
  128. imessage.addHeader("In-Reply-To", reply.msgid);
  129. imessage.addHeader("References", (reply.references == null ? "" : reply.references + " ") + reply.msgid);
  130. }
  131. imessage.setFlag(Flags.Flag.SEEN, message.seen);
  132. if (message.from != null && message.from.length > 0)
  133. imessage.setFrom(message.from[0]);
  134. if (message.to != null && message.to.length > 0)
  135. imessage.setRecipients(Message.RecipientType.TO, message.to);
  136. if (message.cc != null && message.cc.length > 0)
  137. imessage.setRecipients(Message.RecipientType.CC, message.cc);
  138. if (message.bcc != null && message.bcc.length > 0)
  139. imessage.setRecipients(Message.RecipientType.BCC, message.bcc);
  140. if (message.subject != null)
  141. imessage.setSubject(message.subject);
  142. // TODO: plain message?
  143. String body = message.read(context);
  144. BodyPart plain = new MimeBodyPart();
  145. plain.setContent(Jsoup.parse(body).text(), "text/plain; charset=" + Charset.defaultCharset().name());
  146. BodyPart html = new MimeBodyPart();
  147. html.setContent(body, "text/html; charset=" + Charset.defaultCharset().name());
  148. Multipart alternative = new MimeMultipart("alternative");
  149. alternative.addBodyPart(plain);
  150. alternative.addBodyPart(html);
  151. if (attachments.size() == 0) {
  152. imessage.setContent(alternative);
  153. } else {
  154. Multipart multipart = new MimeMultipart("mixed");
  155. BodyPart bp = new MimeBodyPart();
  156. bp.setContent(alternative);
  157. multipart.addBodyPart(bp);
  158. for (final EntityAttachment attachment : attachments)
  159. if (attachment.available) {
  160. BodyPart bpAttachment = new MimeBodyPart();
  161. bpAttachment.setFileName(attachment.name);
  162. File file = EntityAttachment.getFile(context, attachment.id);
  163. FileDataSource dataSource = new FileDataSource(file);
  164. dataSource.setFileTypeMap(new FileTypeMap() {
  165. @Override
  166. public String getContentType(File file) {
  167. return attachment.type;
  168. }
  169. @Override
  170. public String getContentType(String filename) {
  171. return attachment.type;
  172. }
  173. });
  174. bpAttachment.setDataHandler(new DataHandler(dataSource));
  175. if (attachment.cid != null)
  176. bpAttachment.setHeader("Content-ID", attachment.cid);
  177. multipart.addBodyPart(bpAttachment);
  178. }
  179. imessage.setContent(multipart);
  180. }
  181. imessage.setSentDate(new Date());
  182. return imessage;
  183. }
  184. MessageHelper(MimeMessage message) {
  185. this.imessage = message;
  186. }
  187. MessageHelper(String raw, Session isession) throws MessagingException {
  188. byte[] bytes = Base64.decode(raw, Base64.URL_SAFE);
  189. InputStream is = new ByteArrayInputStream(bytes);
  190. this.imessage = new MimeMessage(isession, is);
  191. }
  192. boolean getSeen() throws MessagingException {
  193. return imessage.isSet(Flags.Flag.SEEN);
  194. }
  195. boolean getFlagged() throws MessagingException {
  196. return imessage.isSet(Flags.Flag.FLAGGED);
  197. }
  198. String getMessageID() throws MessagingException {
  199. return imessage.getHeader("Message-ID", null);
  200. }
  201. String[] getReferences() throws MessagingException {
  202. String refs = imessage.getHeader("References", null);
  203. return (refs == null ? new String[0] : refs.split("\\s+"));
  204. }
  205. String getDeliveredTo() throws MessagingException {
  206. return imessage.getHeader("Delivered-To", imessage.getHeader("X-Delivered-To", null));
  207. }
  208. String getInReplyTo() throws MessagingException {
  209. return imessage.getHeader("In-Reply-To", null);
  210. }
  211. String getThreadId(long uid) throws MessagingException {
  212. for (String ref : getReferences())
  213. if (!TextUtils.isEmpty(ref))
  214. return ref;
  215. String msgid = getMessageID();
  216. return (TextUtils.isEmpty(msgid) ? Long.toString(uid) : msgid);
  217. }
  218. Address[] getFrom() throws MessagingException {
  219. return imessage.getFrom();
  220. }
  221. Address[] getTo() throws MessagingException {
  222. return imessage.getRecipients(Message.RecipientType.TO);
  223. }
  224. Address[] getCc() throws MessagingException {
  225. return imessage.getRecipients(Message.RecipientType.CC);
  226. }
  227. Address[] getBcc() throws MessagingException {
  228. return imessage.getRecipients(Message.RecipientType.BCC);
  229. }
  230. Address[] getReply() throws MessagingException {
  231. String[] headers = imessage.getHeader("Reply-To");
  232. if (headers != null && headers.length > 0)
  233. return imessage.getReplyTo();
  234. else
  235. return null;
  236. }
  237. Integer getSize() throws MessagingException {
  238. int size = imessage.getSize();
  239. return (size < 0 ? null : size);
  240. }
  241. static String getFormattedAddresses(Address[] addresses, boolean full) {
  242. if (addresses == null || addresses.length == 0)
  243. return "";
  244. List<String> formatted = new ArrayList<>();
  245. for (Address address : addresses)
  246. if (address instanceof InternetAddress) {
  247. InternetAddress a = (InternetAddress) address;
  248. String personal = a.getPersonal();
  249. if (TextUtils.isEmpty(personal))
  250. formatted.add(address.toString());
  251. else {
  252. personal = personal.replaceAll("[\\,\\<\\>]", "");
  253. if (full)
  254. formatted.add(personal + " <" + a.getAddress() + ">");
  255. else
  256. formatted.add(personal);
  257. }
  258. } else
  259. formatted.add(address.toString());
  260. return TextUtils.join(", ", formatted);
  261. }
  262. String getHtml() throws MessagingException, IOException {
  263. return getHtml(imessage);
  264. }
  265. private static String getHtml(Part part) throws MessagingException, IOException {
  266. if (part.isMimeType("text/*")) {
  267. String s;
  268. try {
  269. s = part.getContent().toString();
  270. } catch (UnsupportedEncodingException ex) {
  271. // x-binaryenc
  272. Log.w(Helper.TAG, "Unsupported encoding: " + part.getContentType());
  273. // https://javaee.github.io/javamail/FAQ#unsupen
  274. InputStream is = part.getInputStream();
  275. ByteArrayOutputStream os = new ByteArrayOutputStream();
  276. byte[] buffer = new byte[4096];
  277. for (int len = is.read(buffer); len != -1; len = is.read(buffer))
  278. os.write(buffer, 0, len);
  279. s = new String(os.toByteArray(), "US-ASCII");
  280. } catch (IOException ex) {
  281. // IOException; Unknown encoding: none
  282. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  283. s = ex.toString();
  284. }
  285. if (part.isMimeType("text/plain"))
  286. s = "<pre>" + s.replaceAll("\\r?\\n", "<br />") + "</pre>";
  287. return s;
  288. }
  289. if (part.isMimeType("multipart/alternative")) {
  290. String text = null;
  291. try {
  292. Multipart mp = (Multipart) part.getContent();
  293. for (int i = 0; i < mp.getCount(); i++) {
  294. Part bp = mp.getBodyPart(i);
  295. if (bp.isMimeType("text/plain")) {
  296. if (text == null)
  297. text = getHtml(bp);
  298. } else if (bp.isMimeType("text/html")) {
  299. String s = getHtml(bp);
  300. if (s != null)
  301. return s;
  302. } else
  303. return getHtml(bp);
  304. }
  305. } catch (ParseException ex) {
  306. // ParseException: In parameter list boundary="...">, expected parameter name, got ";"
  307. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  308. text = ex.toString();
  309. }
  310. return text;
  311. }
  312. if (part.isMimeType("multipart/*"))
  313. try {
  314. Multipart mp = (Multipart) part.getContent();
  315. for (int i = 0; i < mp.getCount(); i++) {
  316. String s = getHtml(mp.getBodyPart(i));
  317. if (s != null)
  318. return s;
  319. }
  320. } catch (ParseException ex) {
  321. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  322. return ex.toString();
  323. }
  324. return null;
  325. }
  326. public List<EntityAttachment> getAttachments() throws IOException, MessagingException {
  327. List<EntityAttachment> result = new ArrayList<>();
  328. try {
  329. Object content = imessage.getContent();
  330. if (content instanceof String)
  331. return result;
  332. if (content instanceof Multipart) {
  333. Multipart multipart = (Multipart) content;
  334. for (int i = 0; i < multipart.getCount(); i++)
  335. result.addAll(getAttachments(multipart.getBodyPart(i)));
  336. }
  337. } catch (ParseException ex) {
  338. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  339. }
  340. return result;
  341. }
  342. private static List<EntityAttachment> getAttachments(BodyPart part) throws
  343. IOException, MessagingException {
  344. List<EntityAttachment> result = new ArrayList<>();
  345. Object content;
  346. try {
  347. content = part.getContent();
  348. } catch (UnsupportedEncodingException ex) {
  349. Log.w(Helper.TAG, "attachment content type=" + part.getContentType());
  350. content = part.getInputStream();
  351. } catch (ParseException ex) {
  352. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  353. content = null;
  354. }
  355. if (content instanceof InputStream || content instanceof String) {
  356. String disposition;
  357. try {
  358. disposition = part.getDisposition();
  359. } catch (MessagingException ex) {
  360. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  361. disposition = null;
  362. }
  363. String filename;
  364. try {
  365. filename = part.getFileName();
  366. } catch (MessagingException ex) {
  367. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  368. filename = null;
  369. }
  370. if (Part.ATTACHMENT.equalsIgnoreCase(disposition) ||
  371. part.isMimeType("image/*") ||
  372. !TextUtils.isEmpty(filename)) {
  373. ContentType ct = new ContentType(part.getContentType());
  374. String[] cid = part.getHeader("Content-ID");
  375. EntityAttachment attachment = new EntityAttachment();
  376. attachment.name = filename;
  377. attachment.type = ct.getBaseType().toLowerCase();
  378. attachment.size = part.getSize();
  379. attachment.cid = (cid == null || cid.length == 0 ? null : cid[0]);
  380. attachment.part = part;
  381. // Try to guess a better content type
  382. // Sometimes PDF files are sent using the wrong type
  383. if ("application/octet-stream".equals(attachment.type)) {
  384. String extension = Helper.getExtension(attachment.name);
  385. if (extension != null) {
  386. String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
  387. if (type != null) {
  388. Log.w(Helper.TAG, "Guessing file=" + attachment.name + " type=" + type);
  389. attachment.type = type;
  390. }
  391. }
  392. }
  393. if (attachment.size < 0)
  394. attachment.size = null;
  395. result.add(attachment);
  396. }
  397. } else if (content instanceof Multipart) {
  398. Multipart multipart = (Multipart) content;
  399. for (int i = 0; i < multipart.getCount(); i++)
  400. result.addAll(getAttachments(multipart.getBodyPart(i)));
  401. }
  402. return result;
  403. }
  404. String getRaw() throws IOException, MessagingException {
  405. if (raw == null) {
  406. ByteArrayOutputStream os = new ByteArrayOutputStream();
  407. imessage.writeTo(os);
  408. raw = Base64.encodeToString(os.toByteArray(), Base64.URL_SAFE);
  409. }
  410. return raw;
  411. }
  412. }