diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java index 1b51b8a1e..6def28737 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java @@ -7,6 +7,7 @@ import java.util.Date; import java.util.EnumSet; import java.util.Set; +import android.support.annotation.NonNull; import android.util.Log; import com.fsck.k9.mail.filter.CountingOutputStream; @@ -129,6 +130,7 @@ public abstract class Message implements Part, CompositeBody { @Override public abstract void setHeader(String name, String value) throws MessagingException; + @NonNull @Override public abstract String[] getHeader(String name) throws MessagingException; diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java index ebfa85750..9197e0d3d 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java @@ -5,6 +5,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import android.support.annotation.NonNull; + + public interface Part { void addHeader(String name, String value) throws MessagingException; @@ -25,6 +28,7 @@ public interface Part { /** * Returns an array of headers of the given name. The array may be empty. */ + @NonNull String[] getHeader(String name) throws MessagingException; boolean isMimeType(String mimeType); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java index 813587825..1235da1d0 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -11,6 +11,8 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; +import android.support.annotation.NonNull; + import org.apache.james.mime4j.util.MimeUtil; /** @@ -61,6 +63,7 @@ public class MimeBodyPart extends BodyPart { mHeader.setHeader(name, value); } + @NonNull @Override public String[] getHeader(String name) throws MessagingException { return mHeader.getHeader(name); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java index c04718b3a..eb902d9bb 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java @@ -8,6 +8,9 @@ import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.*; +import android.support.annotation.NonNull; + + public class MimeHeader implements Cloneable { public static final String HEADER_CONTENT_TYPE = "Content-Type"; public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding"; @@ -47,6 +50,7 @@ public class MimeHeader implements Cloneable { addHeader(name, value); } + @NonNull public Set getHeaderNames() { Set names = new LinkedHashSet(); for (Field field : mFields) { @@ -55,6 +59,7 @@ public class MimeHeader implements Cloneable { return names; } + @NonNull public String[] getHeader(String name) { List values = new ArrayList(); for (Field field : mFields) { diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index ff6c263fa..0bef61bb0 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -15,6 +15,8 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; +import android.support.annotation.NonNull; + import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.dom.field.DateTimeField; @@ -422,6 +424,7 @@ public class MimeMessage extends Message { mHeader.setHeader(name, value); } + @NonNull @Override public String[] getHeader(String name) throws MessagingException { return mHeader.getHeader(name); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java index 5314810de..f74623d7f 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java @@ -20,6 +20,8 @@ import java.io.OutputStream; import java.util.Locale; import java.util.regex.Pattern; +import android.support.annotation.NonNull; + public class MimeUtility { public static final String DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"; @@ -1082,7 +1084,7 @@ public class MimeUtility { return DEFAULT_ATTACHMENT_MIME_TYPE; } - public static String getExtensionByMimeType(String mimeType) { + public static String getExtensionByMimeType(@NonNull String mimeType) { String lowerCaseMimeType = mimeType.toLowerCase(Locale.US); for (String[] contentTypeMapEntry : MIME_TYPE_BY_EXTENSION_MAP) { if (contentTypeMapEntry[1].equals(lowerCaseMimeType)) { diff --git a/k9mail/src/main/AndroidManifest.xml b/k9mail/src/main/AndroidManifest.xml index 31c4f1f3f..eb5c6a786 100644 --- a/k9mail/src/main/AndroidManifest.xml +++ b/k9mail/src/main/AndroidManifest.xml @@ -415,14 +415,14 @@ android:exported="false"/> + android:resource="@xml/decrypted_file_provider_paths" /> diff --git a/k9mail/src/main/java/com/fsck/k9/Globals.java b/k9mail/src/main/java/com/fsck/k9/Globals.java new file mode 100644 index 000000000..31e74e898 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/Globals.java @@ -0,0 +1,21 @@ +package com.fsck.k9; + + +import android.content.Context; + + +public class Globals { + private static Context context; + + static void setContext(Context context) { + Globals.context = context; + } + + public static Context getContext() { + if (context == null) { + throw new IllegalStateException("No context provided"); + } + + return context; + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/K9.java b/k9mail/src/main/java/com/fsck/k9/K9.java index 93ceeccab..34e41347a 100644 --- a/k9mail/src/main/java/com/fsck/k9/K9.java +++ b/k9mail/src/main/java/com/fsck/k9/K9.java @@ -505,6 +505,7 @@ public class K9 extends Application { super.onCreate(); app = this; + Globals.setContext(this); K9MailLib.setDebugStatus(new K9MailLib.DebugStatus() { @Override public boolean enabled() { diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentResolver.java b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentResolver.java index eb2faa321..5742a1a78 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentResolver.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentResolver.java @@ -6,9 +6,9 @@ import java.util.HashMap; import java.util.Map; import java.util.Stack; -import android.content.Context; import android.net.Uri; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; @@ -41,13 +41,15 @@ public class AttachmentResolver { } @WorkerThread - public static AttachmentResolver createFromPart(Context context, Part part) { - Map contentIdToAttachmentUriMap = buildCidToAttachmentUriMap(context, part); - + public static AttachmentResolver createFromPart(Part part) { + AttachmentInfoExtractor attachmentInfoExtractor = AttachmentInfoExtractor.getInstance(); + Map contentIdToAttachmentUriMap = buildCidToAttachmentUriMap(attachmentInfoExtractor, part); return new AttachmentResolver(contentIdToAttachmentUriMap); } - private static Map buildCidToAttachmentUriMap(Context context, Part rootPart) { + @VisibleForTesting + static Map buildCidToAttachmentUriMap(AttachmentInfoExtractor attachmentInfoExtractor, + Part rootPart) { HashMap result = new HashMap<>(); Stack partsToCheck = new Stack<>(); @@ -66,7 +68,7 @@ public class AttachmentResolver { try { String contentId = part.getContentId(); if (contentId != null) { - AttachmentViewInfo attachmentInfo = AttachmentInfoExtractor.extractAttachmentInfo(context, part); + AttachmentViewInfo attachmentInfo = attachmentInfoExtractor.extractAttachmentInfo(part); result.put(contentId, attachmentInfo.uri); } } catch (MessagingException e) { diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptedTempFileBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/DeferredFileBody.java similarity index 67% rename from k9mail/src/main/java/com/fsck/k9/mailstore/DecryptedTempFileBody.java rename to k9mail/src/main/java/com/fsck/k9/mailstore/DeferredFileBody.java index b96030dfe..e21882f5c 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptedTempFileBody.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/DeferredFileBody.java @@ -1,6 +1,7 @@ package com.fsck.k9.mailstore; +import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -14,31 +15,35 @@ import android.util.Log; import com.fsck.k9.K9; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.RawDataBody; import com.fsck.k9.mail.internet.SizeAware; -import org.apache.commons.io.output.DeferredFileOutputStream; +import com.fsck.k9.mailstore.util.DeferredFileOutputStream; +import com.fsck.k9.mailstore.util.FileFactory; +import org.apache.commons.io.IOUtils; -public class DecryptedTempFileBody extends BinaryAttachmentBody implements SizeAware { + +/** This is a body where the data is memory-backed at first and switches to file-backed if it gets larger. + * @see FileFactory + */ +public class DeferredFileBody implements RawDataBody, SizeAware { public static final int MEMORY_BACKED_THRESHOLD = 1024 * 8; - private final File tempDirectory; - @Nullable - private File file; + private final FileFactory fileFactory; + private final String encoding; + @Nullable private byte[] data; + private File file; - public DecryptedTempFileBody(File tempDirectory, String transferEncoding) { - this.tempDirectory = tempDirectory; - try { - setEncoding(transferEncoding); - } catch (MessagingException e) { - throw new AssertionError("setEncoding() must succeed"); - } + public DeferredFileBody(FileFactory fileFactory, String transferEncoding) { + this.fileFactory = fileFactory; + this.encoding = transferEncoding; } public OutputStream getOutputStream() throws IOException { - return new DeferredFileOutputStream(MEMORY_BACKED_THRESHOLD, "decrypted", null, tempDirectory) { + return new DeferredFileOutputStream(MEMORY_BACKED_THRESHOLD, fileFactory) { @Override public void close() throws IOException { super.close(); @@ -57,7 +62,7 @@ public class DecryptedTempFileBody extends BinaryAttachmentBody implements SizeA try { if (file != null) { Log.d(K9.LOG_TAG, "Decrypted data is file-backed."); - return new FileInputStream(file); + return new BufferedInputStream(new FileInputStream(file)); } if (data != null) { Log.d(K9.LOG_TAG, "Decrypted data is memory-backed."); @@ -99,12 +104,26 @@ public class DecryptedTempFileBody extends BinaryAttachmentBody implements SizeA Log.d(K9.LOG_TAG, "Writing body to file for attachment access"); - file = File.createTempFile("decrypted", null, tempDirectory); - FileOutputStream fos = new FileOutputStream(file); fos.write(data); fos.close(); data = null; } + + @Override + public void setEncoding(String encoding) throws MessagingException { + throw new UnsupportedOperationException("Cannot re-encode a DecryptedTempFileBody!"); + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + InputStream inputStream = getInputStream(); + IOUtils.copy(inputStream, out); + } + + @Override + public String getEncoding() { + return encoding; + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java index d5a14b71a..412c02ce4 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalBodyPart.java @@ -9,16 +9,14 @@ public class LocalBodyPart extends MimeBodyPart implements LocalPart { private final String accountUuid; private final LocalMessage message; private final long messagePartId; - private final String displayName; private final long size; - public LocalBodyPart(String accountUuid, LocalMessage message, long messagePartId, String displayName, long size) + public LocalBodyPart(String accountUuid, LocalMessage message, long messagePartId, long size) throws MessagingException { super(); this.accountUuid = accountUuid; this.message = message; this.messagePartId = messagePartId; - this.displayName = displayName; this.size = size; } @@ -32,11 +30,6 @@ public class LocalBodyPart extends MimeBodyPart implements LocalPart { return messagePartId; } - @Override - public String getDisplayName() { - return displayName; - } - @Override public long getSize() { return size; diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 62e70634f..0ef241eff 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -68,6 +68,7 @@ public class LocalFolder extends Folder implements Serializable { private static final long serialVersionUID = -1973296520918624767L; private static final int MAX_BODY_SIZE_FOR_DATABASE = 16 * 1024; + private static final AttachmentInfoExtractor attachmentInfoExtractor = AttachmentInfoExtractor.getInstance(); static final long INVALID_MESSAGE_PART_ID = -1; private final LocalStore localStore; @@ -711,11 +712,12 @@ public class LocalFolder extends Folder implements Serializable { long parentId = cursor.getLong(2); String mimeType = cursor.getString(3); long size = cursor.getLong(4); - String displayName = cursor.getString(5); byte[] header = cursor.getBlob(6); int dataLocation = cursor.getInt(9); String serverExtra = cursor.getString(15); - // TODO we don't currently cache the part types. might want to do that at a later point? + // TODO we don't currently cache much of the part data which is computed with AttachmentInfoExtractor, + // TODO might want to do that at a later point? + // String displayName = cursor.getString(5); // int type = cursor.getInt(1); // boolean firstClassAttachment = (type != MessagePartType.HIDDEN_ATTACHMENT); @@ -730,7 +732,7 @@ public class LocalFolder extends Folder implements Serializable { String parentMimeType = parentPart.getMimeType(); if (MimeUtility.isMultipart(parentMimeType)) { - BodyPart bodyPart = new LocalBodyPart(getAccountUuid(), message, id, displayName, size); + BodyPart bodyPart = new LocalBodyPart(getAccountUuid(), message, id, size); ((Multipart) parentPart.getBody()).addBodyPart(bodyPart); part = bodyPart; } else if (MimeUtility.isMessage(parentMimeType)) { @@ -1401,7 +1403,7 @@ public class LocalFolder extends Folder implements Serializable { } private void missingPartToContentValues(ContentValues cv, Part part) throws MessagingException { - AttachmentViewInfo attachment = AttachmentInfoExtractor.extractAttachmentInfo(part); + AttachmentViewInfo attachment = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); cv.put("display_name", attachment.displayName); cv.put("data_location", DataLocation.MISSING); cv.put("decoded_body_size", attachment.size); @@ -1417,7 +1419,7 @@ public class LocalFolder extends Folder implements Serializable { private File leafPartToContentValues(ContentValues cv, Part part, Body body) throws MessagingException, IOException { - AttachmentViewInfo attachment = AttachmentInfoExtractor.extractAttachmentInfo(part); + AttachmentViewInfo attachment = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); cv.put("display_name", attachment.displayName); String encoding = getTransferEncoding(part); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index a789ece20..001c45f76 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -9,6 +9,7 @@ import java.util.Set; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.NonNull; import android.util.Log; import com.fsck.k9.Account; @@ -517,10 +518,12 @@ public class LocalMessage extends MimeMessage { super.setHeader(name, value); } + @NonNull @Override public String[] getHeader(String name) throws MessagingException { - if (!mHeadersLoaded) + if (!mHeadersLoaded) { loadHeaders(); + } return super.getHeader(name); } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java index 006173cea..57b0b36de 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalPart.java @@ -4,7 +4,6 @@ package com.fsck.k9.mailstore; public interface LocalPart { String getAccountUuid(); long getId(); - String getDisplayName(); long getSize(); LocalMessage getMessage(); } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java index 0a9f17a8a..afe32cba1 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java @@ -10,7 +10,6 @@ import android.content.Context; import android.support.annotation.VisibleForTesting; import com.fsck.k9.R; -import com.fsck.k9.ui.crypto.MessageCryptoSplitter; import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Message; @@ -20,6 +19,7 @@ import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.Viewable; import com.fsck.k9.message.extractors.AttachmentInfoExtractor; import com.fsck.k9.ui.crypto.MessageCryptoAnnotations; +import com.fsck.k9.ui.crypto.MessageCryptoSplitter; import com.fsck.k9.ui.crypto.MessageCryptoSplitter.CryptoMessageParts; import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter; @@ -38,7 +38,11 @@ public class MessageViewInfoExtractor { private static final String FILENAME_SUFFIX = " "; private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length(); - private MessageViewInfoExtractor() {} + + private static final AttachmentInfoExtractor attachmentInfoExtractor = AttachmentInfoExtractor.getInstance(); + + + private MessageViewInfoExtractor() { } public static MessageViewInfo extractMessageForView(Context context, Message message, MessageCryptoAnnotations annotations) throws MessagingException { @@ -70,8 +74,7 @@ public class MessageViewInfoExtractor { extraViewableText = extraViewable.text; } - AttachmentResolver attachmentResolver = - AttachmentResolver.createFromPart(context, rootPart); + AttachmentResolver attachmentResolver = AttachmentResolver.createFromPart(rootPart); return MessageViewInfo.createWithExtractedContent(message, rootPart, viewable.html, attachmentInfos, cryptoResultAnnotation, extraViewableText, extraAttachmentInfos, attachmentResolver); @@ -86,7 +89,7 @@ public class MessageViewInfoExtractor { MessageExtractor.findViewablesAndAttachments(part, viewableParts, attachments); } - attachmentInfos.addAll(AttachmentInfoExtractor.extractAttachmentInfos(context, attachments)); + attachmentInfos.addAll(attachmentInfoExtractor.extractAttachmentInfos(attachments)); return MessageViewInfoExtractor.extractTextFromViewables(context, viewableParts); } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptStreamParser.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MimePartStreamParser.java similarity index 62% rename from k9mail/src/main/java/com/fsck/k9/mailstore/DecryptStreamParser.java rename to k9mail/src/main/java/com/fsck/k9/mailstore/MimePartStreamParser.java index ee50cd0bf..ae86e00ce 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptStreamParser.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MimePartStreamParser.java @@ -1,17 +1,11 @@ package com.fsck.k9.mailstore; -import java.io.BufferedInputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Stack; -import android.content.Context; -import android.util.Log; - -import com.fsck.k9.K9; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Message; @@ -22,34 +16,21 @@ import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mailstore.util.FileFactory; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.MimeException; -import org.apache.james.mime4j.codec.Base64InputStream; -import org.apache.james.mime4j.codec.QuotedPrintableInputStream; import org.apache.james.mime4j.io.EOLConvertingInputStream; import org.apache.james.mime4j.parser.ContentHandler; import org.apache.james.mime4j.parser.MimeStreamParser; import org.apache.james.mime4j.stream.BodyDescriptor; import org.apache.james.mime4j.stream.Field; import org.apache.james.mime4j.stream.MimeConfig; -import org.apache.james.mime4j.util.MimeUtil; -// TODO rename this class? this class doesn't really bear any 'decrypted' semantics anymore... -public class DecryptStreamParser { +public class MimePartStreamParser { - private static final String DECRYPTED_CACHE_DIRECTORY = "decrypted"; - - public static MimeBodyPart parse(Context context, InputStream inputStream) throws MessagingException, IOException { - inputStream = new BufferedInputStream(inputStream, 4096); - - boolean hasInputData = waitForInputData(inputStream); - if (!hasInputData) { - return null; - } - - File decryptedTempDirectory = getDecryptedTempDirectory(context); - - MimeBodyPart decryptedRootPart = new MimeBodyPart(); + public static MimeBodyPart parse(FileFactory fileFactory, InputStream inputStream) + throws MessagingException, IOException { + MimeBodyPart parsedRootPart = new MimeBodyPart(); MimeConfig parserConfig = new MimeConfig(); parserConfig.setMaxHeaderLen(-1); @@ -57,7 +38,7 @@ public class DecryptStreamParser { parserConfig.setMaxHeaderCount(-1); MimeStreamParser parser = new MimeStreamParser(parserConfig); - parser.setContentHandler(new PartBuilder(decryptedTempDirectory, decryptedRootPart)); + parser.setContentHandler(new PartBuilder(fileFactory, parsedRootPart)); parser.setRecurse(); try { @@ -66,46 +47,15 @@ public class DecryptStreamParser { throw new MessagingException("Failed to parse decrypted content", e); } - return decryptedRootPart; + return parsedRootPart; } - private static boolean waitForInputData(InputStream inputStream) { - try { - inputStream.mark(1); - int ret = inputStream.read(); - inputStream.reset(); - return ret != -1; - } catch (IOException e) { - Log.d(K9.LOG_TAG, "got no input from pipe", e); - return false; - } - } - - private static Body createBody(InputStream inputStream, String transferEncoding, File decryptedTempDirectory) - throws IOException { - DecryptedTempFileBody body = new DecryptedTempFileBody(decryptedTempDirectory, transferEncoding); + private static Body createBody(InputStream inputStream, String transferEncoding, + FileFactory fileFactory) throws IOException { + DeferredFileBody body = new DeferredFileBody(fileFactory, transferEncoding); OutputStream outputStream = body.getOutputStream(); try { - InputStream decodingInputStream; - boolean closeStream; - if (MimeUtil.ENC_QUOTED_PRINTABLE.equals(transferEncoding)) { - decodingInputStream = new QuotedPrintableInputStream(inputStream, false); - closeStream = true; - } else if (MimeUtil.ENC_BASE64.equals(transferEncoding)) { - decodingInputStream = new Base64InputStream(inputStream); - closeStream = true; - } else { - decodingInputStream = inputStream; - closeStream = false; - } - - try { - IOUtils.copy(decodingInputStream, outputStream); - } finally { - if (closeStream) { - decodingInputStream.close(); - } - } + IOUtils.copy(inputStream, outputStream); } finally { outputStream.close(); } @@ -113,26 +63,15 @@ public class DecryptStreamParser { return body; } - private static File getDecryptedTempDirectory(Context context) { - File directory = new File(context.getCacheDir(), DECRYPTED_CACHE_DIRECTORY); - if (!directory.exists()) { - if (!directory.mkdir()) { - Log.e(K9.LOG_TAG, "Error creating directory: " + directory.getAbsolutePath()); - } - } - - return directory; - } - private static class PartBuilder implements ContentHandler { - private final File decryptedTempDirectory; + private final FileFactory fileFactory; private final MimeBodyPart decryptedRootPart; - private final Stack stack = new Stack(); + private final Stack stack = new Stack<>(); - public PartBuilder(File decryptedTempDirectory, MimeBodyPart decryptedRootPart) + public PartBuilder(FileFactory fileFactory, MimeBodyPart decryptedRootPart) throws MessagingException { - this.decryptedTempDirectory = decryptedTempDirectory; + this.fileFactory = fileFactory; this.decryptedRootPart = decryptedRootPart; } @@ -234,7 +173,7 @@ public class DecryptStreamParser { Part part = (Part) stack.peek(); String transferEncoding = bd.getTransferEncoding(); - Body body = createBody(inputStream, transferEncoding, decryptedTempDirectory); + Body body = createBody(inputStream, transferEncoding, fileFactory); part.setBody(body); } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/util/DeferredFileOutputStream.java b/k9mail/src/main/java/com/fsck/k9/mailstore/util/DeferredFileOutputStream.java new file mode 100644 index 000000000..a56ea9a9f --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/util/DeferredFileOutputStream.java @@ -0,0 +1,72 @@ +package com.fsck.k9.mailstore.util; + + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.output.ThresholdingOutputStream; + + +/** + * This OutputStream is modelled after apache commons' {@link org.apache.commons.io.output.DeferredFileOutputStream}, + * but uses a {@link FileFactory} instead of directly generating temporary files itself. + */ +public class DeferredFileOutputStream extends ThresholdingOutputStream { + private final FileFactory fileFactory; + + private OutputStream currentOutputStream; + private File outputFile; + + + public DeferredFileOutputStream(int threshold, FileFactory fileFactory) { + super(threshold); + this.fileFactory = fileFactory; + + // scale it so we expand the ByteArrayOutputStream at most three times (assuming quadratic growth) + int size = threshold < 1024 ? 256 : threshold / 4; + currentOutputStream = new ByteArrayOutputStream(size); + } + + @Override + protected OutputStream getStream() throws IOException { + return currentOutputStream; + } + + private boolean isMemoryBacked() { + return currentOutputStream instanceof ByteArrayOutputStream; + } + + @Override + protected void thresholdReached() throws IOException { + if (outputFile != null) { + throw new IllegalStateException("thresholdReached must not be called if we already have an output file!"); + } + if (!isMemoryBacked()) { + throw new IllegalStateException("currentOutputStream must be memory-based at this point!"); + } + ByteArrayOutputStream memoryOutputStream = (ByteArrayOutputStream) currentOutputStream; + + outputFile = fileFactory.createFile(); + currentOutputStream = new FileOutputStream(outputFile); + + memoryOutputStream.writeTo(currentOutputStream); + } + + public byte[] getData() { + if (!isMemoryBacked()) { + throw new IllegalStateException("getData must only be called in memory-backed state!"); + } + return ((ByteArrayOutputStream) currentOutputStream).toByteArray(); + } + + public File getFile() { + if (isMemoryBacked()) { + throw new IllegalStateException("getFile must only be called in file-backed state!"); + } + return outputFile; + } + +} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/util/FileFactory.java b/k9mail/src/main/java/com/fsck/k9/mailstore/util/FileFactory.java new file mode 100644 index 000000000..749999ca6 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/util/FileFactory.java @@ -0,0 +1,10 @@ +package com.fsck.k9.mailstore.util; + + +import java.io.File; +import java.io.IOException; + + +public interface FileFactory { + File createFile() throws IOException; +} diff --git a/k9mail/src/main/java/com/fsck/k9/message/extractors/AttachmentInfoExtractor.java b/k9mail/src/main/java/com/fsck/k9/message/extractors/AttachmentInfoExtractor.java index d510a55c1..ed1420434 100644 --- a/k9mail/src/main/java/com/fsck/k9/message/extractors/AttachmentInfoExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/message/extractors/AttachmentInfoExtractor.java @@ -8,32 +8,49 @@ import java.util.List; import android.content.Context; import android.net.Uri; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.Log; +import com.fsck.k9.Globals; +import com.fsck.k9.K9; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.AttachmentViewInfo; -import com.fsck.k9.mailstore.DecryptedTempFileBody; import com.fsck.k9.mailstore.LocalPart; +import com.fsck.k9.mailstore.DeferredFileBody; import com.fsck.k9.provider.AttachmentProvider; -import com.fsck.k9.provider.K9FileProvider; +import com.fsck.k9.provider.DecryptedFileProvider; public class AttachmentInfoExtractor { - public static List extractAttachmentInfos(Context context, List attachmentParts) - throws MessagingException { + private final Context context; + + + public static AttachmentInfoExtractor getInstance() { + Context context = Globals.getContext(); + return new AttachmentInfoExtractor(context); + } + + @VisibleForTesting + AttachmentInfoExtractor(Context context) { + this.context = context; + } + + public List extractAttachmentInfos(List attachmentParts) throws MessagingException { List attachments = new ArrayList<>(); for (Part part : attachmentParts) { - attachments.add(extractAttachmentInfo(context, part)); + attachments.add(extractAttachmentInfo(part)); } return attachments; } - public static AttachmentViewInfo extractAttachmentInfo(Context context, Part part) throws MessagingException { + public AttachmentViewInfo extractAttachmentInfo(Part part) throws MessagingException { Uri uri; long size; if (part instanceof LocalPart) { @@ -44,31 +61,39 @@ public class AttachmentInfoExtractor { uri = AttachmentProvider.getAttachmentUri(accountUuid, messagePartId); } else { Body body = part.getBody(); - if (body instanceof DecryptedTempFileBody) { - DecryptedTempFileBody decryptedTempFileBody = (DecryptedTempFileBody) body; - try { - size = decryptedTempFileBody.getSize(); - - File file = decryptedTempFileBody.getFile(); - uri = K9FileProvider.getUriForFile(context, file, part.getMimeType()); - } catch (IOException e) { - throw new MessagingException("Error preparing decrypted data as attachment", e); - } - + if (body instanceof DeferredFileBody) { + DeferredFileBody decryptedTempFileBody = (DeferredFileBody) body; + size = decryptedTempFileBody.getSize(); + uri = getDecryptedFileProviderUri(decryptedTempFileBody, part.getMimeType()); return extractAttachmentInfo(part, uri, size); } else { - throw new UnsupportedOperationException(); + throw new IllegalArgumentException("Unsupported part type provided"); } } return extractAttachmentInfo(part, uri, size); } - public static AttachmentViewInfo extractAttachmentInfo(Part part) throws MessagingException { + @Nullable + @VisibleForTesting + protected Uri getDecryptedFileProviderUri(DeferredFileBody decryptedTempFileBody, String mimeType) { + Uri uri; + try { + File file = decryptedTempFileBody.getFile(); + uri = DecryptedFileProvider.getUriForProvidedFile( + context, file, decryptedTempFileBody.getEncoding(), mimeType); + } catch (IOException e) { + Log.e(K9.LOG_TAG, "Decrypted temp file (no longer?) exists!", e); + uri = null; + } + return uri; + } + + public AttachmentViewInfo extractAttachmentInfoForDatabase(Part part) throws MessagingException { return extractAttachmentInfo(part, Uri.EMPTY, AttachmentViewInfo.UNKNOWN_SIZE); } - private static AttachmentViewInfo extractAttachmentInfo(Part part, Uri uri, long size) throws MessagingException { + private AttachmentViewInfo extractAttachmentInfo(Part part, Uri uri, long size) throws MessagingException { boolean firstClassAttachment = true; String mimeType = part.getMimeType(); @@ -82,7 +107,10 @@ public class AttachmentInfoExtractor { if (name == null) { firstClassAttachment = false; - String extension = MimeUtility.getExtensionByMimeType(mimeType); + String extension = null; + if (mimeType != null) { + extension = MimeUtility.getExtensionByMimeType(mimeType); + } name = "noname" + ((extension != null) ? "." + extension : ""); } @@ -100,7 +128,7 @@ public class AttachmentInfoExtractor { return new AttachmentViewInfo(mimeType, name, attachmentSize, uri, firstClassAttachment, part); } - private static long extractAttachmentSize(String contentDisposition, long size) { + private long extractAttachmentSize(String contentDisposition, long size) { if (size != AttachmentViewInfo.UNKNOWN_SIZE) { return size; } diff --git a/k9mail/src/main/java/com/fsck/k9/provider/DecryptedFileProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/DecryptedFileProvider.java new file mode 100644 index 000000000..653e42433 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/provider/DecryptedFileProvider.java @@ -0,0 +1,218 @@ +package com.fsck.k9.provider; + + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; +import java.util.Locale; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.ParcelFileDescriptor; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.FileProvider; +import android.text.TextUtils; +import android.util.Log; + +import com.fsck.k9.BuildConfig; +import com.fsck.k9.K9; +import com.fsck.k9.mailstore.util.FileFactory; +import org.apache.james.mime4j.codec.Base64InputStream; +import org.apache.james.mime4j.codec.QuotedPrintableInputStream; +import org.apache.james.mime4j.util.MimeUtil; +import org.openintents.openpgp.util.ParcelFileDescriptorUtil; + + +public class DecryptedFileProvider extends FileProvider { + private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".decryptedfileprovider"; + private static final String DECRYPTED_CACHE_DIRECTORY = "decrypted"; + private static final long FILE_DELETE_THRESHOLD_MILLISECONDS = 3 * 60 * 1000; + private static final Object cleanupReceiverMonitor = new Object(); + + + private static DecryptedFileProviderCleanupReceiver cleanupReceiver = null; + + + public static FileFactory getFileFactory(Context context) { + final Context applicationContext = context.getApplicationContext(); + + return new FileFactory() { + @Override + public File createFile() throws IOException { + registerFileCleanupReceiver(applicationContext); + File decryptedTempDirectory = getDecryptedTempDirectory(applicationContext); + return File.createTempFile("decrypted-", null, decryptedTempDirectory); + } + }; + } + + @Nullable + public static Uri getUriForProvidedFile(@NonNull Context context, File file, + @Nullable String encoding, @Nullable String mimeType) throws IOException { + try { + Uri.Builder uriBuilder = FileProvider.getUriForFile(context, AUTHORITY, file).buildUpon(); + if (mimeType != null) { + uriBuilder.appendQueryParameter("mime_type", mimeType); + } + if (encoding != null) { + uriBuilder.appendQueryParameter("encoding", encoding); + } + return uriBuilder.build(); + } catch (IllegalArgumentException e) { + return null; + } + } + + public static boolean deleteOldTemporaryFiles(Context context) { + File tempDirectory = getDecryptedTempDirectory(context); + boolean allFilesDeleted = true; + long deletionThreshold = new Date().getTime() - FILE_DELETE_THRESHOLD_MILLISECONDS; + for (File tempFile : tempDirectory.listFiles()) { + long lastModified = tempFile.lastModified(); + if (lastModified < deletionThreshold) { + boolean fileDeleted = tempFile.delete(); + if (!fileDeleted) { + Log.e(K9.LOG_TAG, "Failed to delete temporary file"); + // TODO really do this? might cause our service to stay up indefinitely if a file can't be deleted + allFilesDeleted = false; + } + } else { + if (K9.DEBUG) { + String timeLeftStr = String.format( + Locale.ENGLISH, "%.2f", (lastModified - deletionThreshold) / 1000 / 60.0); + Log.e(K9.LOG_TAG, "Not deleting temp file (for another " + timeLeftStr + " minutes)"); + } + allFilesDeleted = false; + } + } + + return allFilesDeleted; + } + + private static File getDecryptedTempDirectory(Context context) { + File directory = new File(context.getCacheDir(), DECRYPTED_CACHE_DIRECTORY); + if (!directory.exists()) { + if (!directory.mkdir()) { + Log.e(K9.LOG_TAG, "Error creating directory: " + directory.getAbsolutePath()); + } + } + + return directory; + } + + + @Override + public String getType(Uri uri) { + return uri.getQueryParameter("mime_type"); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException(); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + ParcelFileDescriptor pfd = super.openFile(uri, "r"); + + InputStream decodedInputStream; + String encoding = uri.getQueryParameter("encoding"); + if (MimeUtil.isBase64Encoding(encoding)) { + InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); + decodedInputStream = new Base64InputStream(inputStream); + } else if (MimeUtil.isQuotedPrintableEncoded(encoding)) { + InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); + decodedInputStream = new QuotedPrintableInputStream(inputStream); + } else { // no or unknown encoding + if (K9.DEBUG && !TextUtils.isEmpty(encoding)) { + Log.e(K9.LOG_TAG, "unsupported encoding, returning raw stream"); + } + return pfd; + } + + try { + return ParcelFileDescriptorUtil.pipeFrom(decodedInputStream); + } catch (IOException e) { + // not strictly a FileNotFoundException, but failure to create a pipe is basically "can't access right now" + throw new FileNotFoundException(); + } + } + + @Override + public void onTrimMemory(int level) { + if (level < TRIM_MEMORY_COMPLETE) { + return; + } + final Context context = getContext(); + if (context == null) { + return; + } + + new AsyncTask() { + @Override + protected Void doInBackground(Void... voids) { + deleteOldTemporaryFiles(context); + return null; + } + }.execute(); + + unregisterFileCleanupReceiver(context); + } + + private static void unregisterFileCleanupReceiver(Context context) { + synchronized (cleanupReceiverMonitor) { + if (cleanupReceiver == null) { + return; + } + + if (K9.DEBUG) { + Log.d(K9.LOG_TAG, "Unregistering temp file cleanup receiver"); + } + context.unregisterReceiver(cleanupReceiver); + cleanupReceiver = null; + } + } + + private static void registerFileCleanupReceiver(Context context) { + synchronized (cleanupReceiverMonitor) { + if (cleanupReceiver != null) { + return; + } + if (K9.DEBUG) { + Log.d(K9.LOG_TAG, "Registering temp file cleanup receiver"); + } + cleanupReceiver = new DecryptedFileProviderCleanupReceiver(); + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_SCREEN_OFF); + context.registerReceiver(cleanupReceiver, intentFilter); + } + } + + private static class DecryptedFileProviderCleanupReceiver extends BroadcastReceiver { + @Override + @MainThread + public void onReceive(Context context, Intent intent) { + if (!Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { + throw new IllegalArgumentException("onReceive called with action that isn't screen off!"); + } + + if (K9.DEBUG) { + Log.d(K9.LOG_TAG, "Cleaning up temp files"); + } + + boolean allFilesDeleted = deleteOldTemporaryFiles(context); + if (allFilesDeleted) { + unregisterFileCleanupReceiver(context); + } + } + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/provider/K9FileProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/K9FileProvider.java deleted file mode 100644 index 3d032b19d..000000000 --- a/k9mail/src/main/java/com/fsck/k9/provider/K9FileProvider.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.fsck.k9.provider; - - -import java.io.File; - -import android.content.Context; -import android.net.Uri; -import android.support.v4.content.FileProvider; - -import com.fsck.k9.BuildConfig; - - -public class K9FileProvider extends FileProvider { - private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".fileprovider"; - - public static Uri getUriForFile(Context context, File file, String mimeType) { - Uri uri = FileProvider.getUriForFile(context, AUTHORITY, file); - return uri.buildUpon().appendQueryParameter("mime_type", mimeType).build(); - } - - @Override - public String getType(Uri uri) { - return uri.getQueryParameter("mime_type"); - } -} diff --git a/k9mail/src/main/java/com/fsck/k9/ui/compose/QuotedMessagePresenter.java b/k9mail/src/main/java/com/fsck/k9/ui/compose/QuotedMessagePresenter.java index 3464d549e..56205d3b8 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/compose/QuotedMessagePresenter.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/compose/QuotedMessagePresenter.java @@ -132,8 +132,8 @@ public class QuotedMessagePresenter { resources, sourceMessage, content, quoteStyle); // Load the message with the reply header. TODO replace with MessageViewInfo data - view.setQuotedHtml(quotedHtmlContent.getQuotedContent(), AttachmentResolver - .createFromPart(messageCompose, sourceMessage)); + view.setQuotedHtml(quotedHtmlContent.getQuotedContent(), + AttachmentResolver.createFromPart(sourceMessage)); // TODO: Also strip the signature from the text/plain part view.setQuotedText(QuotedMessageHelper.quoteOriginalTextMessage(resources, sourceMessage, @@ -298,7 +298,7 @@ public class QuotedMessagePresenter { } // TODO replace with MessageViewInfo data view.setQuotedHtml(quotedHtmlContent.getQuotedContent(), - AttachmentResolver.createFromPart(messageCompose, message)); + AttachmentResolver.createFromPart(message)); } } if (bodyPlainOffset != null && bodyPlainLength != null) { @@ -404,4 +404,4 @@ public class QuotedMessagePresenter { return quotedTextFormat == SimpleMessageFormat.TEXT; } -} \ No newline at end of file +} diff --git a/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java b/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java index 7193ad933..29cdf035e 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java @@ -34,9 +34,11 @@ import com.fsck.k9.mail.internet.SizeAware; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mailstore.CryptoResultAnnotation; import com.fsck.k9.mailstore.CryptoResultAnnotation.CryptoError; -import com.fsck.k9.mailstore.DecryptStreamParser; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.MessageHelper; +import com.fsck.k9.mailstore.MimePartStreamParser; +import com.fsck.k9.mailstore.util.FileFactory; +import com.fsck.k9.provider.DecryptedFileProvider; import org.apache.commons.io.IOUtils; import org.openintents.openpgp.IOpenPgpService2; import org.openintents.openpgp.OpenPgpDecryptionResult; @@ -425,7 +427,9 @@ public class MessageCryptoHelper { @WorkerThread public MimeBodyPart processData(InputStream is) throws IOException { try { - return DecryptStreamParser.parse(context, is); + FileFactory fileFactory = + DecryptedFileProvider.getFileFactory(context); + return MimePartStreamParser.parse(fileFactory, is); } catch (MessagingException e) { Log.e(K9.LOG_TAG, "Something went wrong while parsing the decrypted MIME part", e); //TODO: pass error to main thread and display error message to user diff --git a/k9mail/src/main/res/xml/allowed_file_provider_paths.xml b/k9mail/src/main/res/xml/allowed_file_provider_paths.xml deleted file mode 100644 index 84682749b..000000000 --- a/k9mail/src/main/res/xml/allowed_file_provider_paths.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/k9mail/src/main/res/xml/decrypted_file_provider_paths.xml b/k9mail/src/main/res/xml/decrypted_file_provider_paths.xml new file mode 100644 index 000000000..25ef4ea94 --- /dev/null +++ b/k9mail/src/main/res/xml/decrypted_file_provider_paths.xml @@ -0,0 +1,3 @@ + + + diff --git a/k9mail/src/test/java/com/fsck/k9/mailstore/AttachmentResolverTest.java b/k9mail/src/test/java/com/fsck/k9/mailstore/AttachmentResolverTest.java new file mode 100644 index 000000000..b2bd08572 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/mailstore/AttachmentResolverTest.java @@ -0,0 +1,99 @@ +package com.fsck.k9.mailstore; + + +import java.util.Map; + +import android.net.Uri; + +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.message.extractors.AttachmentInfoExtractor; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("unchecked") +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = 21) +public class AttachmentResolverTest { + public static final Uri ATTACHMENT_TEST_URI_1 = Uri.parse("uri://test/1"); + public static final Uri ATTACHMENT_TEST_URI_2 = Uri.parse("uri://test/2"); + + + private AttachmentInfoExtractor attachmentInfoExtractor; + + + @Before + public void setUp() throws Exception { + attachmentInfoExtractor = mock(AttachmentInfoExtractor.class); + } + + @Test + public void buildCidMap__onPartWithNoBody__shouldReturnEmptyMap() throws Exception { + Part part = new MimeBodyPart(); + + Map result = AttachmentResolver.buildCidToAttachmentUriMap(attachmentInfoExtractor, part); + + assertTrue(result.isEmpty()); + } + + @Test + public void buildCidMap__onMultipartWithNoParts__shouldReturnEmptyMap() throws Exception { + Multipart multipartBody = new MimeMultipart(); + Part multipartPart = new MimeBodyPart(multipartBody); + + Map result = AttachmentResolver.buildCidToAttachmentUriMap(attachmentInfoExtractor, multipartPart); + + assertTrue(result.isEmpty()); + } + + @Test + public void buildCidMap__onMultipartWithEmptyBodyPart__shouldReturnEmptyMap() throws Exception { + Multipart multipartBody = new MimeMultipart(); + BodyPart bodyPart = mock(BodyPart.class); + Part multipartPart = new MimeBodyPart(multipartBody); + multipartBody.addBodyPart(bodyPart); + + Map result = AttachmentResolver.buildCidToAttachmentUriMap(attachmentInfoExtractor, multipartPart); + + verify(bodyPart).getContentId(); + assertTrue(result.isEmpty()); + } + + @Test + public void buildCidMap__onTwoPart__shouldReturnBothUris() throws Exception { + Multipart multipartBody = new MimeMultipart(); + Part multipartPart = new MimeBodyPart(multipartBody); + + BodyPart subPart1 = mock(BodyPart.class); + BodyPart subPart2 = mock(BodyPart.class); + multipartBody.addBodyPart(subPart1); + multipartBody.addBodyPart(subPart2); + + when(subPart1.getContentId()).thenReturn("cid-1"); + when(subPart2.getContentId()).thenReturn("cid-2"); + + when(attachmentInfoExtractor.extractAttachmentInfo(subPart1)).thenReturn( + new AttachmentViewInfo(null, null, AttachmentViewInfo.UNKNOWN_SIZE, ATTACHMENT_TEST_URI_1, true, subPart1)); + when(attachmentInfoExtractor.extractAttachmentInfo(subPart2)).thenReturn( + new AttachmentViewInfo(null, null, AttachmentViewInfo.UNKNOWN_SIZE, ATTACHMENT_TEST_URI_2, true, subPart2)); + + + Map result = AttachmentResolver.buildCidToAttachmentUriMap(attachmentInfoExtractor, multipartPart); + + assertEquals(2, result.size()); + assertEquals(ATTACHMENT_TEST_URI_1, result.get("cid-1")); + assertEquals(ATTACHMENT_TEST_URI_2, result.get("cid-2")); + } +} diff --git a/k9mail/src/test/java/com/fsck/k9/mailstore/MigrationTest.java b/k9mail/src/test/java/com/fsck/k9/mailstore/MigrationTest.java index 27101ef32..00db71789 100644 --- a/k9mail/src/test/java/com/fsck/k9/mailstore/MigrationTest.java +++ b/k9mail/src/test/java/com/fsck/k9/mailstore/MigrationTest.java @@ -248,7 +248,6 @@ public class MigrationTest { LocalBodyPart attachmentPart = (LocalBodyPart) body.getBodyPart(1); Assert.assertEquals("image/png", attachmentPart.getMimeType()); Assert.assertEquals("2", attachmentPart.getServerExtra()); - Assert.assertEquals("k9small.png", attachmentPart.getDisplayName()); Assert.assertEquals("attachment", MimeUtility.getHeaderParameter(attachmentPart.getDisposition(), null)); Assert.assertEquals("k9small.png", MimeUtility.getHeaderParameter(attachmentPart.getDisposition(), "filename")); Assert.assertEquals("2250", MimeUtility.getHeaderParameter(attachmentPart.getDisposition(), "size")); diff --git a/k9mail/src/test/java/com/fsck/k9/message/extractors/AttachmentInfoExtractorTest.java b/k9mail/src/test/java/com/fsck/k9/message/extractors/AttachmentInfoExtractorTest.java new file mode 100644 index 000000000..9abdb2a01 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/message/extractors/AttachmentInfoExtractorTest.java @@ -0,0 +1,191 @@ +package com.fsck.k9.message.extractors; + + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.Nullable; + +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MimeHeader; +import com.fsck.k9.mailstore.AttachmentViewInfo; +import com.fsck.k9.mailstore.LocalBodyPart; +import com.fsck.k9.mailstore.DeferredFileBody; +import com.fsck.k9.provider.AttachmentProvider; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = 21) +public class AttachmentInfoExtractorTest { + public static final Uri TEST_URI = Uri.parse("uri://test"); + public static final String TEST_MIME_TYPE = "text/plain"; + public static final long TEST_SIZE = 123L; + public static final String TEST_ACCOUNT_UUID = "uuid"; + public static final long TEST_ID = 234L; + public static final String[] TEST_CONTENT_ID = new String[] { "test-content-id" }; + + + private AttachmentInfoExtractor attachmentInfoExtractor; + private Context context; + + + @Before + public void setUp() throws Exception { + context = RuntimeEnvironment.application; + attachmentInfoExtractor = new AttachmentInfoExtractor(context); + } + + @Test(expected = IllegalArgumentException.class) + public void extractInfo__withGenericPart_shouldThrow() throws Exception { + Part part = mock(Part.class); + + attachmentInfoExtractor.extractAttachmentInfo(part); + } + + @Test + public void extractInfo__fromLocalBodyPart__shouldReturnProvidedValues() throws Exception { + LocalBodyPart part = mock(LocalBodyPart.class); + when(part.getId()).thenReturn(TEST_ID); + when(part.getMimeType()).thenReturn(TEST_MIME_TYPE); + when(part.getSize()).thenReturn(TEST_SIZE); + when(part.getAccountUuid()).thenReturn(TEST_ACCOUNT_UUID); + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfo(part); + + assertEquals(AttachmentProvider.getAttachmentUri(TEST_ACCOUNT_UUID, TEST_ID), attachmentViewInfo.uri); + assertEquals(TEST_SIZE, attachmentViewInfo.size); + assertEquals(TEST_MIME_TYPE, attachmentViewInfo.mimeType); + } + + @Test + public void extractInfoForDb__withNoHeaders__shouldReturnEmptyValues() throws Exception { + Part part = mock(Part.class); + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); + + assertEquals(Uri.EMPTY, attachmentViewInfo.uri); + assertEquals(AttachmentViewInfo.UNKNOWN_SIZE, attachmentViewInfo.size); + assertEquals("noname", attachmentViewInfo.displayName); + assertNull(attachmentViewInfo.mimeType); + assertFalse(attachmentViewInfo.firstClassAttachment); + } + + @Test + public void extractInfoForDb__withTextMimeType__shouldReturnTxtExtension() throws Exception { + Part part = mock(Part.class); + when(part.getMimeType()).thenReturn("text/plain"); + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); + + // MimeUtility.getExtensionByMimeType("text/plain"); -> "txt" + assertEquals("noname.txt", attachmentViewInfo.displayName); + assertEquals("text/plain", attachmentViewInfo.mimeType); + } + + @Test + public void extractInfoForDb__withContentTypeAndName__shouldReturnNamedFirstClassAttachment() throws Exception { + Part part = mock(Part.class); + when(part.getMimeType()).thenReturn(TEST_MIME_TYPE); + when(part.getContentType()).thenReturn(TEST_MIME_TYPE + "; name=\"filename.ext\""); + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); + + assertEquals(Uri.EMPTY, attachmentViewInfo.uri); + assertEquals(TEST_MIME_TYPE, attachmentViewInfo.mimeType); + assertEquals("filename.ext", attachmentViewInfo.displayName); + assertTrue(attachmentViewInfo.firstClassAttachment); + } + + @Test + public void extractInfoForDb__withContentTypeAndEncodedWordName__shouldReturnDecodedName() throws Exception { + Part part = mock(Part.class); + when(part.getContentType()).thenReturn(TEST_MIME_TYPE + "; name=\"=?ISO-8859-1?Q?Sm=F8rrebr=F8d?=\""); + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); + + assertEquals("Smørrebrød", attachmentViewInfo.displayName); + } + + @Test + public void extractInfoForDb__withDispositionAttach__shouldReturnNamedFirstClassAttachment() throws Exception { + Part part = mock(Part.class); + when(part.getDisposition()).thenReturn("attachment" + "; filename=\"filename.ext\"; meaningless=\"dummy\""); + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); + + assertEquals(Uri.EMPTY, attachmentViewInfo.uri); + assertEquals("filename.ext", attachmentViewInfo.displayName); + assertTrue(attachmentViewInfo.firstClassAttachment); + } + + @Test + public void extractInfoForDb__withDispositionInlineAndContentId__shouldReturnNotFirstClassAttachment() + throws Exception { + Part part = mock(Part.class); + when(part.getHeader(MimeHeader.HEADER_CONTENT_ID)).thenReturn(TEST_CONTENT_ID); + when(part.getDisposition()).thenReturn("inline" + ";\n filename=\"filename.ext\";\n meaningless=\"dummy\""); + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); + + assertFalse(attachmentViewInfo.firstClassAttachment); + } + + @Test + public void extractInfoForDb__withDispositionSizeParam__shouldReturnThatSize() throws Exception { + Part part = mock(Part.class); + when(part.getDisposition()).thenReturn("doesntmatter" + ";\n size=\"" + TEST_SIZE + "\""); + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); + + assertEquals(TEST_SIZE, attachmentViewInfo.size); + } + + @Test + public void extractInfoForDb__withDispositionInvalidSizeParam__shouldReturnUnknownSize() throws Exception { + Part part = mock(Part.class); + when(part.getDisposition()).thenReturn("doesntmatter" + "; size=\"notanint\""); + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfoForDatabase(part); + + assertEquals(AttachmentViewInfo.UNKNOWN_SIZE, attachmentViewInfo.size); + } + + @Test + public void extractInfo__withDeferredFileBody() throws Exception { + attachmentInfoExtractor = new AttachmentInfoExtractor(context) { + @Nullable + @Override + protected Uri getDecryptedFileProviderUri(DeferredFileBody decryptedTempFileBody, String mimeType) { + return TEST_URI; + } + }; + + DeferredFileBody body = mock(DeferredFileBody.class); + Part part = mock(Part.class); + when(part.getBody()).thenReturn(body); + when(part.getMimeType()).thenReturn(TEST_MIME_TYPE); + + when(body.getSize()).thenReturn(TEST_SIZE); + + + AttachmentViewInfo attachmentViewInfo = attachmentInfoExtractor.extractAttachmentInfo(part); + + + assertEquals(TEST_URI, attachmentViewInfo.uri); + assertEquals(TEST_SIZE, attachmentViewInfo.size); + assertEquals(TEST_MIME_TYPE, attachmentViewInfo.mimeType); + assertFalse(attachmentViewInfo.firstClassAttachment); + } +} diff --git a/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java b/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java index dc0fdab70..911bec60a 100644 --- a/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java +++ b/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/ParcelFileDescriptorUtil.java @@ -18,6 +18,7 @@ package org.openintents.openpgp.util; +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -93,8 +94,9 @@ public class ParcelFileDescriptorUtil { public static DataSinkTransferThread asyncPipeToDataSink( OpenPgpDataSink dataSink, ParcelFileDescriptor output) throws IOException { + InputStream inputStream = new BufferedInputStream(new AutoCloseInputStream(output)); DataSinkTransferThread dataSinkTransferThread = - new DataSinkTransferThread(dataSink, new AutoCloseInputStream(output)); + new DataSinkTransferThread(dataSink, inputStream); dataSinkTransferThread.start(); return dataSinkTransferThread; }