Merge pull request #1484 from k9mail/decrypted-file-provider

decrypted file provider
This commit is contained in:
cketti 2016-07-18 00:01:59 +02:00 committed by GitHub
commit 2622b08767
30 changed files with 780 additions and 181 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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<String> getHeaderNames() {
Set<String> names = new LinkedHashSet<String>();
for (Field field : mFields) {
@ -55,6 +59,7 @@ public class MimeHeader implements Cloneable {
return names;
}
@NonNull
public String[] getHeader(String name) {
List<String> values = new ArrayList<String>();
for (Field field : mFields) {

View file

@ -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);

View file

@ -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)) {

View file

@ -415,14 +415,14 @@
android:exported="false"/>
<provider
android:name=".provider.K9FileProvider"
android:authorities="${applicationId}.fileprovider"
android:name=".provider.DecryptedFileProvider"
android:authorities="${applicationId}.decryptedfileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/allowed_file_provider_paths" />
android:resource="@xml/decrypted_file_provider_paths" />
</provider>

View file

@ -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;
}
}

View file

@ -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() {

View file

@ -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<String,Uri> contentIdToAttachmentUriMap = buildCidToAttachmentUriMap(context, part);
public static AttachmentResolver createFromPart(Part part) {
AttachmentInfoExtractor attachmentInfoExtractor = AttachmentInfoExtractor.getInstance();
Map<String, Uri> contentIdToAttachmentUriMap = buildCidToAttachmentUriMap(attachmentInfoExtractor, part);
return new AttachmentResolver(contentIdToAttachmentUriMap);
}
private static Map<String,Uri> buildCidToAttachmentUriMap(Context context, Part rootPart) {
@VisibleForTesting
static Map<String,Uri> buildCidToAttachmentUriMap(AttachmentInfoExtractor attachmentInfoExtractor,
Part rootPart) {
HashMap<String,Uri> result = new HashMap<>();
Stack<Part> 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) {

View file

@ -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;
}
}

View file

@ -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;

View file

@ -68,6 +68,7 @@ public class LocalFolder extends Folder<LocalMessage> 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<LocalMessage> 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<LocalMessage> 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<LocalMessage> 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<LocalMessage> 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);

View file

@ -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);
}

View file

@ -4,7 +4,6 @@ package com.fsck.k9.mailstore;
public interface LocalPart {
String getAccountUuid();
long getId();
String getDisplayName();
long getSize();
LocalMessage getMessage();
}

View file

@ -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);
}

View file

@ -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<Object> stack = new Stack<Object>();
private final Stack<Object> 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);
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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<AttachmentViewInfo> extractAttachmentInfos(Context context, List<Part> 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<AttachmentViewInfo> extractAttachmentInfos(List<Part> attachmentParts) throws MessagingException {
List<AttachmentViewInfo> 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;
}

View file

@ -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<Void,Void,Void>() {
@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);
}
}
}
}

View file

@ -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");
}
}

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -1,3 +0,0 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="decrypted" path="decrypted" />
</paths>

View file

@ -0,0 +1,3 @@
<paths>
<cache-path name="decrypted" path="decrypted" />
</paths>

View file

@ -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<String,Uri> 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<String,Uri> 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<String,Uri> 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<String,Uri> 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"));
}
}

View file

@ -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"));

View file

@ -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);
}
}

View file

@ -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 <T> DataSinkTransferThread<T> asyncPipeToDataSink(
OpenPgpDataSink<T> dataSink, ParcelFileDescriptor output) throws IOException {
InputStream inputStream = new BufferedInputStream(new AutoCloseInputStream(output));
DataSinkTransferThread<T> dataSinkTransferThread =
new DataSinkTransferThread<T>(dataSink, new AutoCloseInputStream(output));
new DataSinkTransferThread<T>(dataSink, inputStream);
dataSinkTransferThread.start();
return dataSinkTransferThread;
}