Files
FairEmail/app/src/main/java/eu/faircode/email/MessageHelper.java

457 lines
18 KiB
Java
Raw Normal View History

2018-08-02 13:33:06 +00:00
package eu.faircode.email;
/*
2018-08-14 05:53:24 +00:00
This file is part of FairEmail.
2018-08-02 13:33:06 +00:00
2018-08-14 05:53:24 +00:00
FairEmail is free software: you can redistribute it and/or modify
2018-08-02 13:33:06 +00:00
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)
*/
2018-08-13 08:58:36 +00:00
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
2018-08-02 13:33:06 +00:00
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
2018-09-05 07:57:34 +00:00
import android.webkit.MimeTypeMap;
2018-08-02 13:33:06 +00:00
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
2018-08-13 08:58:36 +00:00
import java.io.File;
2018-08-02 13:33:06 +00:00
import java.io.IOException;
import java.io.InputStream;
2018-08-22 15:13:01 +00:00
import java.io.UnsupportedEncodingException;
2018-08-11 06:50:21 +00:00
import java.nio.charset.Charset;
2018-08-02 13:33:06 +00:00
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
2018-08-11 06:50:21 +00:00
import javax.activation.DataHandler;
2018-08-13 08:58:36 +00:00
import javax.activation.FileDataSource;
import javax.activation.FileTypeMap;
2018-08-02 13:33:06 +00:00
import javax.mail.Address;
2018-08-03 12:07:51 +00:00
import javax.mail.BodyPart;
2018-08-02 13:33:06 +00:00
import javax.mail.Flags;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.Session;
2018-08-03 12:07:51 +00:00
import javax.mail.internet.ContentType;
2018-08-02 13:33:06 +00:00
import javax.mail.internet.InternetAddress;
2018-08-11 06:50:21 +00:00
import javax.mail.internet.MimeBodyPart;
2018-08-02 13:33:06 +00:00
import javax.mail.internet.MimeMessage;
2018-08-11 06:50:21 +00:00
import javax.mail.internet.MimeMultipart;
2018-09-06 10:24:30 +00:00
import javax.mail.internet.ParseException;
2018-08-02 13:33:06 +00:00
public class MessageHelper {
private MimeMessage imessage;
private String raw = null;
2018-09-08 18:03:10 +00:00
private final static int NETWORK_TIMEOUT = 60 * 1000; // milliseconds
static Properties getSessionProperties(Context context, int auth_type) {
2018-08-02 13:33:06 +00:00
Properties props = new Properties();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
2018-08-02 13:33:06 +00:00
// https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#properties
2018-08-08 06:55:47 +00:00
props.put("mail.imaps.ssl.checkserveridentity", "true");
props.put("mail.imaps.ssl.trust", "*");
props.put("mail.imaps.starttls.enable", "false");
2018-08-15 16:58:27 +00:00
// TODO: make timeouts configurable?
2018-09-08 18:03:10 +00:00
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
2018-08-02 13:33:06 +00:00
props.put("mail.imaps.connectionpooltimeout", Integer.toString(3 * 60 * 1000)); // default: 45 sec
// https://tools.ietf.org/html/rfc4978
// https://docs.oracle.com/javase/8/docs/api/java/util/zip/Deflater.html
if (prefs.getBoolean("compress", 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");
}
2018-08-15 16:58:27 +00:00
// https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html#properties
2018-08-08 06:55:47 +00:00
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");
2018-08-15 16:58:27 +00:00
2018-09-08 18:03:10 +00:00
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));
2018-08-08 06:55:47 +00:00
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");
2018-08-15 16:58:27 +00:00
2018-09-08 18:03:10 +00:00
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));
2018-08-02 13:33:06 +00:00
2018-08-26 12:17:09 +00:00
props.put("mail.imaps.peek", "true");
//props.put("mail.imaps.minidletime", "5000");
props.put("mail.mime.address.strict", "false");
props.put("mail.mime.decodetext.strict", "false");
2018-08-29 05:31:00 +00:00
props.put("mail.mime.ignoreunknownencoding", "true"); // Content-Transfer-Encoding
2018-08-28 18:47:22 +00:00
props.put("mail.mime.decodefilename", "true");
props.put("mail.mime.encodefilename", "true");
2018-08-29 05:31:00 +00:00
props.put("mail.mime.multipart.ignoremissingboundaryparameter", "true"); // javax.mail.internet.ParseException: In parameter list
2018-08-28 18:47:22 +00:00
props.put("mail.mime.multipart.ignoreexistingboundaryparameter", "true");
2018-08-27 14:31:45 +00:00
// https://javaee.github.io/javamail/OAuth2
2018-08-27 15:08:23 +00:00
Log.i(Helper.TAG, "Auth type=" + auth_type);
2018-08-27 14:31:45 +00:00
if (auth_type == Helper.AUTH_TYPE_GMAIL) {
props.put("mail.imaps.auth.mechanisms", "XOAUTH2");
props.put("mail.smtps.auth.mechanisms", "XOAUTH2");
2018-08-27 15:08:23 +00:00
props.put("mail.smtp.auth.mechanisms", "XOAUTH2");
2018-08-27 14:31:45 +00:00
}
2018-08-02 13:33:06 +00:00
return props;
}
2018-08-19 06:53:56 +00:00
static MimeMessageEx from(Context context, EntityMessage message, List<EntityAttachment> attachments, Session isession) throws MessagingException, IOException {
MimeMessageEx imessage = new MimeMessageEx(isession, message.msgid);
2018-08-02 13:33:06 +00:00
2018-08-11 17:31:33 +00:00
imessage.setFlag(Flags.Flag.SEEN, message.seen);
if (message.from != null && message.from.length > 0)
imessage.setFrom(message.from[0]);
2018-08-02 13:33:06 +00:00
if (message.to != null && message.to.length > 0)
imessage.setRecipients(Message.RecipientType.TO, message.to);
2018-08-02 13:33:06 +00:00
if (message.cc != null && message.cc.length > 0)
imessage.setRecipients(Message.RecipientType.CC, message.cc);
2018-08-03 07:39:43 +00:00
if (message.bcc != null && message.bcc.length > 0)
imessage.setRecipients(Message.RecipientType.BCC, message.bcc);
2018-08-02 13:33:06 +00:00
if (message.subject != null)
imessage.setSubject(message.subject);
2018-08-11 07:20:42 +00:00
// TODO: plain message?
if (attachments.size() == 0)
2018-08-19 06:53:56 +00:00
imessage.setText(message.read(context), Charset.defaultCharset().name(), "html");
else {
2018-08-11 06:50:21 +00:00
Multipart multipart = new MimeMultipart();
BodyPart bpMessage = new MimeBodyPart();
2018-08-19 06:53:56 +00:00
bpMessage.setContent(message.read(context), "text/html; charset=" + Charset.defaultCharset().name());
multipart.addBodyPart(bpMessage);
2018-08-11 06:50:21 +00:00
2018-08-13 08:58:36 +00:00
for (final EntityAttachment attachment : attachments)
2018-08-19 06:53:56 +00:00
if (attachment.available) {
2018-08-13 08:58:36 +00:00
BodyPart bpAttachment = new MimeBodyPart();
bpAttachment.setFileName(attachment.name);
2018-08-21 14:25:42 +00:00
File file = EntityAttachment.getFile(context, attachment.id);
2018-08-13 08:58:36 +00:00
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));
multipart.addBodyPart(bpAttachment);
}
2018-08-11 06:50:21 +00:00
imessage.setContent(multipart);
}
2018-08-02 13:33:06 +00:00
imessage.setSentDate(new Date());
return imessage;
}
2018-08-19 06:53:56 +00:00
static MimeMessageEx from(Context context, EntityMessage message, EntityMessage reply, List<EntityAttachment> attachments, Session isession) throws MessagingException, IOException {
2018-08-13 08:58:36 +00:00
MimeMessageEx imessage = from(context, message, attachments, isession);
2018-08-08 06:55:47 +00:00
imessage.addHeader("In-Reply-To", reply.msgid);
imessage.addHeader("References", (reply.references == null ? "" : reply.references + " ") + reply.msgid);
2018-08-02 13:33:06 +00:00
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);
}
2018-08-03 12:07:51 +00:00
boolean getSeen() throws MessagingException {
return imessage.isSet(Flags.Flag.SEEN);
}
2018-09-07 15:12:43 +00:00
boolean getFlagged() throws MessagingException {
return imessage.isSet(Flags.Flag.FLAGGED);
}
2018-08-02 13:33:06 +00:00
String getMessageID() throws MessagingException {
2018-08-08 06:55:47 +00:00
return imessage.getHeader("Message-ID", null);
2018-08-02 13:33:06 +00:00
}
String[] getReferences() throws MessagingException {
2018-08-08 06:55:47 +00:00
String refs = imessage.getHeader("References", null);
2018-08-02 13:33:06 +00:00
return (refs == null ? new String[0] : refs.split("\\s+"));
}
String getInReplyTo() throws MessagingException {
2018-08-08 06:55:47 +00:00
return imessage.getHeader("In-Reply-To", null);
2018-08-02 13:33:06 +00:00
}
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();
2018-08-02 13:33:06 +00:00
}
Address[] getTo() throws MessagingException {
return imessage.getRecipients(Message.RecipientType.TO);
2018-08-02 13:33:06 +00:00
}
Address[] getCc() throws MessagingException {
return imessage.getRecipients(Message.RecipientType.CC);
2018-08-02 13:33:06 +00:00
}
Address[] getBcc() throws MessagingException {
return imessage.getRecipients(Message.RecipientType.BCC);
2018-08-03 07:39:43 +00:00
}
Address[] getReply() throws MessagingException {
String[] headers = imessage.getHeader("Reply-To");
if (headers != null && headers.length > 0)
return imessage.getReplyTo();
else
return null;
2018-08-02 13:33:06 +00:00
}
2018-08-09 21:33:14 +00:00
static String getFormattedAddresses(Address[] addresses, boolean full) {
2018-08-28 14:59:52 +00:00
if (addresses == null || addresses.length == 0)
return "";
2018-08-02 13:33:06 +00:00
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());
2018-09-05 07:39:05 +00:00
else {
personal = personal.replaceAll("[\\,\\<\\>]", "");
if (full)
formatted.add(personal + " <" + a.getAddress() + ">");
else
formatted.add(personal);
}
} else
formatted.add(address.toString());
2018-08-08 06:55:47 +00:00
return TextUtils.join(", ", formatted);
2018-08-02 13:33:06 +00:00
}
2018-08-29 05:31:00 +00:00
String getHtml() throws MessagingException, IOException {
2018-08-03 12:07:51 +00:00
return getHtml(imessage);
}
2018-09-06 09:52:02 +00:00
private static String getHtml(Part part) throws MessagingException, IOException {
2018-08-29 05:31:00 +00:00
if (part.isMimeType("text/*")) {
String s;
2018-08-02 13:33:06 +00:00
try {
2018-08-29 05:31:00 +00:00
s = part.getContent().toString();
} catch (UnsupportedEncodingException ex) {
2018-08-29 05:31:00 +00:00
// x-binaryenc
Log.w(Helper.TAG, "Unsupported encoding: " + part.getContentType());
2018-08-28 18:34:53 +00:00
// https://javaee.github.io/javamail/FAQ#unsupen
2018-08-29 05:31:00 +00:00
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");
2018-09-06 10:24:30 +00:00
} catch (IOException ex) {
// IOException; Unknown encoding: none
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
s = ex.toString();
2018-08-02 13:33:06 +00:00
}
2018-08-29 05:31:00 +00:00
if (part.isMimeType("text/plain"))
s = "<pre>" + s.replaceAll("\\r?\\n", "<br />") + "</pre>";
return s;
}
2018-08-02 13:33:06 +00:00
if (part.isMimeType("multipart/alternative")) {
String text = null;
2018-09-06 10:24:30 +00:00
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();
2018-08-02 13:33:06 +00:00
}
return text;
}
2018-09-06 10:24:30 +00:00
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();
2018-08-02 13:33:06 +00:00
}
return null;
}
2018-08-03 12:07:51 +00:00
public List<EntityAttachment> getAttachments() throws IOException, MessagingException {
List<EntityAttachment> result = new ArrayList<>();
2018-09-06 10:24:30 +00:00
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));
2018-08-03 12:07:51 +00:00
}
return result;
}
2018-09-06 09:52:02 +00:00
private static List<EntityAttachment> getAttachments(BodyPart part) throws
IOException, MessagingException {
2018-08-03 12:07:51 +00:00
List<EntityAttachment> result = new ArrayList<>();
2018-08-29 07:26:10 +00:00
Object content;
try {
content = part.getContent();
} catch (UnsupportedEncodingException ex) {
Log.w(Helper.TAG, "attachment content type=" + part.getContentType());
content = part.getInputStream();
2018-09-06 10:24:30 +00:00
} catch (ParseException ex) {
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
content = null;
2018-08-29 07:26:10 +00:00
}
2018-09-06 10:24:30 +00:00
2018-08-03 12:07:51 +00:00
if (content instanceof InputStream || content instanceof String) {
2018-08-22 15:13:01 +00:00
String disposition;
try {
disposition = part.getDisposition();
} catch (MessagingException ex) {
2018-09-06 07:16:33 +00:00
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
2018-08-22 15:13:01 +00:00
disposition = null;
}
String filename;
try {
filename = part.getFileName();
} catch (MessagingException ex) {
2018-09-06 07:16:33 +00:00
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
2018-08-22 15:13:01 +00:00
filename = null;
}
2018-08-22 16:55:18 +00:00
if (Part.ATTACHMENT.equalsIgnoreCase(disposition) || !TextUtils.isEmpty(filename)) {
2018-08-03 12:07:51 +00:00
ContentType ct = new ContentType(part.getContentType());
EntityAttachment attachment = new EntityAttachment();
2018-08-22 15:13:01 +00:00
attachment.name = filename;
2018-08-03 12:07:51 +00:00
attachment.type = ct.getBaseType();
2018-08-04 10:32:34 +00:00
attachment.size = part.getSize();
2018-08-03 19:12:19 +00:00
attachment.part = part;
2018-09-05 07:57:34 +00:00
// Try to guess a better content type
// Sometimes PDF files are sent using the wrong type
if ("application/octet-stream".equals(attachment.type) && attachment.name != null) {
String extension = MimeTypeMap.getFileExtensionFromUrl(attachment.name.toLowerCase());
if (extension != null) {
String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (type != null) {
Log.w(Helper.TAG, "Guessing file=" + attachment.name + " type=" + type);
attachment.type = type;
}
}
}
2018-08-04 10:32:34 +00:00
if (attachment.size < 0)
attachment.size = null;
2018-09-05 07:57:34 +00:00
2018-08-03 12:07:51 +00:00
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;
2018-08-02 13:33:06 +00:00
}
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;
}
}