parts = new ArrayList<>();
parts.addAll(text);
parts.addAll(extra);
for (PartHolder h : parts) {
if (h.part.getSize() > MAX_MESSAGE_SIZE) {
warnings.add(context.getString(R.string.title_insufficient_memory));
return null;
}
String result;
try {
Object content = h.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);
else
result = content.toString();
} catch (IOException | FolderClosedException | MessageRemovedException ex) {
throw ex;
} catch (Throwable ex) {
Log.w(ex);
warnings.add(Log.formatThrowable(ex, false));
return null;
}
// Check character set
String charset = h.contentType.getParameter("charset");
if (UnknownCharsetProvider.charsetForMime(charset) == null)
warnings.add(context.getString(R.string.title_no_charset, charset));
if ((TextUtils.isEmpty(charset) || charset.equalsIgnoreCase(StandardCharsets.US_ASCII.name())))
charset = null;
Charset cs = null;
try {
if (charset != null)
cs = Charset.forName(charset);
} catch (UnsupportedCharsetException ignored) {
}
if (h.isPlainText()) {
if (charset == null || StandardCharsets.ISO_8859_1.equals(cs))
if (StandardCharsets.ISO_8859_1.equals(cs) && CharsetHelper.isUTF8(result)) {
Log.i("Charset upgrade=UTF8");
result = new String(result.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
} else {
Charset detected = CharsetHelper.detect(result);
if (detected == null) {
if (CharsetHelper.isUTF8(result)) {
Log.i("Charset plain=UTF8");
result = new String(result.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}
} else {
Log.i("Charset plain=" + detected.name());
result = new String(result.getBytes(StandardCharsets.ISO_8859_1), detected);
}
}
if ("flowed".equalsIgnoreCase(h.contentType.getParameter("format")))
result = HtmlHelper.flow(result);
result = "" + HtmlHelper.formatPre(result) + "
";
} else if (h.isHtml()) {
// Conditionally upgrade to UTF8
if ((cs == null ||
StandardCharsets.US_ASCII.equals(cs) ||
StandardCharsets.ISO_8859_1.equals(cs)) &&
CharsetHelper.isUTF8(result))
result = new String(result.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
// Fix incorrect UTF16
try {
if (CHARSET16.contains(cs)) {
Charset detected = CharsetHelper.detect(result);
if (!CHARSET16.contains(detected))
Log.e(new Throwable("Charset=" + cs + " detected=" + detected));
if (StandardCharsets.US_ASCII.equals(detected) ||
StandardCharsets.UTF_8.equals(detected)) {
charset = null;
result = new String(result.getBytes(cs), detected);
}
}
} catch (Throwable ex) {
Log.w(ex);
}
if (charset == null) {
//
//
String excerpt = result.substring(0, Math.min(MAX_META_EXCERPT, result.length()));
Document d = JsoupEx.parse(excerpt);
for (Element meta : d.select("meta")) {
if ("Content-Type".equalsIgnoreCase(meta.attr("http-equiv"))) {
try {
ContentType ct = new ContentType(meta.attr("content"));
charset = ct.getParameter("charset");
} catch (ParseException ex) {
Log.w(ex);
}
} else
charset = meta.attr("charset");
if (!TextUtils.isEmpty(charset))
try {
Log.i("Charset meta=" + meta);
Charset c = Charset.forName(charset);
// US-ASCII is a subset of ISO8859-1
if (StandardCharsets.US_ASCII.equals(c))
break;
// Check if really UTF-8
if (StandardCharsets.UTF_8.equals(c) && !CharsetHelper.isUTF8(result)) {
Log.e("Charset meta=" + meta + " !isUTF8");
break;
}
// 16 bits charsets cannot be converted to 8 bits
if (CHARSET16.contains(c)) {
Log.e("Charset meta=" + meta);
break;
}
Charset detected = CharsetHelper.detect(result);
if (c.equals(detected))
break;
if (StandardCharsets.US_ASCII.equals(detected) &&
("windows-1252".equals(c.name()) ||
StandardCharsets.UTF_8.equals(c) ||
StandardCharsets.ISO_8859_1.equals(c)))
break;
if (BuildConfig.PLAY_STORE_RELEASE)
Log.w("Converting detected=" + detected + " meta=" + c);
else
Log.e("Converting detected=" + detected + " meta=" + c);
// Convert
result = new String(result.getBytes(StandardCharsets.ISO_8859_1), c);
break;
} catch (Throwable ex) {
Log.e(ex);
}
}
}
} else if (h.isDSN()) {
StringBuilder report = new StringBuilder();
report.append("
");
for (String line : result.split("\\r?\\n"))
if (line.length() == 0)
report.append("
");
else if (Character.isWhitespace(line.charAt(0)))
report.append(line).append("
");
else {
int colon = line.indexOf(':');
if (colon < 0)
report.append(line);
else {
String name = line.substring(0, colon).trim();
String value = line.substring(colon + 1).trim();
value = decodeMime(value);
report
.append("")
.append(TextUtils.htmlEncode(name))
.append("")
.append(": ")
.append(TextUtils.htmlEncode(value))
.append("
");
}
}
report.append("
");
result = report.toString();
} else
Log.w("Unexpected content type=" + h.contentType);
sb.append(result);
}
return sb.toString();
}
List getAttachmentParts() {
return attachments;
}
List getAttachments() {
List result = new ArrayList<>();
for (AttachmentPart apart : attachments)
result.add(apart.attachment);
return result;
}
Integer getEncryption() {
for (AttachmentPart apart : attachments)
if (EntityAttachment.PGP_SIGNATURE.equals(apart.attachment.encryption))
return EntityMessage.PGP_SIGNONLY;
else if (EntityAttachment.PGP_MESSAGE.equals(apart.attachment.encryption))
return EntityMessage.PGP_SIGNENCRYPT;
else if (EntityAttachment.SMIME_SIGNATURE.equals(apart.attachment.encryption) ||
EntityAttachment.SMIME_SIGNED_DATA.equals(apart.attachment.encryption))
return EntityMessage.SMIME_SIGNONLY;
else if (EntityAttachment.SMIME_MESSAGE.equals(apart.attachment.encryption))
return EntityMessage.SMIME_SIGNENCRYPT;
return null;
}
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);
if (Helper.isTnef(local.type, local.name))
decodeTNEF(context, 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, 0);
if (EntityAttachment.PGP_CONTENT.equals(apart.encrypt) ||
EntityAttachment.SMIME_CONTENT.equals(apart.encrypt)) {
ContentType ct = new ContentType(apart.part.getContentType());
String boundary = ct.getParameter("boundary");
if (TextUtils.isEmpty(boundary))
throw new ParseException("Signed boundary missing");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
apart.part.writeTo(bos);
String raw = new String(bos.toByteArray());
String[] parts = raw.split("\\r?\\n" + Pattern.quote("--" + boundary) + "\\r?\\n");
if (parts.length < 2)
throw new ParseException("Signed part missing");
String c = parts[1]
.replaceAll(" +$", "") // trim trailing spaces
.replaceAll("\\r?\\n", "\r\n"); // normalize new lines
try (OutputStream os = new FileOutputStream(file)) {
os.write(c.getBytes());
}
db.attachment().setDownloaded(local.id, file.length());
} else {
try (InputStream is = apart.part.getInputStream()) {
long size = 0;
long total = apart.part.getSize();
long lastprogress = System.currentTimeMillis();
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) {
long now = System.currentTimeMillis();
if (now - lastprogress > ATTACHMENT_PROGRESS_UPDATE) {
lastprogress = now;
db.attachment().setProgress(local.id, (int) (size * 100 / total));
}
}
}
}
// Store attachment data
db.attachment().setDownloaded(local.id, size);
Log.i("Downloaded attachment size=" + size);
} catch (FolderClosedIOException ex) {
db.attachment().setError(local.id, Log.formatThrowable(ex));
throw new FolderClosedException(ex.getFolder(), "downloadAttachment", ex);
} catch (MessageRemovedIOException ex) {
db.attachment().setError(local.id, Log.formatThrowable(ex));
throw new MessagingException("downloadAttachment", ex);
} catch (Throwable ex) {
// Reset progress on failure
if (ex instanceof IOException)
Log.i(ex);
else
Log.e(ex);
db.attachment().setError(local.id, Log.formatThrowable(ex));
throw ex;
}
if ("message/rfc822".equals(local.type))
try (FileInputStream fis = new FileInputStream(local.getFile(context))) {
Properties props = MessageHelper.getSessionProperties();
Session isession = Session.getInstance(props, null);
MimeMessage imessage = new MimeMessage(isession, fis);
MessageHelper helper = new MessageHelper(imessage, context);
MessageHelper.MessageParts parts = helper.getMessageParts();
int subsequence = 1;
for (AttachmentPart epart : parts.getAttachmentParts())
try {
Log.i("Embedded attachment seq=" + local.sequence + ":" + subsequence);
epart.attachment.message = local.message;
epart.attachment.sequence = local.sequence;
epart.attachment.subsequence = subsequence++;
epart.attachment.id = db.attachment().insertAttachment(epart.attachment);
File efile = epart.attachment.getFile(context);
Log.i("Writing to " + efile);
try (InputStream is = epart.part.getInputStream()) {
try (OutputStream os = new FileOutputStream(efile)) {
byte[] buffer = new byte[Helper.BUFFER_SIZE];
for (int len = is.read(buffer); len != -1; len = is.read(buffer))
os.write(buffer, 0, len);
}
}
db.attachment().setDownloaded(epart.attachment.id, efile.length());
} catch (Throwable ex) {
db.attachment().setError(epart.attachment.id, Log.formatThrowable(ex));
db.attachment().setAvailable(epart.attachment.id, true); // unrecoverable
}
} catch (Throwable ex) {
Log.e(ex);
}
}
}
private void decodeTNEF(Context context, EntityAttachment local) {
try {
DB db = DB.getInstance(context);
int subsequence = 0;
// https://poi.apache.org/components/hmef/index.html
File file = local.getFile(context);
org.apache.poi.hmef.HMEFMessage msg = new org.apache.poi.hmef.HMEFMessage(new FileInputStream(file));
String subject = msg.getSubject();
if (!TextUtils.isEmpty(subject)) {
EntityAttachment attachment = new EntityAttachment();
attachment.message = local.message;
attachment.sequence = local.sequence;
attachment.subsequence = ++subsequence;
attachment.name = "subject.txt";
attachment.type = "text/plain";
attachment.disposition = Part.ATTACHMENT;
attachment.id = db.attachment().insertAttachment(attachment);
Helper.writeText(attachment.getFile(context), subject);
db.attachment().setDownloaded(attachment.id, (long) subject.length());
}
String body = msg.getBody();
if (TextUtils.isEmpty(body)) {
org.apache.poi.hmef.attribute.MAPIAttribute attr =
msg.getMessageMAPIAttribute(org.apache.poi.hsmf.datatypes.MAPIProperty.BODY_HTML);
if (attr == null)
attr = msg.getMessageMAPIAttribute(org.apache.poi.hsmf.datatypes.MAPIProperty.BODY);
if (attr != null) {
EntityAttachment attachment = new EntityAttachment();
attachment.message = local.message;
attachment.sequence = local.sequence;
attachment.subsequence = ++subsequence;
if (attr.getProperty().equals(org.apache.poi.hsmf.datatypes.MAPIProperty.BODY_HTML)) {
attachment.name = "body.html";
attachment.type = "text/html";
} else {
attachment.name = "body.txt";
attachment.type = "text/plain";
}
attachment.disposition = Part.ATTACHMENT;
attachment.id = db.attachment().insertAttachment(attachment);
byte[] data = attr.getData();
Helper.writeText(attachment.getFile(context), new String(data));
db.attachment().setDownloaded(attachment.id, (long) data.length);
}
} else {
EntityAttachment attachment = new EntityAttachment();
attachment.message = local.message;
attachment.sequence = local.sequence;
attachment.subsequence = ++subsequence;
attachment.name = "body.rtf";
attachment.type = "application/rtf";
attachment.disposition = Part.ATTACHMENT;
attachment.id = db.attachment().insertAttachment(attachment);
Helper.writeText(attachment.getFile(context), body);
db.attachment().setDownloaded(attachment.id, (long) body.length());
}
for (org.apache.poi.hmef.Attachment at : msg.getAttachments())
try {
String filename = at.getLongFilename();
if (filename == null)
filename = at.getFilename();
if (filename == null) {
String ext = at.getExtension();
if (ext != null)
filename = "document." + ext;
}
EntityAttachment attachment = new EntityAttachment();
attachment.message = local.message;
attachment.sequence = local.sequence;
attachment.subsequence = ++subsequence;
attachment.name = filename;
attachment.type = Helper.guessMimeType(attachment.name);
attachment.disposition = Part.ATTACHMENT;
attachment.id = db.attachment().insertAttachment(attachment);
byte[] data = at.getContents();
try (OutputStream os = new FileOutputStream(attachment.getFile(context))) {
os.write(data);
}
db.attachment().setDownloaded(attachment.id, (long) data.length);
} catch (Throwable ex) {
// java.lang.IllegalArgumentException: Attachment corrupt - no Data section
Log.e(ex);
}
StringBuilder sb = new StringBuilder();
for (org.apache.poi.hmef.attribute.TNEFAttribute attr : msg.getMessageAttributes())
sb.append(attr.toString()).append("\r\n");
for (org.apache.poi.hmef.attribute.MAPIAttribute attr : msg.getMessageMAPIAttributes())
if (!org.apache.poi.hsmf.datatypes.MAPIProperty.RTF_COMPRESSED.equals(attr.getProperty()) &&
!org.apache.poi.hsmf.datatypes.MAPIProperty.BODY_HTML.equals(attr.getProperty()))
sb.append(attr.toString()).append("\r\n");
if (sb.length() > 0) {
EntityAttachment attachment = new EntityAttachment();
attachment.message = local.message;
attachment.sequence = local.sequence;
attachment.subsequence = ++subsequence;
attachment.name = "attributes.txt";
attachment.type = "text/plain";
attachment.disposition = Part.ATTACHMENT;
attachment.id = db.attachment().insertAttachment(attachment);
Helper.writeText(attachment.getFile(context), sb.toString());
db.attachment().setDownloaded(attachment.id, (long) sb.length());
}
} catch (Throwable ex) {
Log.w(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;
Integer encrypt;
Part part;
EntityAttachment attachment;
}
MessageParts getMessageParts() throws IOException, MessagingException {
MessageParts parts = new MessageParts();
try {
ensureStructure();
try {
MimePart part = imessage;
if (part.isMimeType("multipart/mixed")) {
Object content = part.getContent();
if (content instanceof Multipart) {
Multipart mp = (Multipart) content;
for (int i = 0; i < mp.getCount(); i++) {
BodyPart bp = mp.getBodyPart(i);
if (bp.isMimeType("multipart/signed") || bp.isMimeType("multipart/encrypted")) {
part = (MimePart) bp;
break;
}
}
} else {
String msg = "Multipart=" + (content == null ? null : content.getClass().getName());
Log.e(msg);
throw new MessagingException(msg);
}
}
if (part.isMimeType("multipart/signed")) {
ContentType ct = new ContentType(part.getContentType());
String protocol = ct.getParameter("protocol");
if ("application/pgp-signature".equals(protocol) ||
"application/pkcs7-signature".equals(protocol) ||
"application/x-pkcs7-signature".equals(protocol)) {
Object content = part.getContent();
if (content instanceof Multipart) {
Multipart multipart = (Multipart) content;
if (multipart.getCount() == 2) {
getMessageParts(multipart.getBodyPart(0), parts, null);
getMessageParts(multipart.getBodyPart(1), parts,
"application/pgp-signature".equals(protocol)
? EntityAttachment.PGP_SIGNATURE
: EntityAttachment.SMIME_SIGNATURE);
AttachmentPart apart = new AttachmentPart();
apart.disposition = Part.INLINE;
apart.filename = "content.asc";
apart.encrypt = "application/pgp-signature".equals(protocol)
? EntityAttachment.PGP_CONTENT
: EntityAttachment.SMIME_CONTENT;
apart.part = part;
apart.attachment = new EntityAttachment();
apart.attachment.disposition = apart.disposition;
apart.attachment.name = apart.filename;
apart.attachment.type = "text/plain";
apart.attachment.size = getSize();
apart.attachment.encryption = apart.encrypt;
parts.attachments.add(apart);
return parts;
} else {
StringBuilder sb = new StringBuilder();
sb.append(ct);
for (int i = 0; i < multipart.getCount(); i++)
sb.append(' ').append(i).append('=').append(multipart.getBodyPart(i).getContentType());
Log.e(sb.toString());
}
} else {
String msg = "Multipart=" + (content == null ? null : content.getClass().getName());
Log.e(msg);
throw new MessagingException(msg);
}
} else
Log.e(ct.toString());
} else if (part.isMimeType("multipart/encrypted")) {
ContentType ct = new ContentType(part.getContentType());
String protocol = ct.getParameter("protocol");
if ("application/pgp-encrypted".equals(protocol) || protocol == null) {
Object content = part.getContent();
if (content instanceof Multipart) {
Multipart multipart = (Multipart) content;
if (multipart.getCount() == 2) {
// Ignore header
getMessageParts(multipart.getBodyPart(1), parts, EntityAttachment.PGP_MESSAGE);
return parts;
} else {
StringBuilder sb = new StringBuilder();
sb.append(ct);
for (int i = 0; i < multipart.getCount(); i++)
sb.append(' ').append(i).append('=').append(multipart.getBodyPart(i).getContentType());
Log.e(sb.toString());
}
} else {
String msg = "Multipart=" + (content == null ? null : content.getClass().getName());
Log.e(msg);
throw new MessagingException(msg);
}
} else
Log.e(ct.toString());
} else if (part.isMimeType("application/pkcs7-mime") ||
part.isMimeType("application/x-pkcs7-mime")) {
ContentType ct = new ContentType(part.getContentType());
String smimeType = ct.getParameter("smime-type");
if ("enveloped-data".equalsIgnoreCase(smimeType)) {
getMessageParts(part, parts, EntityAttachment.SMIME_MESSAGE);
return parts;
} else if ("signed-data".equalsIgnoreCase(smimeType)) {
getMessageParts(part, parts, EntityAttachment.SMIME_SIGNED_DATA);
return parts;
} else {
if (TextUtils.isEmpty(smimeType)) {
String name = ct.getParameter("name");
if ("smime.p7m".equalsIgnoreCase(name)) {
getMessageParts(part, parts, EntityAttachment.SMIME_MESSAGE);
return parts;
} else if ("smime.p7s".equalsIgnoreCase(name)) {
getMessageParts(part, parts, EntityAttachment.SMIME_SIGNED_DATA);
return parts;
}
}
Log.e(ct.toString());
}
}
} catch (ParseException ex) {
Log.w(ex);
}
getMessageParts(imessage, parts, null);
} catch (OutOfMemoryError ex) {
Log.e(ex);
parts.warnings.add(Log.formatThrowable(ex, false));
/*
java.lang.OutOfMemoryError: Failed to allocate a xxx byte allocation with yyy free bytes and zzMB until OOM
at java.io.ByteArrayOutputStream.expand(ByteArrayOutputStream.java:91)
at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:201)
at com.sun.mail.util.ASCIIUtility.getBytes(ASCIIUtility:279)
at javax.mail.internet.MimeMessage.parse(MimeMessage:336)
at javax.mail.internet.MimeMessage.(MimeMessage:199)
at eu.faircode.email.MimeMessageEx.(MimeMessageEx:44)
at eu.faircode.email.MessageHelper._ensureMessage(MessageHelper:2732)
at eu.faircode.email.MessageHelper.ensureStructure(MessageHelper:2685)
at eu.faircode.email.MessageHelper.getMessageParts(MessageHelper:2368)
*/
}
return parts;
}
private void getMessageParts(Part part, MessageParts parts, Integer encrypt) throws IOException, MessagingException {
try {
Log.d("Part class=" + part.getClass() + " type=" + part.getContentType());
// https://github.com/autocrypt/protected-headers
try {
ContentType ct = new ContentType(part.getContentType());
if ("v1".equals(ct.getParameter("protected-headers"))) {
String[] subject = part.getHeader("subject");
if (subject != null && subject.length != 0) {
subject[0] = subject[0].replaceAll("\\?=[\\r\\n\\t ]+=\\?", "\\?==\\?");
parts.protected_subject = decodeMime(subject[0]);
}
}
} catch (Throwable ex) {
Log.e(ex);
}
if (part.isMimeType("multipart/*")) {
Multipart multipart;
Object content = part.getContent(); // Should always be Multipart
if (content instanceof Multipart)
multipart = (Multipart) part.getContent();
else {
String msg = "Multipart=" + (content == null ? null : content.getClass().getName());
Log.e(msg);
throw new MessagingException(msg);
}
boolean other = false;
List plain = new ArrayList<>();
int count = multipart.getCount();
boolean alternative = part.isMimeType("multipart/alternative");
for (int i = 0; i < count; i++)
try {
BodyPart child = multipart.getBodyPart(i);
if (alternative && count > 1 && child.isMimeType("text/plain"))
plain.add(child);
else {
getMessageParts(child, parts, encrypt);
other = true;
}
} catch (ParseException ex) {
// Nested body: try to continue
// ParseException: In parameter list boundary="...">, expected parameter name, got ";"
Log.w(ex);
parts.warnings.add(Log.formatThrowable(ex, false));
}
if (alternative && count > 1 && !other)
for (Part child : plain)
try {
getMessageParts(child, parts, encrypt);
} catch (ParseException ex) {
// Nested body: try to continue
// ParseException: In parameter list boundary="...">, expected parameter name, got ";"
Log.w(ex);
parts.warnings.add(Log.formatThrowable(ex, false));
}
} else {
// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
String disposition;
try {
// From the body structure
disposition = part.getDisposition();
if (disposition != null)
disposition = disposition.toLowerCase(Locale.ROOT);
} catch (MessagingException ex) {
Log.w(ex);
parts.warnings.add(Log.formatThrowable(ex, false));
disposition = null;
}
String filename;
try {
// From the body structure:
// 1. disposition filename
// 2. content type name
filename = part.getFileName(); // IMAPBodyPart/BODYSTRUCTURE
if (filename != null) {
// https://tools.ietf.org/html/rfc2231
// http://kb.mozillazine.org/Attachments_renamed
// https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
int q1 = filename.indexOf('\'');
int q2 = filename.indexOf('\'', q1 + 1);
if (q1 >= 0 && q2 > 0) {
try {
String charset = filename.substring(0, q1);
String language = filename.substring(q1 + 1, q2);
String name = filename.substring(q2 + 1)
.replace("+", "%2B");
if (!TextUtils.isEmpty(charset))
filename = URLDecoder.decode(name, charset);
} catch (Throwable ex) {
Log.e(ex);
}
}
filename = decodeMime(filename);
}
} catch (MessagingException ex) {
Log.w(ex);
parts.warnings.add(Log.formatThrowable(ex, false));
filename = null;
}
ContentType contentType;
try {
// From the body structure
contentType = new ContentType(part.getContentType());
} catch (ParseException ex) {
if (part instanceof MimeMessage)
Log.w("MimeMessage content type=" + ex.getMessage());
else
Log.w(ex);
contentType = new ContentType(Helper.guessMimeType(filename));
}
String ct = contentType.getBaseType();
if (("text/plain".equalsIgnoreCase(ct) || "text/html".equalsIgnoreCase(ct)) &&
!Part.ATTACHMENT.equalsIgnoreCase(disposition) && TextUtils.isEmpty(filename)) {
parts.text.add(new PartHolder(part, contentType));
} else {
if ("message/delivery-status".equalsIgnoreCase(contentType.getBaseType()) ||
"message/disposition-notification".equalsIgnoreCase(contentType.getBaseType()))
parts.extra.add(new PartHolder(part, contentType));
AttachmentPart apart = new AttachmentPart();
apart.disposition = disposition;
apart.filename = filename;
apart.encrypt = encrypt;
apart.part = part;
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(Log.formatThrowable(ex, false));
}
apart.attachment = new EntityAttachment();
apart.attachment.disposition = apart.disposition;
apart.attachment.name = apart.filename;
apart.attachment.type = contentType.getBaseType().toLowerCase(Locale.ROOT);
apart.attachment.size = (long) apart.part.getSize();
apart.attachment.cid = (cid == null || cid.length == 0 ? null : MimeUtility.unfold(cid[0]));
apart.attachment.encryption = apart.encrypt;
if ("text/calendar".equalsIgnoreCase(apart.attachment.type) &&
TextUtils.isEmpty(apart.attachment.name))
apart.attachment.name = "invite.ics";
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) {
if (ex instanceof ParseException)
Log.e(ex);
else
Log.w(ex);
parts.warnings.add(Log.formatThrowable(ex, false));
}
}
private void ensureEnvelope() throws MessagingException {
_ensureMessage(false, false);
}
private void ensureHeaders() throws MessagingException {
_ensureMessage(false, true);
}
private void ensureStructure() throws MessagingException {
_ensureMessage(true, true);
}
private void _ensureMessage(boolean structure, boolean headers) throws MessagingException {
if (structure) {
if (ensuredStructure)
return;
ensuredStructure = true;
} else if (headers) {
if (ensuredHeaders)
return;
ensuredHeaders = true;
} else {
if (ensuredEnvelope)
return;
ensuredEnvelope = true;
}
Log.i("Ensure structure=" + structure + " headers=" + headers);
try {
if (imessage instanceof IMAPMessage) {
if (structure)
imessage.getContentType(); // force loadBODYSTRUCTURE
else {
if (headers)
imessage.getAllHeaders(); // force loadHeaders
else
imessage.getMessageID(); // force loadEnvelope
}
}
} catch (MessagingException ex) {
// https://javaee.github.io/javamail/FAQ#imapserverbug
if ("Failed to load IMAP envelope".equals(ex.getMessage()) ||
"Unable to load BODYSTRUCTURE".equals(ex.getMessage()))
try {
Log.w("Fetching raw message");
File file = File.createTempFile("serverbug", null, cacheDir);
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
imessage.writeTo(os);
}
Properties props = MessageHelper.getSessionProperties();
Session isession = Session.getInstance(props, null);
Log.w("Decoding raw message");
try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
imessage = new MimeMessageEx(isession, is, imessage);
}
file.delete();
} catch (IOException ex1) {
Log.e(ex1);
throw ex;
}
else
throw ex;
}
}
static int getMessageCount(Folder folder) {
try {
// Prevent pool lock
if (folder instanceof IMAPFolder) {
int count = ((IMAPFolder) folder).getCachedCount();
Log.i(folder.getFullName() + " total count=" + count);
return count;
}
int count = 0;
for (Message message : folder.getMessages())
if (!message.isExpunged())
count++;
return count;
} catch (Throwable ex) {
Log.e(ex);
return -1;
}
}
static boolean hasCapability(IMAPFolder ifolder, final String capability) throws MessagingException {
// Folder can have different capabilities than the store
return (boolean) ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
return protocol.hasCapability(capability);
}
});
}
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 =
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 Normalizer.normalize(sb.toString(), Normalizer.Form.NFKD)
.replaceAll("[^\\p{ASCII}]", "");
}
static String sanitizeEmail(String email) {
if (email.contains("<") && email.contains(">"))
try {
InternetAddress address = new InternetAddress(email);
return address.getAddress();
} catch (AddressException ex) {
Log.e(ex);
}
return email;
}
static boolean isRemoved(Throwable ex) {
while (ex != null) {
if (ex instanceof MessageRemovedException ||
ex instanceof MessageRemovedIOException)
return true;
ex = ex.getCause();
}
return false;
}
static boolean equalEmail(Address a1, Address a2) {
String email1 = ((InternetAddress) a1).getAddress();
String email2 = ((InternetAddress) a2).getAddress();
if (email1 != null)
email1 = email1.toLowerCase(Locale.ROOT);
if (email2 != null)
email2 = email2.toLowerCase(Locale.ROOT);
return Objects.equals(email1, email2);
}
static boolean equalEmail(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 (!equalEmail(a1[i], a2[i]))
return false;
return true;
}
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;
}
}