Merge pull request #1484 from k9mail/decrypted-file-provider
decrypted file provider
This commit is contained in:
commit
2622b08767
30 changed files with 780 additions and 181 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
21
k9mail/src/main/java/com/fsck/k9/Globals.java
Normal file
21
k9mail/src/main/java/com/fsck/k9/Globals.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ package com.fsck.k9.mailstore;
|
|||
public interface LocalPart {
|
||||
String getAccountUuid();
|
||||
long getId();
|
||||
String getDisplayName();
|
||||
long getSize();
|
||||
LocalMessage getMessage();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<cache-path name="decrypted" path="decrypted" />
|
||||
</paths>
|
|
@ -0,0 +1,3 @@
|
|||
<paths>
|
||||
<cache-path name="decrypted" path="decrypted" />
|
||||
</paths>
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue