warnings = new ArrayList<>();
Boolean isPlainOnly() {
if (plain == null && html == null)
return null;
return (html == null);
}
String getHtml(Context context) throws MessagingException, IOException {
if (plain == null && html == null) {
Log.i("No body part");
return null;
}
String result;
Part part = (html == null ? plain : html);
try {
Object content = part.getContent();
Log.i("Content class=" + (content == null ? null : content.getClass().getName()));
if (content == null) {
warnings.add(context.getString(R.string.title_no_body));
return null;
}
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());
else
result = content.toString();
} catch (IOException | FolderClosedException | MessageRemovedException ex) {
throw ex;
} catch (Throwable ex) {
Log.w(ex);
warnings.add(Helper.formatThrowable(ex, false));
return null;
}
try {
ContentType ct = new ContentType(part.getContentType());
String charset = ct.getParameter("charset");
// Fix common mistakes
if (charset != null) {
charset = charset.replace("\"", "");
if ("ASCII".equals(charset.toUpperCase()))
charset = "us-ascii";
}
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));
} else {
if ("US-ASCII".equals(Charset.forName(charset).name()))
warnings.add(context.getString(R.string.title_no_charset, charset));
}
} catch (ParseException ex) {
Log.w(ex);
warnings.add(Helper.formatThrowable(ex, false));
}
// Prevent Jsoup throwing an exception
result = result.replace("\0", "");
if (part.isMimeType("text/plain"))
result = "" + TextUtils.htmlEncode(result) + "
";
return result;
}
List getAttachmentParts() {
return attachments;
}
List getAttachments() {
List result = new ArrayList<>();
for (AttachmentPart apart : attachments)
result.add(apart.attachment);
return result;
}
void downloadAttachment(Context context, EntityAttachment local) throws IOException, MessagingException {
List remotes = getAttachments();
// Some servers order attachments randomly
int index = -1;
boolean warning = false;
// Get attachment by position
if (local.sequence <= remotes.size()) {
EntityAttachment remote = remotes.get(local.sequence - 1);
if (Objects.equals(remote.name, local.name) &&
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;
}
}
}
// Match attachment by type/size
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);
if (Objects.equals(remote.type, local.type) &&
Objects.equals(remote.size, local.size)) {
index = i;
break;
}
}
}
if (index < 0 || warning) {
Map crumb = new HashMap<>();
crumb.put("local", local.toString());
Log.w("Attachment not found local=" + local);
for (int i = 0; i < remotes.size(); i++) {
EntityAttachment remote = remotes.get(i);
crumb.put("remote:" + i, remote.toString());
Log.w("Attachment remote=" + remote);
}
Log.breadcrumb("attachments", crumb);
}
if (index < 0)
throw new IllegalArgumentException("Attachment not found");
downloadAttachment(context, index, local);
}
void downloadAttachment(Context context, int index, EntityAttachment local) throws MessagingException, IOException {
Log.i("downloading attachment id=" + local.id + " index=" + index + " " + local);
DB db = DB.getInstance(context);
// Get data
AttachmentPart apart = attachments.get(index);
// Download attachment
File file = EntityAttachment.getFile(context, local.id, local.name);
db.attachment().setProgress(local.id, null);
try (InputStream is = apart.part.getInputStream()) {
long size = 0;
long total = apart.part.getSize();
int lastprogress = 0;
try (OutputStream os = new FileOutputStream(file)) {
byte[] buffer = new byte[Helper.BUFFER_SIZE];
for (int len = is.read(buffer); len != -1; len = is.read(buffer)) {
size += len;
os.write(buffer, 0, len);
// Update progress
if (total > 0) {
int progress = (int) (size * 100 / total / 20 * 20);
if (progress != lastprogress) {
lastprogress = progress;
db.attachment().setProgress(local.id, progress);
}
}
}
}
// Store attachment data
db.attachment().setDownloaded(local.id, size);
Log.i("Downloaded attachment size=" + size);
} catch (FolderClosedIOException ex) {
db.attachment().setError(local.id, Helper.formatThrowable(ex));
throw new FolderClosedException(ex.getFolder(), "downloadAttachment", ex);
} catch (MessageRemovedIOException ex) {
db.attachment().setError(local.id, Helper.formatThrowable(ex));
throw new MessagingException("downloadAttachment", ex);
} catch (Throwable ex) {
// Reset progress on failure
Log.e(ex);
db.attachment().setError(local.id, Helper.formatThrowable(ex));
throw ex;
}
}
String getWarnings(String existing) {
if (existing != null)
warnings.add(0, existing);
if (warnings.size() == 0)
return null;
else
return TextUtils.join(", ", warnings);
}
}
class AttachmentPart {
String disposition;
String filename;
boolean pgp;
Part part;
EntityAttachment attachment;
}
MessageParts getMessageParts() throws IOException, FolderClosedException {
MessageParts parts = new MessageParts();
MimeMessage cmessage = imessage;
try {
// Load body structure
cmessage.getContentID();
} catch (MessagingException ex) {
// https://javaee.github.io/javamail/FAQ#imapserverbug
if ("Unable to load BODYSTRUCTURE".equals(ex.getMessage())) {
Log.w(ex);
parts.warnings.add(Helper.formatThrowable(ex, false));
try {
cmessage = new MimeMessage(imessage);
} catch (MessagingException ignored) {
}
}
}
getMessageParts(cmessage, parts, false);
return parts;
}
private void getMessageParts(Part part, MessageParts parts, boolean pgp) throws IOException, FolderClosedException {
try {
if (BuildConfig.DEBUG)
Log.i("Part class=" + part.getClass() + " type=" + part.getContentType());
if (part.isMimeType("multipart/*")) {
Multipart multipart;
Object content = part.getContent();
if (content instanceof Multipart)
multipart = (Multipart) part.getContent();
else if (content instanceof String) {
String text = (String) content;
String sample = text.substring(0, Math.min(80, text.length()));
throw new ParseException(content.getClass().getName() + ": " + sample);
} else
throw new ParseException(content.getClass().getName());
for (int i = 0; i < multipart.getCount(); i++)
try {
Part cpart = multipart.getBodyPart(i);
try {
ContentType ct = new ContentType(cpart.getContentType());
if ("application/pgp-encrypted".equals(ct.getBaseType().toLowerCase())) {
pgp = true;
continue;
}
} catch (ParseException ex) {
Log.w(ex);
}
getMessageParts(cpart, parts, pgp);
} catch (ParseException ex) {
// Nested body: try to continue
// ParseException: In parameter list boundary="...">, expected parameter name, got ";"
Log.w(ex);
parts.warnings.add(Helper.formatThrowable(ex, false));
}
} else {
// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
String disposition;
try {
disposition = part.getDisposition();
if (disposition != null)
disposition = disposition.toLowerCase();
} catch (MessagingException ex) {
Log.w(ex);
parts.warnings.add(Helper.formatThrowable(ex, false));
disposition = null;
}
String filename;
try {
filename = part.getFileName();
if (filename != null)
filename = decodeMime(filename);
} catch (MessagingException ex) {
Log.w(ex);
parts.warnings.add(Helper.formatThrowable(ex, false));
filename = null;
}
if (!Part.ATTACHMENT.equalsIgnoreCase(disposition) &&
TextUtils.isEmpty(filename) &&
((parts.plain == null && part.isMimeType("text/plain")) ||
(parts.html == null && part.isMimeType("text/html")))) {
if (part.isMimeType("text/plain")) {
if (parts.plain == null)
parts.plain = part;
} else {
if (parts.html == null)
parts.html = part;
}
} else {
AttachmentPart apart = new AttachmentPart();
apart.disposition = disposition;
apart.filename = filename;
apart.pgp = pgp;
apart.part = part;
ContentType ct;
try {
ct = new ContentType(apart.part.getContentType());
} catch (ParseException ex) {
Log.w(ex);
parts.warnings.add(Helper.formatThrowable(ex, false));
ct = new ContentType("application/octet-stream");
}
String[] cid = null;
try {
cid = apart.part.getHeader("Content-ID");
} catch (MessagingException ex) {
Log.w(ex);
if (!"Failed to fetch headers".equals(ex.getMessage()))
parts.warnings.add(Helper.formatThrowable(ex, false));
}
apart.attachment = new EntityAttachment();
apart.attachment.name = apart.filename;
apart.attachment.type = ct.getBaseType().toLowerCase();
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);
if ("text/calendar".equalsIgnoreCase(apart.attachment.type) && TextUtils.isEmpty(apart.attachment.name))
apart.attachment.name = "invite.ics";
// Try to guess a better content type
// For example, sometimes PDF files are sent as application/octet-stream
if (!apart.pgp) {
String extension = Helper.getExtension(apart.attachment.name);
if (extension != null &&
("pdf".equals(extension.toLowerCase()) ||
"application/octet-stream".equals(apart.attachment.type))) {
String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase());
if (type != null) {
if (!type.equals(apart.attachment.type))
Log.w("Guessing file=" + apart.attachment.name + " type=" + type);
apart.attachment.type = type;
}
}
}
if (apart.attachment.size <= 0)
apart.attachment.size = null;
// 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 += ">";
}
parts.attachments.add(apart);
}
}
} catch (FolderClosedException ex) {
throw ex;
} catch (MessagingException ex) {
Log.w(ex);
parts.warnings.add(Helper.formatThrowable(ex, false));
}
}
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;
}
}