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

1394 lines
53 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.
2018-10-29 10:46:49 +00:00
FairEmail is distributed in the hope that it will be useful,
2018-08-02 13:33:06 +00:00
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
2018-10-29 10:46:49 +00:00
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
2018-08-02 13:33:06 +00:00
2018-12-31 08:04:33 +00:00
Copyright 2018-2019 by Marcel Bokhorst (M66B)
2018-08-02 13:33:06 +00:00
*/
2018-08-13 08:58:36 +00:00
import android.content.Context;
2019-08-22 16:09:52 +00:00
import android.content.SharedPreferences;
2019-05-21 12:50:10 +00:00
import android.net.MailTo;
2019-09-08 10:57:21 +00:00
import android.net.Uri;
2018-08-02 13:33:06 +00:00
import android.text.TextUtils;
2019-08-22 16:09:52 +00:00
import androidx.preference.PreferenceManager;
2019-04-25 18:40:37 +00:00
import com.sun.mail.util.FolderClosedIOException;
2019-06-02 05:58:43 +00:00
import com.sun.mail.util.MessageRemovedIOException;
2019-04-25 18:40:37 +00:00
2019-08-22 14:11:49 +00:00
import org.jsoup.nodes.Document;
2019-11-19 20:53:12 +00:00
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
2019-08-22 14:11:49 +00:00
2018-10-28 14:29:45 +00:00
import java.io.BufferedReader;
2018-08-13 08:58:36 +00:00
import java.io.File;
2019-01-16 10:57:57 +00:00
import java.io.FileOutputStream;
2018-10-28 14:29:45 +00:00
import java.io.FileReader;
2018-08-02 13:33:06 +00:00
import java.io.IOException;
import java.io.InputStream;
2019-01-16 10:57:57 +00:00
import java.io.OutputStream;
2019-08-21 09:55:42 +00:00
import java.io.UnsupportedEncodingException;
2018-08-11 06:50:21 +00:00
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
2018-08-02 13:33:06 +00:00
import java.util.ArrayList;
2019-10-01 11:56:48 +00:00
import java.util.Arrays;
2018-08-02 13:33:06 +00:00
import java.util.Date;
2019-01-25 09:37:43 +00:00
import java.util.Enumeration;
2019-08-21 15:25:23 +00:00
import java.util.HashMap;
2018-08-02 13:33:06 +00:00
import java.util.List;
2019-09-23 20:07:22 +00:00
import java.util.Locale;
2019-08-21 15:25:23 +00:00
import java.util.Map;
2019-06-26 19:21:09 +00:00
import java.util.Objects;
2018-08-02 13:33:06 +00:00
import java.util.Properties;
2019-06-14 15:25:54 +00:00
import java.util.TimeZone;
2018-08-02 13:33:06 +00:00
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.FolderClosedException;
2019-01-25 09:37:43 +00:00
import javax.mail.Header;
2018-08-02 13:33:06 +00:00
import javax.mail.Message;
import javax.mail.MessageRemovedException;
2018-08-02 13:33:06 +00:00
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.Session;
2019-01-19 19:08:01 +00:00
import javax.mail.internet.AddressException;
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;
2019-06-14 15:25:54 +00:00
import javax.mail.internet.MailDateFormat;
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;
2019-01-01 13:58:51 +00:00
import javax.mail.internet.MimeUtility;
2018-09-06 10:24:30 +00:00
import javax.mail.internet.ParseException;
2018-08-02 13:33:06 +00:00
2019-05-18 17:44:21 +00:00
import biweekly.Biweekly;
import biweekly.ICalendar;
2018-08-02 13:33:06 +00:00
public class MessageHelper {
private MimeMessage imessage;
static final int SMALL_MESSAGE_SIZE = 32 * 1024; // bytes
2019-07-14 19:34:43 +00:00
static final int DEFAULT_ATTACHMENT_DOWNLOAD_SIZE = 256 * 1024; // bytes
2019-01-16 10:57:57 +00:00
2019-07-26 15:57:40 +00:00
static void setSystemProperties(Context context) {
2019-02-06 13:51:41 +00:00
System.setProperty("mail.mime.decodetext.strict", "false");
System.setProperty("mail.mime.ignoreunknownencoding", "true"); // Content-Transfer-Encoding
System.setProperty("mail.mime.base64.ignoreerrors", "true");
System.setProperty("mail.mime.decodefilename", "true");
System.setProperty("mail.mime.encodefilename", "true");
2019-02-17 10:47:50 +00:00
System.setProperty("mail.mime.allowutf8", "false"); // InternetAddress, MimeBodyPart, MimeUtility
2019-06-14 15:12:05 +00:00
System.setProperty("mail.mime.cachemultipart", "false");
2019-02-06 13:51:41 +00:00
// https://docs.oracle.com/javaee/6/api/javax/mail/internet/MimeMultipart.html
System.setProperty("mail.mime.multipart.ignoremissingboundaryparameter", "true"); // javax.mail.internet.ParseException: In parameter list
System.setProperty("mail.mime.multipart.ignoreexistingboundaryparameter", "true");
2019-11-18 12:06:05 +00:00
System.setProperty("mail.mime.multipart.ignoremissingendboundary", "true");
System.setProperty("mail.mime.multipart.allowempty", "true");
2019-02-06 13:51:41 +00:00
}
2019-07-29 14:42:17 +00:00
static Properties getSessionProperties() {
2018-08-02 13:33:06 +00:00
Properties props = new Properties();
2019-02-09 21:03:53 +00:00
// MIME
2019-02-17 10:47:50 +00:00
props.put("mail.mime.allowutf8", "false"); // SMTPTransport, MimeMessage
2018-08-26 12:17:09 +00:00
props.put("mail.mime.address.strict", "false");
2018-09-16 14:01:18 +00:00
2018-08-02 13:33:06 +00:00
return props;
}
2019-04-17 15:27:57 +00:00
static MimeMessageEx from(Context context, EntityMessage message, EntityIdentity identity, Session isession)
throws MessagingException, IOException {
DB db = DB.getInstance(context);
MimeMessageEx imessage = new MimeMessageEx(isession, message.msgid);
2018-08-02 13:33:06 +00:00
// Flags
imessage.setFlag(Flags.Flag.SEEN, message.seen);
imessage.setFlag(Flags.Flag.FLAGGED, message.flagged);
imessage.setFlag(Flags.Flag.ANSWERED, message.answered);
// Priority
2019-09-30 14:55:58 +00:00
if (EntityMessage.PRIORITIY_LOW.equals(message.priority)) {
// Low
imessage.addHeader("Importance", "Low");
imessage.addHeader("Priority", "Non-Urgent");
imessage.addHeader("X-Priority", "5"); // Lowest
imessage.addHeader("X-MSMail-Priority", "Low");
} else if (EntityMessage.PRIORITIY_HIGH.equals(message.priority)) {
// High
imessage.addHeader("Importance", "High");
imessage.addHeader("Priority", "Urgent");
imessage.addHeader("X-Priority", "1"); // Highest
imessage.addHeader("X-MSMail-Priority", "High");
}
// References
2019-01-21 16:45:05 +00:00
if (message.references != null)
imessage.addHeader("References", message.references);
if (message.inreplyto != null)
imessage.addHeader("In-Reply-To", message.inreplyto);
2019-09-01 14:02:48 +00:00
imessage.addHeader("X-Correlation-ID", message.msgid);
2018-11-20 15:51:51 +00:00
// Addresses
if (message.from != null && message.from.length > 0) {
2018-11-09 07:22:44 +00:00
String email = ((InternetAddress) message.from[0]).getAddress();
String name = ((InternetAddress) message.from[0]).getPersonal();
if (email != null && identity != null && identity.sender_extra && !TextUtils.isEmpty(message.extra)) {
2018-11-09 07:22:44 +00:00
int at = email.indexOf('@');
2019-02-13 08:08:40 +00:00
email = message.extra + email.substring(at);
2018-12-24 12:27:45 +00:00
Log.i("extra=" + email);
2018-11-09 07:22:44 +00:00
}
imessage.setFrom(new InternetAddress(email, name));
}
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);
2019-10-01 11:56:48 +00:00
// Send message
if (identity != null) {
// Add reply to
if (identity.replyto != null)
imessage.setReplyTo(InternetAddress.parse(identity.replyto));
// Add extra bcc
if (identity.bcc != null) {
List<Address> bcc = new ArrayList<>();
2019-10-01 11:56:48 +00:00
Address[] existing = imessage.getRecipients(Message.RecipientType.BCC);
if (existing != null)
bcc.addAll(Arrays.asList(existing));
Address[] all = imessage.getAllRecipients();
Address[] abccs = InternetAddress.parse(identity.bcc);
for (Address abcc : abccs) {
boolean found = false;
if (all != null)
for (Address a : all)
if (equalEmail(a, abcc)) {
found = true;
break;
}
if (!found)
bcc.add(abcc);
}
2019-10-01 11:56:48 +00:00
imessage.setRecipients(Message.RecipientType.BCC, bcc.toArray(new Address[0]));
}
// Delivery/read request
if (message.receipt_request != null && message.receipt_request) {
String to = (identity.replyto == null ? identity.email : identity.replyto);
// defacto standard
imessage.addHeader("Return-Receipt-To", to);
// https://tools.ietf.org/html/rfc3798
imessage.addHeader("Disposition-Notification-To", to);
}
}
// Auto answer
if (message.unsubscribe != null)
imessage.addHeader("List-Unsubscribe", "<" + message.unsubscribe + ">");
2019-06-14 15:25:54 +00:00
MailDateFormat mdf = new MailDateFormat();
mdf.setTimeZone(TimeZone.getTimeZone("UTC"));
imessage.setHeader("Date", mdf.format(new Date()));
//imessage.setSentDate(new Date());
2018-10-28 12:49:13 +00:00
List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
2018-10-28 14:29:45 +00:00
if (message.from != null && message.from.length > 0)
for (EntityAttachment attachment : attachments)
2019-06-30 06:56:33 +00:00
if (attachment.available && EntityAttachment.PGP_KEY.equals(attachment.encryption)) {
2018-10-28 14:29:45 +00:00
InternetAddress from = (InternetAddress) message.from[0];
2019-03-14 07:18:42 +00:00
File file = attachment.getFile(context);
2018-10-28 14:29:45 +00:00
StringBuilder sb = new StringBuilder();
2019-02-22 15:59:23 +00:00
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
2018-10-28 14:29:45 +00:00
String line;
while ((line = br.readLine()) != null)
if (!line.startsWith("-----") && !line.endsWith("-----"))
sb.append(line);
}
2019-07-16 15:58:43 +00:00
imessage.addHeader("Autocrypt",
"addr=" + from.getAddress() + ";" +
" prefer-encrypt=mutual;" +
" keydata=" + sb.toString());
2018-10-28 14:29:45 +00:00
}
2018-10-28 12:49:13 +00:00
for (final EntityAttachment attachment : attachments)
2019-01-05 14:09:47 +00:00
if (attachment.available && EntityAttachment.PGP_MESSAGE.equals(attachment.encryption)) {
2019-11-18 17:44:23 +00:00
// https://tools.ietf.org/html/rfc3156
2018-10-28 12:49:13 +00:00
Multipart multipart = new MimeMultipart("encrypted; protocol=\"application/pgp-encrypted\"");
BodyPart pgp = new MimeBodyPart();
pgp.setContent("", "application/pgp-encrypted");
multipart.addBodyPart(pgp);
BodyPart bpAttachment = new MimeBodyPart();
bpAttachment.setFileName(attachment.name);
2019-03-14 07:18:42 +00:00
File file = attachment.getFile(context);
2018-10-28 12:49:13 +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));
bpAttachment.setDisposition(Part.INLINE);
multipart.addBodyPart(bpAttachment);
imessage.setContent(multipart);
return imessage;
}
2018-08-11 07:20:42 +00:00
2019-04-23 19:21:06 +00:00
build(context, message, attachments, identity, imessage);
2018-10-29 09:13:29 +00:00
return imessage;
}
2019-04-23 19:21:06 +00:00
static void build(Context context, EntityMessage message, List<EntityAttachment> attachments, EntityIdentity identity, MimeMessage imessage) throws IOException, MessagingException {
2019-10-01 11:56:48 +00:00
if (message.receipt != null && message.receipt) {
2019-04-18 17:13:38 +00:00
// https://www.ietf.org/rfc/rfc3798.txt
Multipart report = new MimeMultipart("report; report-type=disposition-notification");
String plainContent = HtmlHelper.getText(Helper.readText(message.getFile(context)));
BodyPart plainPart = new MimeBodyPart();
plainPart.setContent(plainContent, "text/plain; charset=" + Charset.defaultCharset().name());
report.addBodyPart(plainPart);
2019-10-02 07:25:18 +00:00
String from = null;
if (message.from != null && message.from.length > 0)
from = ((InternetAddress) message.from[0]).getAddress();
StringBuilder sb = new StringBuilder();
sb.append("Reporting-UA: ").append(BuildConfig.APPLICATION_ID).append("; ").append(BuildConfig.VERSION_NAME).append("\r\n");
if (from != null)
sb.append("Original-Recipient: rfc822;").append(from).append("\r\n");
sb.append("Disposition: manual-action/MDN-sent-manually; displayed").append("\r\n");
2019-04-18 17:13:38 +00:00
BodyPart dnsPart = new MimeBodyPart();
2019-10-02 07:25:18 +00:00
dnsPart.setContent(sb.toString(), "message/disposition-notification; name=\"MDNPart2.txt\"");
2019-04-18 17:13:38 +00:00
dnsPart.setDisposition(Part.INLINE);
report.addBodyPart(dnsPart);
//BodyPart headersPart = new MimeBodyPart();
//headersPart.setContent("", "text/rfc822-headers; name=\"MDNPart3.txt\"");
//headersPart.setDisposition(Part.INLINE);
//report.addBodyPart(headersPart);
imessage.setContent(report);
return;
}
2019-08-22 14:11:49 +00:00
// Build html body
2019-11-19 20:53:12 +00:00
Document document = JsoupEx.parse(Helper.readText(message.getFile(context)));
Elements ref = document.select("div[fairemail=reference]");
ref.remove();
ref.removeAttr("fairemail");
if (document.body() != null) {
// When sending message
if (identity != null && !TextUtils.isEmpty(identity.signature) && message.signature) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean usenet = prefs.getBoolean("usenet_signature", false);
if (usenet) {
// https://www.ietf.org/rfc/rfc3676.txt
Element span = document.createElement("span");
span.text("-- ");
span.appendElement("br");
document.body().appendChild(span);
2019-08-22 16:09:52 +00:00
}
2019-11-19 20:53:12 +00:00
document.body().append(identity.signature);
2019-08-22 14:11:49 +00:00
}
2019-11-19 20:53:12 +00:00
if (ref.size() > 0)
document.body().appendChild(ref.first());
}
2019-01-21 16:45:05 +00:00
2019-07-17 07:38:40 +00:00
// multipart/mixed
// multipart/related
// multipart/alternative
// text/plain
// text/html
// inlines
// attachments
2019-11-19 20:53:12 +00:00
String htmlContent = document.html();
2019-08-22 14:11:49 +00:00
String plainContent = HtmlHelper.getText(htmlContent);
2019-01-06 13:51:38 +00:00
BodyPart plainPart = new MimeBodyPart();
plainPart.setContent(plainContent, "text/plain; charset=" + Charset.defaultCharset().name());
BodyPart htmlPart = new MimeBodyPart();
2019-08-22 14:11:49 +00:00
htmlPart.setContent(htmlContent, "text/html; charset=" + Charset.defaultCharset().name());
2019-01-06 13:51:38 +00:00
2019-07-17 07:38:40 +00:00
Multipart altMultiPart = new MimeMultipart("alternative");
altMultiPart.addBodyPart(plainPart);
altMultiPart.addBodyPart(htmlPart);
boolean plain_only = (message.plain_only != null && message.plain_only);
2019-01-06 13:51:38 +00:00
2019-07-17 07:38:40 +00:00
int availableAttachments = 0;
boolean hasInline = false;
2019-01-11 18:54:55 +00:00
for (EntityAttachment attachment : attachments)
2019-07-17 07:38:40 +00:00
if (attachment.available) {
availableAttachments++;
if (attachment.isInline())
hasInline = true;
}
2019-01-11 18:54:55 +00:00
2019-07-17 07:38:40 +00:00
if (availableAttachments == 0)
if (plain_only)
imessage.setContent(plainContent, "text/plain; charset=" + Charset.defaultCharset().name());
else
2019-07-17 07:38:40 +00:00
imessage.setContent(altMultiPart);
2019-01-06 13:51:38 +00:00
else {
2019-07-17 07:38:40 +00:00
Multipart mixedMultiPart = new MimeMultipart("mixed");
Multipart relatedMultiPart = new MimeMultipart("related");
2018-08-11 06:50:21 +00:00
2019-07-17 07:38:40 +00:00
BodyPart bodyPart = new MimeBodyPart();
if (plain_only)
bodyPart.setContent(plainContent, "text/plain; charset=" + Charset.defaultCharset().name());
else
2019-07-17 07:38:40 +00:00
bodyPart.setContent(altMultiPart);
if (hasInline && !plain_only) {
relatedMultiPart.addBodyPart(bodyPart);
MimeBodyPart relatedPart = new MimeBodyPart();
relatedPart.setContent(relatedMultiPart);
mixedMultiPart.addBodyPart(relatedPart);
} else
mixedMultiPart.addBodyPart(bodyPart);
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) {
2019-07-17 07:38:40 +00:00
BodyPart attachmentPart = new MimeBodyPart();
2018-08-13 08:58:36 +00:00
2019-03-14 07:18:42 +00:00
File file = attachment.getFile(context);
2019-05-18 17:44:21 +00:00
2018-08-13 08:58:36 +00:00
FileDataSource dataSource = new FileDataSource(file);
dataSource.setFileTypeMap(new FileTypeMap() {
@Override
public String getContentType(File file) {
2019-05-18 17:44:21 +00:00
// https://tools.ietf.org/html/rfc6047
if ("text/calendar".equals(attachment.type))
try {
ICalendar icalendar = Biweekly.parse(file).first();
if (icalendar != null &&
icalendar.getMethod() != null &&
icalendar.getMethod().isReply())
return "text/calendar" +
"; method=REPLY" +
"; charset=" + Charset.defaultCharset().name();
} catch (IOException ex) {
Log.e(ex);
}
2018-08-13 08:58:36 +00:00
return attachment.type;
}
@Override
public String getContentType(String filename) {
2019-05-18 17:44:21 +00:00
return getContentType(new File(filename));
2018-08-13 08:58:36 +00:00
}
});
2019-07-17 07:38:40 +00:00
attachmentPart.setDataHandler(new DataHandler(dataSource));
2019-05-18 17:44:21 +00:00
2019-07-17 07:38:40 +00:00
attachmentPart.setFileName(attachment.name);
if (attachment.disposition != null)
2019-07-17 07:38:40 +00:00
attachmentPart.setDisposition(attachment.disposition);
2018-09-13 18:31:05 +00:00
if (attachment.cid != null)
2019-07-17 07:38:40 +00:00
attachmentPart.setHeader("Content-ID", attachment.cid);
2018-08-13 08:58:36 +00:00
2019-07-17 07:38:40 +00:00
if (attachment.isInline() && !plain_only)
relatedMultiPart.addBodyPart(attachmentPart);
else
mixedMultiPart.addBodyPart(attachmentPart);
2018-08-13 08:58:36 +00:00
}
2018-08-11 06:50:21 +00:00
2019-07-17 07:38:40 +00:00
imessage.setContent(mixedMultiPart);
2018-08-11 06:50:21 +00:00
}
2018-08-02 13:33:06 +00:00
}
MessageHelper(MimeMessage message) {
this.imessage = message;
}
2018-08-03 12:07:51 +00:00
boolean getSeen() throws MessagingException {
return imessage.isSet(Flags.Flag.SEEN);
}
2018-11-24 18:14:28 +00:00
boolean getAnsered() throws MessagingException {
return imessage.isSet(Flags.Flag.ANSWERED);
}
2018-09-07 15:12:43 +00:00
boolean getFlagged() throws MessagingException {
return imessage.isSet(Flags.Flag.FLAGGED);
}
2019-01-29 20:15:24 +00:00
String getFlags() throws MessagingException {
if (!BuildConfig.DEBUG)
return null;
Flags flags = imessage.getFlags();
flags.clearUserFlags();
return flags.toString();
}
2018-11-25 12:34:08 +00:00
String[] getKeywords() throws MessagingException {
return imessage.getFlags().getUserFlags();
}
2018-08-02 13:33:06 +00:00
String getMessageID() throws MessagingException {
// Outlook outbox -> sent
2019-09-01 14:02:48 +00:00
String header = imessage.getHeader("X-Correlation-ID", null);
2019-05-03 07:26:11 +00:00
if (header == null)
header = imessage.getHeader("Message-ID", null);
return (header == null ? null : MimeUtility.unfold(header));
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);
2019-05-03 07:26:11 +00:00
return (refs == null ? new String[0] : MimeUtility.unfold(refs).split("\\s+"));
2018-08-02 13:33:06 +00:00
}
2018-09-18 08:55:59 +00:00
String getDeliveredTo() throws MessagingException {
2019-04-27 14:05:28 +00:00
String header = imessage.getHeader("Delivered-To", null);
if (header == null)
header = imessage.getHeader("X-Delivered-To", null);
2019-05-03 07:26:11 +00:00
return (header == null ? null : MimeUtility.unfold(header));
2018-09-18 08:55:59 +00:00
}
2018-08-02 13:33:06 +00:00
String getInReplyTo() throws MessagingException {
2019-05-03 07:26:11 +00:00
String header = imessage.getHeader("In-Reply-To", null);
return (header == null ? null : MimeUtility.unfold(header));
2018-08-02 13:33:06 +00:00
}
2019-03-30 10:57:29 +00:00
String getThreadId(Context context, long account, long uid) throws MessagingException {
List<String> refs = new ArrayList<>();
2018-08-02 13:33:06 +00:00
for (String ref : getReferences())
if (!TextUtils.isEmpty(ref))
2019-03-30 10:57:29 +00:00
refs.add(ref);
String inreplyto = getInReplyTo();
2019-03-30 10:57:29 +00:00
if (!TextUtils.isEmpty(inreplyto) && !refs.contains(inreplyto))
refs.add(inreplyto);
DB db = DB.getInstance(context);
for (String ref : refs) {
2019-11-06 16:38:55 +00:00
List<EntityMessage> messages = db.message().getMessagesByMsgId(account, ref);
2019-03-30 10:57:29 +00:00
if (messages.size() > 0)
return messages.get(0).thread;
}
if (refs.size() > 0)
return refs.get(0);
2018-08-02 13:33:06 +00:00
String msgid = getMessageID();
return (TextUtils.isEmpty(msgid) ? Long.toString(uid) : msgid);
}
2019-09-30 14:55:58 +00:00
Integer getPriority() throws MessagingException {
Integer priority = null;
// https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/2bb19f1b-b35e-4966-b1cb-1afd044e83ab
String header = imessage.getHeader("Importance", null);
if (header == null)
header = imessage.getHeader("Priority", null);
if (header == null)
header = imessage.getHeader("X-Priority", null);
if (header == null)
header = imessage.getHeader("X-MSMail-Priority", null);
2019-10-01 07:36:08 +00:00
if (header != null) {
int sp = header.indexOf(" ");
if (sp >= 0)
header = header.substring(0, sp); // "2 (High)"
2019-10-07 18:20:28 +00:00
header = header.trim();
2019-10-01 07:36:08 +00:00
}
2019-10-02 11:58:45 +00:00
if ("high".equalsIgnoreCase(header) ||
2019-10-07 18:20:28 +00:00
"highest".equalsIgnoreCase(header) ||
2019-10-09 14:13:43 +00:00
"u".equalsIgnoreCase(header) || // Urgent?
2019-10-02 11:58:45 +00:00
"urgent".equalsIgnoreCase(header) ||
2019-10-04 19:26:32 +00:00
"critical".equalsIgnoreCase(header) ||
"yes".equalsIgnoreCase(header))
2019-09-30 14:55:58 +00:00
priority = EntityMessage.PRIORITIY_HIGH;
2019-10-02 11:58:45 +00:00
else if ("normal".equalsIgnoreCase(header) ||
2019-10-20 12:01:34 +00:00
"medium".equalsIgnoreCase(header) ||
"med".equalsIgnoreCase(header))
2019-09-30 14:55:58 +00:00
priority = EntityMessage.PRIORITIY_NORMAL;
2019-10-02 11:58:45 +00:00
else if ("low".equalsIgnoreCase(header) ||
"non-urgent".equalsIgnoreCase(header) ||
2019-10-03 10:59:11 +00:00
"marketing".equalsIgnoreCase(header) ||
2019-10-13 19:30:27 +00:00
"bulk".equalsIgnoreCase(header) ||
2019-10-21 07:06:53 +00:00
"batch".equalsIgnoreCase(header) ||
2019-10-13 19:30:27 +00:00
"b".equalsIgnoreCase(header))
2019-09-30 14:55:58 +00:00
priority = EntityMessage.PRIORITIY_LOW;
2019-10-07 18:20:28 +00:00
else if (!TextUtils.isEmpty(header))
2019-09-30 14:55:58 +00:00
try {
priority = Integer.parseInt(header);
if (priority < 3)
priority = EntityMessage.PRIORITIY_HIGH;
else if (priority > 3)
priority = EntityMessage.PRIORITIY_LOW;
else
priority = EntityMessage.PRIORITIY_NORMAL;
} catch (NumberFormatException ex) {
Log.e("priority=" + header);
}
if (EntityMessage.PRIORITIY_NORMAL.equals(priority))
priority = null;
return priority;
}
boolean getReceiptRequested() throws MessagingException {
return (imessage.getHeader("Return-Receipt-To") != null ||
imessage.getHeader("Disposition-Notification-To") != null);
}
2019-04-18 17:13:38 +00:00
Address[] getReceiptTo() throws MessagingException {
2019-06-22 11:26:45 +00:00
return getAddressHeader("Disposition-Notification-To");
2019-04-18 17:13:38 +00:00
}
String getAuthentication() throws MessagingException {
2019-05-03 07:26:11 +00:00
String header = imessage.getHeader("Authentication-Results", null);
return (header == null ? null : MimeUtility.unfold(header));
}
2019-04-12 18:41:06 +00:00
static Boolean getAuthentication(String type, String header) {
if (header == null)
return null;
// https://tools.ietf.org/html/rfc7601
Boolean result = null;
String[] part = header.split(";");
for (int i = 1; i < part.length; i++) {
String[] kv = part[i].split("=");
if (kv.length > 1) {
String key = kv[0].trim();
String[] val = kv[1].trim().split(" ");
if (val.length > 0 && type.equals(key)) {
if ("fail".equals(val[0]))
result = false;
else if ("pass".equals(val[0]))
if (result == null)
result = true;
}
}
}
return result;
}
2019-06-22 11:26:45 +00:00
private Address[] getAddressHeader(String name) throws MessagingException {
String header = imessage.getHeader(name, ",");
if (header == null)
return null;
header = new String(header.getBytes(StandardCharsets.ISO_8859_1));
2019-08-21 09:55:42 +00:00
Address[] addresses = InternetAddress.parseHeader(header, false);
for (Address address : addresses) {
InternetAddress iaddress = (InternetAddress) address;
iaddress.setAddress(decodeMime(iaddress.getAddress()));
String personal = iaddress.getPersonal();
if (personal != null) {
try {
iaddress.setPersonal(decodeMime(personal));
} catch (UnsupportedEncodingException ex) {
Log.w(ex);
}
}
}
return addresses;
2019-06-22 11:26:45 +00:00
}
Address[] getFrom() throws MessagingException {
2019-06-22 11:26:45 +00:00
Address[] address = getAddressHeader("From");
if (address == null)
address = getAddressHeader("Sender");
return address;
2018-08-02 13:33:06 +00:00
}
Address[] getTo() throws MessagingException {
2019-06-22 11:26:45 +00:00
return getAddressHeader("To");
2018-08-02 13:33:06 +00:00
}
Address[] getCc() throws MessagingException {
2019-06-22 11:26:45 +00:00
return getAddressHeader("Cc");
2018-08-02 13:33:06 +00:00
}
Address[] getBcc() throws MessagingException {
2019-06-22 11:26:45 +00:00
return getAddressHeader("Bcc");
2018-08-03 07:39:43 +00:00
}
Address[] getReply() throws MessagingException {
2019-06-22 11:26:45 +00:00
return getAddressHeader("Reply-To");
2018-08-02 13:33:06 +00:00
}
2019-04-23 09:47:56 +00:00
Address[] getListPost() throws MessagingException {
2019-06-22 11:26:45 +00:00
String list;
2019-05-10 10:10:23 +00:00
try {
// https://www.ietf.org/rfc/rfc2369.txt
2019-06-09 11:21:56 +00:00
list = imessage.getHeader("List-Post", null);
2019-05-21 12:50:10 +00:00
if (list == null)
2019-05-10 10:10:23 +00:00
return null;
list = MimeUtility.unfold(list);
2019-09-08 10:57:21 +00:00
list = decodeMime(list);
2019-06-14 06:46:33 +00:00
// List-Post: NO (posting not allowed on this list)
if (list != null && list.startsWith("NO"))
2019-06-04 15:04:01 +00:00
return null;
2019-05-21 12:50:10 +00:00
// https://www.ietf.org/rfc/rfc2368.txt
2019-09-08 10:57:21 +00:00
for (String entry : list.split(",")) {
entry = entry.trim();
int lt = entry.indexOf("<");
int gt = entry.lastIndexOf(">");
2019-06-21 06:52:06 +00:00
if (lt >= 0 && gt > lt)
2019-06-09 13:46:36 +00:00
try {
2019-09-08 10:57:21 +00:00
MailTo mailto = MailTo.parse(entry.substring(lt + 1, gt));
2019-06-09 13:46:36 +00:00
if (mailto.getTo() != null)
2019-09-08 16:02:20 +00:00
return new Address[]{new InternetAddress(mailto.getTo().split(",")[0], null)};
} catch (Throwable ex) {
2019-06-09 13:46:36 +00:00
Log.i(ex);
}
}
2019-05-10 10:10:23 +00:00
2019-06-09 13:46:36 +00:00
Log.w(new IllegalArgumentException("List-Post: " + list));
2019-05-21 12:50:10 +00:00
return null;
2019-04-23 09:47:56 +00:00
} catch (AddressException ex) {
Log.w(ex);
return null;
2019-05-10 10:10:23 +00:00
}
2019-04-23 09:47:56 +00:00
}
2019-09-08 10:57:21 +00:00
String getListUnsubscribe() throws MessagingException {
String list;
try {
// https://www.ietf.org/rfc/rfc2369.txt
list = imessage.getHeader("List-Unsubscribe", null);
if (list == null)
return null;
list = MimeUtility.unfold(list);
list = decodeMime(list);
if (list != null && list.startsWith("NO"))
return null;
2019-09-23 10:15:25 +00:00
String link = null;
String mailto = null;
2019-09-08 10:57:21 +00:00
for (String entry : list.split(",")) {
entry = entry.trim();
int lt = entry.indexOf("<");
int gt = entry.lastIndexOf(">");
if (lt >= 0 && gt > lt) {
String unsubscribe = entry.substring(lt + 1, gt);
Uri uri = Uri.parse(unsubscribe);
String scheme = uri.getScheme();
2019-09-23 10:15:25 +00:00
if (mailto == null && "mailto".equals(scheme))
mailto = unsubscribe;
if (link == null && ("http".equals(scheme) || "https".equals(scheme)))
link = unsubscribe;
2019-09-08 10:57:21 +00:00
}
}
2019-09-23 10:15:25 +00:00
if (link != null)
return link;
if (mailto != null)
return mailto;
2019-09-08 10:57:21 +00:00
Log.w(new IllegalArgumentException("List-Unsubscribe: " + list));
return null;
} catch (AddressException ex) {
Log.w(ex);
return null;
}
}
2019-04-06 18:42:05 +00:00
String getSubject() throws MessagingException {
2019-05-03 07:26:11 +00:00
String subject = imessage.getHeader("Subject", null);
if (subject == null)
return null;
subject = MimeUtility.unfold(subject);
2019-06-22 11:26:45 +00:00
subject = new String(subject.getBytes(StandardCharsets.ISO_8859_1));
subject = decodeMime(subject);
return subject;
2019-01-01 13:58:51 +00:00
}
2019-04-05 09:08:18 +00:00
Long getSize() throws MessagingException {
long size = imessage.getSize();
2019-10-05 18:44:45 +00:00
//if (size == 0)
// throw new MessagingException("Message empty");
return (size < 0 ? null : size);
}
2019-02-10 19:47:46 +00:00
long getReceived() throws MessagingException {
2019-06-26 18:42:52 +00:00
Date received = imessage.getReceivedDate();
if (received == null)
received = imessage.getSentDate();
2019-06-26 18:42:52 +00:00
return (received == null ? new Date() : received).getTime();
2019-02-10 19:47:46 +00:00
}
Long getSent() throws MessagingException {
Date date = imessage.getSentDate();
return (date == null ? null : date.getTime());
}
2019-01-25 09:37:43 +00:00
String getHeaders() throws MessagingException {
StringBuilder sb = new StringBuilder();
Enumeration<Header> headers = imessage.getAllHeaders();
while (headers.hasMoreElements()) {
Header header = headers.nextElement();
sb.append(header.getName()).append(": ").append(header.getValue()).append("\n");
}
return sb.toString();
}
2019-01-18 14:25:26 +00:00
static String formatAddresses(Address[] addresses) {
return formatAddresses(addresses, true, false);
}
static String formatAddressesShort(Address[] addresses) {
return formatAddresses(addresses, false, false);
}
static String formatAddressesCompose(Address[] addresses) {
String result = formatAddresses(addresses, true, true);
if (!TextUtils.isEmpty(result))
result += ", ";
return result;
}
2019-01-20 10:18:42 +00:00
static String formatAddresses(Address[] addresses, boolean full, boolean compose) {
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<>();
2019-08-04 11:43:37 +00:00
for (int i = 0; i < addresses.length; i++) {
boolean duplicate = false;
for (int j = 0; j < i; j++)
if (addresses[i].equals(addresses[j])) {
duplicate = true;
break;
}
if (duplicate)
continue;
if (addresses[i] instanceof InternetAddress) {
InternetAddress address = (InternetAddress) addresses[i];
String personal = address.getPersonal();
if (TextUtils.isEmpty(personal))
2019-08-04 11:43:37 +00:00
formatted.add(address.getAddress());
2018-09-05 07:39:05 +00:00
else {
2019-01-18 14:25:26 +00:00
if (compose) {
boolean quote = false;
2019-11-15 15:32:38 +00:00
personal = personal.replace("\"", "");
2019-08-04 11:43:37 +00:00
for (int c = 0; c < personal.length(); c++)
if ("()<>,;:\\\"[]@".indexOf(personal.charAt(c)) >= 0) {
2019-01-18 14:25:26 +00:00
quote = true;
break;
}
if (quote)
personal = "\"" + personal + "\"";
}
2018-09-05 07:39:05 +00:00
if (full)
2019-08-04 11:43:37 +00:00
formatted.add(personal + " <" + address.getAddress() + ">");
2018-09-05 07:39:05 +00:00
else
formatted.add(personal);
}
} else
2019-08-04 11:43:37 +00:00
formatted.add(addresses[i].toString());
}
2018-08-08 06:55:47 +00:00
return TextUtils.join(", ", formatted);
2018-08-02 13:33:06 +00:00
}
2019-04-26 08:14:07 +00:00
static String decodeMime(String text) {
2019-04-06 18:42:05 +00:00
if (text == null)
return null;
2019-06-24 07:59:44 +00:00
// https://tools.ietf.org/html/rfc2047
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
2019-04-06 18:42:05 +00:00
int i = 0;
2019-07-18 14:42:20 +00:00
boolean first = true;
List<MimeTextPart> parts = new ArrayList<>();
2019-05-13 18:26:37 +00:00
while (i < text.length()) {
int s = text.indexOf("=?", i);
if (s < 0)
break;
2019-06-24 07:59:44 +00:00
int q1 = text.indexOf("?", s + 2);
if (q1 < 0)
break;
int q2 = text.indexOf("?", q1 + 1);
if (q2 < 0)
break;
int e = text.indexOf("?=", q2 + 1);
2019-05-13 18:26:37 +00:00
if (e < 0)
break;
2019-07-18 14:42:20 +00:00
String plain = text.substring(i, s);
if (!first)
plain = plain.replaceAll("[ \t\n\r]$", "");
if (!TextUtils.isEmpty(plain))
parts.add(new MimeTextPart(plain));
parts.add(new MimeTextPart(
text.substring(s + 2, q1),
text.substring(q1 + 1, q2),
text.substring(q2 + 1, e)));
i = e + 2;
first = false;
}
if (i < text.length())
parts.add(new MimeTextPart(text.substring(i)));
// Fold words to not break encoding
2019-08-13 15:11:16 +00:00
/*
2019-07-18 14:42:20 +00:00
int p = 0;
while (p + 1 < parts.size()) {
MimeTextPart p1 = parts.get(p);
MimeTextPart p2 = parts.get(p + 1);
if (p1.charset != null && p1.charset.equalsIgnoreCase(p2.charset) &&
p1.encoding != null && p1.encoding.equalsIgnoreCase(p2.encoding)) {
p1.text += p2.text;
parts.remove(p + 1);
} else
p++;
}
2019-08-13 15:11:16 +00:00
*/
2019-07-18 14:42:20 +00:00
StringBuilder sb = new StringBuilder();
for (MimeTextPart part : parts)
sb.append(part);
return sb.toString();
}
private static class MimeTextPart {
String charset;
String encoding;
String text;
MimeTextPart(String text) {
this.text = text;
}
MimeTextPart(String charset, String encoding, String text) {
this.charset = charset;
this.encoding = encoding;
this.text = text;
}
@Override
public String toString() {
if (charset == null)
return text;
String word = "=?" + charset + "?" + encoding + "?" + text + "?=";
2019-04-06 18:42:05 +00:00
try {
2019-07-18 14:42:20 +00:00
return decodeMime(MimeUtility.decodeWord(word));
} catch (Throwable ex) {
Log.w(new IllegalArgumentException(word, ex));
return word;
2019-04-06 18:42:05 +00:00
}
}
}
2018-12-27 11:32:20 +00:00
static String getSortKey(Address[] addresses) {
if (addresses == null || addresses.length == 0)
return null;
InternetAddress address = (InternetAddress) addresses[0];
2018-12-28 08:04:56 +00:00
// Sort on name will result in inconsistent results
// because the sender name and sender contact name can differ
return address.getAddress();
2018-12-27 11:32:20 +00:00
}
2019-01-16 10:57:57 +00:00
class MessageParts {
private Part plain = null;
private Part html = null;
private List<AttachmentPart> attachments = new ArrayList<>();
2019-01-19 19:08:01 +00:00
private ArrayList<String> warnings = new ArrayList<>();
2018-08-03 12:07:51 +00:00
2019-05-04 18:52:21 +00:00
Boolean isPlainOnly() {
if (plain == null && html == null)
return null;
return (html == null);
}
2019-09-30 19:00:28 +00:00
Long getBodySize() throws MessagingException {
Part part = (html == null ? plain : html);
if (part == null)
return null;
int size = part.getSize();
if (size < 0)
return null;
else
return (long) size;
}
String getHtml(Context context) throws MessagingException, IOException {
2019-01-17 08:09:33 +00:00
if (plain == null && html == null) {
2019-06-14 14:23:44 +00:00
Log.i("No body part");
2019-01-16 10:57:57 +00:00
return null;
2019-01-17 08:09:33 +00:00
}
2018-12-20 06:13:30 +00:00
2019-01-16 10:57:57 +00:00
String result;
Part part = (html == null ? plain : html);
2019-01-10 13:44:31 +00:00
2018-08-02 13:33:06 +00:00
try {
2018-12-20 06:13:30 +00:00
Object content = part.getContent();
2019-06-14 14:23:44 +00:00
Log.i("Content class=" + (content == null ? null : content.getClass().getName()));
if (content == null) {
warnings.add(context.getString(R.string.title_no_body));
return null;
}
2019-01-16 10:57:57 +00:00
if (content instanceof String)
result = (String) content;
else if (content instanceof InputStream)
// Typically com.sun.mail.util.QPDecoderStream
result = Helper.readStream((InputStream) content, StandardCharsets.UTF_8.name());
2019-01-16 10:57:57 +00:00
else
result = content.toString();
} catch (IOException | FolderClosedException | MessageRemovedException ex) {
2019-04-25 14:11:32 +00:00
throw ex;
2019-01-16 10:57:57 +00:00
} catch (Throwable ex) {
2018-12-24 12:27:45 +00:00
Log.w(ex);
2019-06-26 14:51:29 +00:00
warnings.add(Helper.formatThrowable(ex, false));
2019-05-10 20:14:24 +00:00
return null;
2018-08-02 13:33:06 +00:00
}
2019-05-10 20:14:24 +00:00
try {
ContentType ct = new ContentType(part.getContentType());
String charset = ct.getParameter("charset");
2019-07-08 09:01:40 +00:00
2019-08-05 13:41:15 +00:00
// Fix common mistakes
if (charset != null) {
charset = charset.replace("\"", "");
if ("ASCII".equals(charset.toUpperCase()))
2019-09-25 06:32:05 +00:00
charset = "US-ASCII";
2019-07-05 18:12:12 +00:00
}
2019-07-08 09:01:40 +00:00
if (TextUtils.isEmpty(charset) || "US-ASCII".equals(charset.toUpperCase())) {
// The first 127 characters are the same as in US-ASCII
result = new String(result.getBytes(StandardCharsets.ISO_8859_1));
2019-05-10 20:14:24 +00:00
} else {
2019-09-25 06:32:05 +00:00
// See UnknownCharsetProvider class
if ("US-ASCII".equals(Charset.forName(charset).name())) {
Log.w("Unsupported encoding charset=" + charset);
2019-07-08 09:01:40 +00:00
warnings.add(context.getString(R.string.title_no_charset, charset));
2019-09-25 06:32:05 +00:00
result = new String(result.getBytes(StandardCharsets.ISO_8859_1));
}
}
2019-05-10 20:14:24 +00:00
} catch (ParseException ex) {
Log.w(ex);
2019-06-26 14:51:29 +00:00
warnings.add(Helper.formatThrowable(ex, false));
2019-01-16 18:01:00 +00:00
}
2019-11-14 08:15:18 +00:00
if (part == plain)
2019-11-19 08:57:55 +00:00
result = "<div>" + HtmlHelper.formatPre(result) + "</div>";
2018-12-20 06:13:30 +00:00
2019-01-17 08:11:41 +00:00
return result;
2018-08-29 05:31:00 +00:00
}
2019-02-06 16:40:47 +00:00
List<AttachmentPart> getAttachmentParts() {
2019-02-06 16:16:06 +00:00
return attachments;
}
List<EntityAttachment> getAttachments() {
2019-01-16 10:57:57 +00:00
List<EntityAttachment> result = new ArrayList<>();
for (AttachmentPart apart : attachments)
result.add(apart.attachment);
2019-01-16 10:57:57 +00:00
return result;
2018-08-02 13:33:06 +00:00
}
2019-06-26 19:21:09 +00:00
void downloadAttachment(Context context, EntityAttachment local) throws IOException, MessagingException {
List<EntityAttachment> remotes = getAttachments();
// Some servers order attachments randomly
2019-09-06 11:21:07 +00:00
int index = -1;
2019-09-06 11:21:07 +00:00
boolean warning = false;
2019-09-06 11:21:07 +00:00
// Get attachment by position
if (local.sequence <= remotes.size()) {
EntityAttachment remote = remotes.get(local.sequence - 1);
2019-06-26 19:21:09 +00:00
if (Objects.equals(remote.name, local.name) &&
2019-09-06 11:21:07 +00:00
Objects.equals(remote.type, local.type) &&
Objects.equals(remote.disposition, local.disposition) &&
Objects.equals(remote.cid, local.cid) &&
Objects.equals(remote.size, local.size))
index = local.sequence - 1;
}
// Match attachment by name/cid
if (index < 0 && !(local.name == null && local.cid == null)) {
warning = true;
Log.w("Matching attachment by name/cid");
for (int i = 0; i < remotes.size(); i++) {
EntityAttachment remote = remotes.get(i);
if (Objects.equals(remote.name, local.name) &&
Objects.equals(remote.cid, local.cid)) {
index = i;
break;
}
2019-06-26 19:21:09 +00:00
}
}
// Match attachment by type/size
2019-09-06 11:21:07 +00:00
if (index < 0) {
warning = true;
Log.w("Matching attachment by type/size");
for (int i = 0; i < remotes.size(); i++) {
EntityAttachment remote = remotes.get(i);
2019-09-01 12:27:48 +00:00
if (Objects.equals(remote.type, local.type) &&
Objects.equals(remote.size, local.size)) {
index = i;
break;
}
}
2019-09-06 11:21:07 +00:00
}
2019-09-06 11:21:07 +00:00
if (index < 0 || warning) {
2019-08-21 15:25:23 +00:00
Map<String, String> crumb = new HashMap<>();
crumb.put("local", local.toString());
2019-06-26 19:21:09 +00:00
Log.w("Attachment not found local=" + local);
2019-08-21 15:25:23 +00:00
for (int i = 0; i < remotes.size(); i++) {
EntityAttachment remote = remotes.get(i);
crumb.put("remote:" + i, remote.toString());
2019-06-26 19:21:09 +00:00
Log.w("Attachment remote=" + remote);
2019-08-21 15:25:23 +00:00
}
Log.breadcrumb("attachments", crumb);
2019-06-26 19:21:09 +00:00
}
2019-09-06 11:21:07 +00:00
if (index < 0)
throw new IllegalArgumentException("Attachment not found");
downloadAttachment(context, index, local);
2019-06-26 19:21:09 +00:00
}
void downloadAttachment(Context context, int index, EntityAttachment local) throws MessagingException, IOException {
Log.i("downloading attachment id=" + local.id + " index=" + index + " " + local);
2018-08-02 13:33:06 +00:00
2019-03-14 07:18:42 +00:00
DB db = DB.getInstance(context);
2019-01-16 10:57:57 +00:00
// Get data
AttachmentPart apart = attachments.get(index);
2018-08-02 13:33:06 +00:00
2019-01-16 10:57:57 +00:00
// Download attachment
2019-06-26 19:21:09 +00:00
File file = EntityAttachment.getFile(context, local.id, local.name);
db.attachment().setProgress(local.id, null);
2019-02-22 15:59:23 +00:00
try (InputStream is = apart.part.getInputStream()) {
2019-01-16 10:57:57 +00:00
long size = 0;
2019-01-16 18:27:03 +00:00
long total = apart.part.getSize();
2019-06-07 10:19:13 +00:00
int lastprogress = 0;
2019-02-22 15:59:23 +00:00
try (OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[Helper.BUFFER_SIZE];
2019-02-22 15:59:23 +00:00
for (int len = is.read(buffer); len != -1; len = is.read(buffer)) {
size += len;
os.write(buffer, 0, len);
// Update progress
2019-06-07 10:19:13 +00:00
if (total > 0) {
int progress = (int) (size * 100 / total / 20 * 20);
if (progress != lastprogress) {
lastprogress = progress;
2019-06-26 19:21:09 +00:00
db.attachment().setProgress(local.id, progress);
2019-06-07 10:19:13 +00:00
}
}
2019-02-22 15:59:23 +00:00
}
2019-01-05 14:09:47 +00:00
}
2019-01-16 10:57:57 +00:00
// Store attachment data
2019-06-26 19:21:09 +00:00
db.attachment().setDownloaded(local.id, size);
2019-01-16 10:57:57 +00:00
Log.i("Downloaded attachment size=" + size);
2019-04-25 18:40:37 +00:00
} catch (FolderClosedIOException ex) {
2019-06-26 19:21:09 +00:00
db.attachment().setError(local.id, Helper.formatThrowable(ex));
2019-04-25 18:40:37 +00:00
throw new FolderClosedException(ex.getFolder(), "downloadAttachment", ex);
2019-06-02 05:58:43 +00:00
} catch (MessageRemovedIOException ex) {
2019-06-26 19:21:09 +00:00
db.attachment().setError(local.id, Helper.formatThrowable(ex));
2019-06-02 05:58:43 +00:00
throw new MessagingException("downloadAttachment", ex);
2019-01-16 18:27:03 +00:00
} catch (Throwable ex) {
2019-01-16 10:57:57 +00:00
// Reset progress on failure
2019-06-29 14:52:43 +00:00
Log.e(ex);
2019-06-26 19:21:09 +00:00
db.attachment().setError(local.id, Helper.formatThrowable(ex));
2019-03-14 08:05:28 +00:00
throw ex;
2019-01-16 10:57:57 +00:00
}
2018-08-03 12:07:51 +00:00
}
2019-01-16 18:01:00 +00:00
2019-01-19 19:08:01 +00:00
String getWarnings(String existing) {
if (existing != null)
warnings.add(0, existing);
2019-01-16 18:01:00 +00:00
if (warnings.size() == 0)
return null;
else
return TextUtils.join(", ", warnings);
}
2019-01-16 10:57:57 +00:00
}
2018-08-03 12:07:51 +00:00
2019-02-06 16:16:06 +00:00
class AttachmentPart {
2019-01-16 10:57:57 +00:00
String disposition;
String filename;
boolean pgp;
Part part;
EntityAttachment attachment;
2018-08-03 12:07:51 +00:00
}
2019-09-26 08:53:27 +00:00
MessageParts getMessageParts() throws IOException, MessagingException {
2019-01-16 10:57:57 +00:00
MessageParts parts = new MessageParts();
2019-09-26 08:53:27 +00:00
getMessageParts(imessage, parts, false);
2019-01-16 10:57:57 +00:00
return parts;
}
2018-09-06 10:24:30 +00:00
2019-09-26 08:53:27 +00:00
private void getMessageParts(Part part, MessageParts parts, boolean pgp) throws IOException, MessagingException {
2019-01-21 17:19:02 +00:00
try {
2019-09-02 08:48:11 +00:00
if (BuildConfig.DEBUG)
Log.i("Part class=" + part.getClass() + " type=" + part.getContentType());
2019-01-21 17:19:02 +00:00
if (part.isMimeType("multipart/*")) {
2019-06-01 16:59:56 +00:00
Multipart multipart;
2019-06-02 07:16:21 +00:00
Object content = part.getContent();
2019-06-01 16:59:56 +00:00
if (content instanceof Multipart)
multipart = (Multipart) part.getContent();
2019-06-02 07:16:21 +00:00
else if (content instanceof String) {
2019-06-01 16:59:56 +00:00
String text = (String) content;
String sample = text.substring(0, Math.min(80, text.length()));
throw new ParseException(content.getClass().getName() + ": " + sample);
2019-06-02 07:16:21 +00:00
} else
throw new ParseException(content.getClass().getName());
2019-06-01 16:59:56 +00:00
2019-01-21 17:19:02 +00:00
for (int i = 0; i < multipart.getCount(); i++)
try {
Part cpart = multipart.getBodyPart(i);
2019-09-06 09:21:12 +00:00
try {
ContentType ct = new ContentType(cpart.getContentType());
2019-09-23 20:07:22 +00:00
if ("application/pgp-encrypted".equals(ct.getBaseType().toLowerCase(Locale.ROOT))) {
2019-09-06 09:21:12 +00:00
pgp = true;
2019-09-07 16:16:44 +00:00
continue;
}
2019-09-06 09:21:12 +00:00
} catch (ParseException ex) {
Log.w(ex);
}
getMessageParts(cpart, parts, pgp);
2019-01-21 17:19:02 +00:00
} catch (ParseException ex) {
// Nested body: try to continue
// ParseException: In parameter list boundary="...">, expected parameter name, got ";"
Log.w(ex);
2019-06-26 14:51:29 +00:00
parts.warnings.add(Helper.formatThrowable(ex, false));
2019-01-21 17:19:02 +00:00
}
} else {
// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
String disposition;
2019-01-16 10:57:57 +00:00
try {
2019-01-21 17:19:02 +00:00
disposition = part.getDisposition();
2019-05-25 14:01:51 +00:00
if (disposition != null)
2019-09-23 20:07:22 +00:00
disposition = disposition.toLowerCase(Locale.ROOT);
} catch (MessagingException ex) {
2019-01-16 10:57:57 +00:00
Log.w(ex);
2019-06-26 14:51:29 +00:00
parts.warnings.add(Helper.formatThrowable(ex, false));
2019-01-21 17:19:02 +00:00
disposition = null;
2019-01-16 10:57:57 +00:00
}
2018-08-22 15:13:01 +00:00
2019-01-21 17:19:02 +00:00
String filename;
try {
filename = part.getFileName();
2019-08-21 09:57:02 +00:00
if (filename != null)
filename = decodeMime(filename);
2019-01-21 17:19:02 +00:00
} catch (MessagingException ex) {
Log.w(ex);
2019-06-26 14:51:29 +00:00
parts.warnings.add(Helper.formatThrowable(ex, false));
filename = null;
2019-01-21 17:19:02 +00:00
}
2018-08-22 15:13:01 +00:00
2019-11-20 13:22:21 +00:00
ContentType contentType;
try {
String c = part.getContentType();
contentType = new ContentType(c == null ? "" : c);
} catch (ParseException ex) {
Log.w(ex);
parts.warnings.add(Helper.formatThrowable(ex, false));
if (part instanceof MimeMessage)
contentType = new ContentType("text/html");
else
contentType = new ContentType(Helper.guessMimeType(filename));
2019-10-03 10:58:43 +00:00
}
2019-01-21 17:19:02 +00:00
if (!Part.ATTACHMENT.equalsIgnoreCase(disposition) &&
2019-07-07 09:58:56 +00:00
TextUtils.isEmpty(filename) &&
2019-10-03 10:58:43 +00:00
((parts.plain == null && "text/plain".equalsIgnoreCase(contentType.getBaseType())) ||
(parts.html == null && "text/html".equalsIgnoreCase(contentType.getBaseType())))) {
2019-10-03 11:32:52 +00:00
if ("text/html".equalsIgnoreCase(contentType.getBaseType()))
2019-10-03 10:58:43 +00:00
parts.html = part;
2019-10-03 11:32:52 +00:00
else
parts.plain = part;
2019-01-21 17:19:02 +00:00
} else {
AttachmentPart apart = new AttachmentPart();
apart.disposition = disposition;
apart.filename = filename;
apart.pgp = pgp;
apart.part = part;
2019-05-11 05:52:30 +00:00
String[] cid = null;
try {
cid = apart.part.getHeader("Content-ID");
} catch (MessagingException ex) {
Log.w(ex);
2019-09-19 14:55:36 +00:00
if (!"Failed to fetch headers".equals(ex.getMessage()))
parts.warnings.add(Helper.formatThrowable(ex, false));
2019-05-11 05:52:30 +00:00
}
apart.attachment = new EntityAttachment();
apart.attachment.name = apart.filename;
2019-11-20 13:22:21 +00:00
apart.attachment.type = contentType.getBaseType().toLowerCase(Locale.ROOT);
apart.attachment.disposition = apart.disposition;
apart.attachment.size = (long) apart.part.getSize();
apart.attachment.cid = (cid == null || cid.length == 0 ? null : MimeUtility.unfold(cid[0]));
apart.attachment.encryption = (apart.pgp ? EntityAttachment.PGP_MESSAGE : null);
2019-10-20 18:31:40 +00:00
if ("text/calendar".equalsIgnoreCase(apart.attachment.type) &&
TextUtils.isEmpty(apart.attachment.name))
apart.attachment.name = "invite.ics";
2019-05-25 14:01:51 +00:00
if (apart.attachment.size <= 0)
apart.attachment.size = null;
2019-05-24 07:19:22 +00:00
// https://tools.ietf.org/html/rfc2392
if (apart.attachment.cid != null) {
if (!apart.attachment.cid.startsWith("<"))
apart.attachment.cid = "<" + apart.attachment.cid;
if (!apart.attachment.cid.endsWith(">"))
apart.attachment.cid += ">";
}
2019-01-21 17:19:02 +00:00
parts.attachments.add(apart);
}
2019-01-05 14:09:47 +00:00
}
} catch (FolderClosedException ex) {
throw ex;
2019-01-21 17:19:02 +00:00
} catch (MessagingException ex) {
2019-09-26 08:53:27 +00:00
if (retryRaw(ex))
throw ex;
2019-01-21 17:19:02 +00:00
Log.w(ex);
2019-06-26 14:51:29 +00:00
parts.warnings.add(Helper.formatThrowable(ex, false));
2018-08-03 12:07:51 +00:00
}
2019-01-16 10:57:57 +00:00
}
2018-08-03 12:07:51 +00:00
2019-09-26 08:53:27 +00:00
static boolean retryRaw(MessagingException ex) {
return ("Failed to load IMAP envelope".equals(ex.getMessage()) ||
"Unable to load BODYSTRUCTURE".equals(ex.getMessage()));
}
2019-09-26 10:11:46 +00:00
static String sanitizeKeyword(String keyword) {
// https://tools.ietf.org/html/rfc3501
StringBuilder sb = new StringBuilder();
for (int i = 0; i < keyword.length(); i++) {
// flag-keyword = atom
// atom = 1*ATOM-CHAR
// ATOM-CHAR = <any CHAR except atom-specials>
char kar = keyword.charAt(i);
// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials / resp-specials
if (kar == '(' || kar == ')' || kar == '{' || kar == ' ' || Character.isISOControl(kar))
continue;
// list-wildcards = "%" / "*"
if (kar == '%' || kar == '*')
continue;
// quoted-specials = DQUOTE / "\"
if (kar == '"' || kar == '\\')
continue;
// resp-specials = "]"
if (kar == ']')
continue;
sb.append(kar);
}
return sb.toString();
}
static boolean equalEmail(Address a1, Address a2) {
String email1 = ((InternetAddress) a1).getAddress();
String email2 = ((InternetAddress) a2).getAddress();
if (email1 != null)
email1 = email1.toLowerCase();
if (email2 != null)
email2 = email2.toLowerCase();
return Objects.equals(email1, email2);
}
2018-12-25 08:22:07 +00:00
static boolean equal(Address[] a1, Address[] a2) {
if (a1 == null && a2 == null)
return true;
if (a1 == null || a2 == null)
return false;
if (a1.length != a2.length)
return false;
for (int i = 0; i < a1.length; i++)
if (!a1[i].toString().equals(a2[i].toString()))
return false;
return true;
2018-08-02 13:33:06 +00:00
}
}