|
|
- package eu.faircode.email;
-
- /*
- This file is part of FairEmail.
-
- FairEmail is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- NetGuard is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
-
- Copyright 2018 by Marcel Bokhorst (M66B)
- */
-
- import android.content.Context;
- import android.text.TextUtils;
- import android.util.Base64;
- import android.util.Log;
- import android.webkit.MimeTypeMap;
-
- import org.jsoup.Jsoup;
-
- import java.io.ByteArrayInputStream;
- import java.io.ByteArrayOutputStream;
- import java.io.File;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.UnsupportedEncodingException;
- import java.nio.charset.Charset;
- import java.util.ArrayList;
- import java.util.Date;
- import java.util.List;
- import java.util.Properties;
-
- import javax.activation.DataHandler;
- import javax.activation.FileDataSource;
- import javax.activation.FileTypeMap;
- import javax.mail.Address;
- import javax.mail.BodyPart;
- import javax.mail.Flags;
- import javax.mail.Message;
- import javax.mail.MessagingException;
- import javax.mail.Multipart;
- import javax.mail.Part;
- import javax.mail.Session;
- import javax.mail.internet.ContentType;
- import javax.mail.internet.InternetAddress;
- import javax.mail.internet.MimeBodyPart;
- import javax.mail.internet.MimeMessage;
- import javax.mail.internet.MimeMultipart;
- import javax.mail.internet.ParseException;
-
- public class MessageHelper {
- private MimeMessage imessage;
- private String raw = null;
-
- final static int NETWORK_TIMEOUT = 60 * 1000; // milliseconds
-
- static Properties getSessionProperties(int auth_type) {
- Properties props = new Properties();
-
- // https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#properties
- props.put("mail.imaps.ssl.checkserveridentity", "true");
- props.put("mail.imaps.ssl.trust", "*");
- props.put("mail.imaps.starttls.enable", "false");
-
- // TODO: make timeouts configurable?
- props.put("mail.imaps.connectiontimeout", Integer.toString(NETWORK_TIMEOUT));
- props.put("mail.imaps.timeout", Integer.toString(NETWORK_TIMEOUT));
- props.put("mail.imaps.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
-
- props.put("mail.imaps.connectionpool.debug", "true");
- props.put("mail.imaps.connectionpooltimeout", Integer.toString(3 * 60 * 1000)); // default: 45 sec
-
- // "mail.imaps.finalizecleanclose"
-
- // https://tools.ietf.org/html/rfc4978
- // https://docs.oracle.com/javase/8/docs/api/java/util/zip/Deflater.html
- if (true) {
- Log.i(Helper.TAG, "IMAP compress enabled");
- props.put("mail.imaps.compress.enable", "true");
- //props.put("mail.imaps.compress.level", "-1");
- //props.put("mail.imaps.compress.strategy", "0");
- }
-
- props.put("mail.imaps.fetchsize", Integer.toString(48 * 1024)); // default 16K
- props.put("mail.imaps.peek", "true");
-
- // https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html#properties
- props.put("mail.smtps.ssl.checkserveridentity", "true");
- props.put("mail.smtps.ssl.trust", "*");
- props.put("mail.smtps.starttls.enable", "false");
- props.put("mail.smtps.starttls.required", "false");
- props.put("mail.smtps.auth", "true");
-
- props.put("mail.smtps.connectiontimeout", Integer.toString(NETWORK_TIMEOUT));
- props.put("mail.smtps.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
- props.put("mail.smtps.timeout", Integer.toString(NETWORK_TIMEOUT));
-
- props.put("mail.smtp.ssl.checkserveridentity", "true");
- props.put("mail.smtp.ssl.trust", "*");
- props.put("mail.smtp.starttls.enable", "true");
- props.put("mail.smtp.starttls.required", "true");
- props.put("mail.smtp.auth", "true");
-
- props.put("mail.smtp.connectiontimeout", Integer.toString(NETWORK_TIMEOUT));
- props.put("mail.smtp.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
- props.put("mail.smtp.timeout", Integer.toString(NETWORK_TIMEOUT));
-
- props.put("mail.mime.address.strict", "false");
- props.put("mail.mime.decodetext.strict", "false");
-
- props.put("mail.mime.ignoreunknownencoding", "true"); // Content-Transfer-Encoding
- props.put("mail.mime.decodefilename", "true");
- props.put("mail.mime.encodefilename", "true");
-
- // https://docs.oracle.com/javaee/6/api/javax/mail/internet/MimeMultipart.html
- props.put("mail.mime.multipart.ignoremissingboundaryparameter", "true"); // javax.mail.internet.ParseException: In parameter list
- props.put("mail.mime.multipart.ignoreexistingboundaryparameter", "true");
-
- // The documentation is unclear/inconsistent whether this are system or session properties:
- System.setProperty("mail.mime.address.strict", "false");
- System.setProperty("mail.mime.decodetext.strict", "false");
-
- System.setProperty("mail.mime.ignoreunknownencoding", "true"); // Content-Transfer-Encoding
- System.setProperty("mail.mime.decodefilename", "true");
- System.setProperty("mail.mime.encodefilename", "true");
-
- System.setProperty("mail.mime.multipart.ignoremissingboundaryparameter", "true"); // javax.mail.internet.ParseException: In parameter list
- System.setProperty("mail.mime.multipart.ignoreexistingboundaryparameter", "true");
-
- if (false) {
- Log.i(Helper.TAG, "Prefering IPv4");
- System.setProperty("java.net.preferIPv4Stack", "true");
- }
-
- // https://javaee.github.io/javamail/OAuth2
- Log.i(Helper.TAG, "Auth type=" + auth_type);
- if (auth_type == Helper.AUTH_TYPE_GMAIL) {
- props.put("mail.imaps.auth.mechanisms", "XOAUTH2");
- props.put("mail.smtps.auth.mechanisms", "XOAUTH2");
- props.put("mail.smtp.auth.mechanisms", "XOAUTH2");
- }
-
- return props;
- }
-
- static MimeMessageEx from(Context context, EntityMessage message, EntityMessage reply, List<EntityAttachment> attachments, Session isession) throws MessagingException, IOException {
- MimeMessageEx imessage = new MimeMessageEx(isession, message.msgid);
-
- if (reply == null)
- imessage.addHeader("References", message.msgid);
- else {
- imessage.addHeader("In-Reply-To", reply.msgid);
- imessage.addHeader("References", (reply.references == null ? "" : reply.references + " ") + reply.msgid);
- }
-
- imessage.setFlag(Flags.Flag.SEEN, message.seen);
-
- if (message.from != null && message.from.length > 0)
- imessage.setFrom(message.from[0]);
-
- if (message.to != null && message.to.length > 0)
- imessage.setRecipients(Message.RecipientType.TO, message.to);
-
- if (message.cc != null && message.cc.length > 0)
- imessage.setRecipients(Message.RecipientType.CC, message.cc);
-
- if (message.bcc != null && message.bcc.length > 0)
- imessage.setRecipients(Message.RecipientType.BCC, message.bcc);
-
- if (message.subject != null)
- imessage.setSubject(message.subject);
-
- // TODO: plain message?
-
- String body = message.read(context);
-
- BodyPart plain = new MimeBodyPart();
- plain.setContent(Jsoup.parse(body).text(), "text/plain; charset=" + Charset.defaultCharset().name());
-
- BodyPart html = new MimeBodyPart();
- html.setContent(body, "text/html; charset=" + Charset.defaultCharset().name());
-
- Multipart alternative = new MimeMultipart("alternative");
- alternative.addBodyPart(plain);
- alternative.addBodyPart(html);
-
- if (attachments.size() == 0) {
- imessage.setContent(alternative);
- } else {
- Multipart multipart = new MimeMultipart("mixed");
-
- BodyPart bp = new MimeBodyPart();
- bp.setContent(alternative);
- multipart.addBodyPart(bp);
-
- for (final EntityAttachment attachment : attachments)
- if (attachment.available) {
- BodyPart bpAttachment = new MimeBodyPart();
- bpAttachment.setFileName(attachment.name);
-
- File file = EntityAttachment.getFile(context, attachment.id);
- FileDataSource dataSource = new FileDataSource(file);
- dataSource.setFileTypeMap(new FileTypeMap() {
- @Override
- public String getContentType(File file) {
- return attachment.type;
- }
-
- @Override
- public String getContentType(String filename) {
- return attachment.type;
- }
- });
- bpAttachment.setDataHandler(new DataHandler(dataSource));
- if (attachment.cid != null)
- bpAttachment.setHeader("Content-ID", attachment.cid);
-
- multipart.addBodyPart(bpAttachment);
- }
-
- imessage.setContent(multipart);
- }
-
- imessage.setSentDate(new Date());
-
- return imessage;
- }
-
- MessageHelper(MimeMessage message) {
- this.imessage = message;
- }
-
- MessageHelper(String raw, Session isession) throws MessagingException {
- byte[] bytes = Base64.decode(raw, Base64.URL_SAFE);
- InputStream is = new ByteArrayInputStream(bytes);
- this.imessage = new MimeMessage(isession, is);
- }
-
- boolean getSeen() throws MessagingException {
- return imessage.isSet(Flags.Flag.SEEN);
- }
-
- boolean getFlagged() throws MessagingException {
- return imessage.isSet(Flags.Flag.FLAGGED);
- }
-
- String getMessageID() throws MessagingException {
- return imessage.getHeader("Message-ID", null);
- }
-
- String[] getReferences() throws MessagingException {
- String refs = imessage.getHeader("References", null);
- return (refs == null ? new String[0] : refs.split("\\s+"));
- }
-
- String getDeliveredTo() throws MessagingException {
- return imessage.getHeader("Delivered-To", imessage.getHeader("X-Delivered-To", null));
- }
-
- String getInReplyTo() throws MessagingException {
- return imessage.getHeader("In-Reply-To", null);
- }
-
- String getThreadId(long uid) throws MessagingException {
- for (String ref : getReferences())
- if (!TextUtils.isEmpty(ref))
- return ref;
- String msgid = getMessageID();
- return (TextUtils.isEmpty(msgid) ? Long.toString(uid) : msgid);
- }
-
- Address[] getFrom() throws MessagingException {
- return imessage.getFrom();
- }
-
- Address[] getTo() throws MessagingException {
- return imessage.getRecipients(Message.RecipientType.TO);
- }
-
- Address[] getCc() throws MessagingException {
- return imessage.getRecipients(Message.RecipientType.CC);
- }
-
- Address[] getBcc() throws MessagingException {
- return imessage.getRecipients(Message.RecipientType.BCC);
- }
-
- Address[] getReply() throws MessagingException {
- String[] headers = imessage.getHeader("Reply-To");
- if (headers != null && headers.length > 0)
- return imessage.getReplyTo();
- else
- return null;
- }
-
- Integer getSize() throws MessagingException {
- int size = imessage.getSize();
- return (size < 0 ? null : size);
- }
-
- static String getFormattedAddresses(Address[] addresses, boolean full) {
- if (addresses == null || addresses.length == 0)
- return "";
-
- List<String> formatted = new ArrayList<>();
- for (Address address : addresses)
- if (address instanceof InternetAddress) {
- InternetAddress a = (InternetAddress) address;
- String personal = a.getPersonal();
- if (TextUtils.isEmpty(personal))
- formatted.add(address.toString());
- else {
- personal = personal.replaceAll("[\\,\\<\\>]", "");
- if (full)
- formatted.add(personal + " <" + a.getAddress() + ">");
- else
- formatted.add(personal);
- }
- } else
- formatted.add(address.toString());
- return TextUtils.join(", ", formatted);
- }
-
- String getHtml() throws MessagingException, IOException {
- return getHtml(imessage);
- }
-
- private static String getHtml(Part part) throws MessagingException, IOException {
- if (part.isMimeType("text/*")) {
- String s;
- try {
- s = part.getContent().toString();
- } catch (UnsupportedEncodingException ex) {
- // x-binaryenc
- Log.w(Helper.TAG, "Unsupported encoding: " + part.getContentType());
- // https://javaee.github.io/javamail/FAQ#unsupen
- InputStream is = part.getInputStream();
- ByteArrayOutputStream os = new ByteArrayOutputStream();
- byte[] buffer = new byte[4096];
- for (int len = is.read(buffer); len != -1; len = is.read(buffer))
- os.write(buffer, 0, len);
- s = new String(os.toByteArray(), "US-ASCII");
- } catch (IOException ex) {
- // IOException; Unknown encoding: none
- Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
- s = ex.toString();
- }
-
- if (part.isMimeType("text/plain"))
- s = "<pre>" + s.replaceAll("\\r?\\n", "<br />") + "</pre>";
- return s;
- }
-
- if (part.isMimeType("multipart/alternative")) {
- String text = null;
- try {
- Multipart mp = (Multipart) part.getContent();
- for (int i = 0; i < mp.getCount(); i++) {
- Part bp = mp.getBodyPart(i);
- if (bp.isMimeType("text/plain")) {
- if (text == null)
- text = getHtml(bp);
- } else if (bp.isMimeType("text/html")) {
- String s = getHtml(bp);
- if (s != null)
- return s;
- } else
- return getHtml(bp);
- }
- } catch (ParseException ex) {
- // ParseException: In parameter list boundary="...">, expected parameter name, got ";"
- Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
- text = ex.toString();
- }
- return text;
- }
-
- if (part.isMimeType("multipart/*"))
- try {
- Multipart mp = (Multipart) part.getContent();
- for (int i = 0; i < mp.getCount(); i++) {
- String s = getHtml(mp.getBodyPart(i));
- if (s != null)
- return s;
- }
- } catch (ParseException ex) {
- Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
- return ex.toString();
- }
-
- return null;
- }
-
- public List<EntityAttachment> getAttachments() throws IOException, MessagingException {
- List<EntityAttachment> result = new ArrayList<>();
-
- try {
- Object content = imessage.getContent();
- if (content instanceof String)
- return result;
-
- if (content instanceof Multipart) {
- Multipart multipart = (Multipart) content;
- for (int i = 0; i < multipart.getCount(); i++)
- result.addAll(getAttachments(multipart.getBodyPart(i)));
- }
- } catch (ParseException ex) {
- Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
- }
-
- return result;
- }
-
- private static List<EntityAttachment> getAttachments(BodyPart part) throws
- IOException, MessagingException {
- List<EntityAttachment> result = new ArrayList<>();
-
- Object content;
- try {
- content = part.getContent();
- } catch (UnsupportedEncodingException ex) {
- Log.w(Helper.TAG, "attachment content type=" + part.getContentType());
- content = part.getInputStream();
- } catch (ParseException ex) {
- Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
- content = null;
- }
-
- if (content instanceof InputStream || content instanceof String) {
- String disposition;
- try {
- disposition = part.getDisposition();
- } catch (MessagingException ex) {
- Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
- disposition = null;
- }
-
- String filename;
- try {
- filename = part.getFileName();
- } catch (MessagingException ex) {
- Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
- filename = null;
- }
-
- if (Part.ATTACHMENT.equalsIgnoreCase(disposition) ||
- part.isMimeType("image/*") ||
- !TextUtils.isEmpty(filename)) {
- ContentType ct = new ContentType(part.getContentType());
- String[] cid = part.getHeader("Content-ID");
-
- EntityAttachment attachment = new EntityAttachment();
- attachment.name = filename;
- attachment.type = ct.getBaseType().toLowerCase();
- attachment.size = part.getSize();
- attachment.cid = (cid == null || cid.length == 0 ? null : cid[0]);
- attachment.part = part;
-
- // Try to guess a better content type
- // Sometimes PDF files are sent using the wrong type
- if ("application/octet-stream".equals(attachment.type)) {
- String extension = Helper.getExtension(attachment.name);
- if (extension != null) {
- String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
- if (type != null) {
- Log.w(Helper.TAG, "Guessing file=" + attachment.name + " type=" + type);
- attachment.type = type;
- }
- }
- }
-
- if (attachment.size < 0)
- attachment.size = null;
-
- result.add(attachment);
- }
- } else if (content instanceof Multipart) {
- Multipart multipart = (Multipart) content;
- for (int i = 0; i < multipart.getCount(); i++)
- result.addAll(getAttachments(multipart.getBodyPart(i)));
- }
-
- return result;
- }
-
- String getRaw() throws IOException, MessagingException {
- if (raw == null) {
- ByteArrayOutputStream os = new ByteArrayOutputStream();
- imessage.writeTo(os);
- raw = Base64.encodeToString(os.toByteArray(), Base64.URL_SAFE);
- }
- return raw;
- }
- }
|