diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java index 17870c140..157b4046c 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java @@ -1,6 +1,5 @@ package com.fsck.k9.mail.internet; -import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.filter.Base64OutputStream; import org.apache.commons.io.IOUtils; @@ -15,7 +14,7 @@ import java.io.*; * and writeTo one time. After writeTo is called, or the InputStream returned from * getInputStream is closed the file is deleted and the Body should be considered disposed of. */ -public class BinaryTempFileBody implements RawDataBody { +public class BinaryTempFileBody implements RawDataBody, SizeAware { private static File mTempDirectory; private File mFile; @@ -26,6 +25,10 @@ public class BinaryTempFileBody implements RawDataBody { mTempDirectory = tempDirectory; } + public static File getTempDirectory() { + return mTempDirectory; + } + @Override public String getEncoding() { return mEncoding; @@ -101,6 +104,15 @@ public class BinaryTempFileBody implements RawDataBody { } } + @Override + public long getSize() { + return mFile.length(); + } + + public File getFile() { + return mFile; + } + class BinaryTempFileBodyInputStream extends FilterInputStream { public BinaryTempFileBodyInputStream(InputStream in) { super(in); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java new file mode 100644 index 000000000..07d5fdef9 --- /dev/null +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java @@ -0,0 +1,6 @@ +package com.fsck.k9.mail.internet; + + +public interface SizeAware { + long getSize(); +} diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java index d6355662f..84162bd41 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java @@ -10,10 +10,11 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import com.fsck.k9.mail.filter.CountingOutputStream; import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.util.MimeUtil; -public class TextBody implements Body { +public class TextBody implements Body, SizeAware { /** * Immutable empty byte array @@ -98,4 +99,33 @@ public class TextBody implements Body { public void setComposedMessageOffset(Integer composedMessageOffset) { this.mComposedMessageOffset = composedMessageOffset; } + + @Override + public long getSize() { + try { + byte[] bytes = mBody.getBytes(mCharset); + + if (MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) { + return bytes.length; + } else { + return getLengthWhenQuotedPrintableEncoded(bytes); + } + } catch (IOException e) { + throw new RuntimeException("Couldn't get body size", e); + } + } + + private long getLengthWhenQuotedPrintableEncoded(byte[] bytes) throws IOException { + CountingOutputStream countingOutputStream = new CountingOutputStream(); + OutputStream quotedPrintableOutputStream = new QuotedPrintableOutputStream(countingOutputStream, false); + try { + quotedPrintableOutputStream.write(bytes); + } finally { + try { + quotedPrintableOutputStream.close(); + } catch (IOException e) { /* ignore */ } + } + + return countingOutputStream.getCount(); + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java index 46b9e8339..b4a289f3a 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java @@ -9,9 +9,10 @@ import java.io.OutputStream; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.RawDataBody; +import com.fsck.k9.mail.internet.SizeAware; -public class BinaryMemoryBody implements Body, RawDataBody { +public class BinaryMemoryBody implements Body, RawDataBody, SizeAware { private final byte[] data; private final String encoding; @@ -39,4 +40,9 @@ public class BinaryMemoryBody implements Body, RawDataBody { public void writeTo(OutputStream out) throws IOException, MessagingException { out.write(data); } + + @Override + public long getSize() { + return data.length; + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index c2033ad27..9b7905b85 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -4,8 +4,11 @@ package com.fsck.k9.mailstore; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; @@ -43,9 +46,11 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.filter.CountingOutputStream; +import com.fsck.k9.mail.internet.BinaryTempFileBody; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.SizeAware; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; import org.apache.commons.io.IOUtils; @@ -62,6 +67,8 @@ public class LocalFolder extends Folder implements Serializable { private static final long serialVersionUID = -1973296520918624767L; private static final Uri PLACEHOLDER_URI = Uri.EMPTY; + private static final int MAX_BODY_SIZE_FOR_DATABASE = 16 * 1024; + private static final long INVALID_MESSAGE_PART_ID = -1; private final LocalStore localStore; @@ -1340,26 +1347,48 @@ public class LocalFolder extends Folder implements Serializable { cv.put("seq", order); cv.put("server_extra", part.getServerExtra()); - partToContentValues(cv, part); - - return db.insertOrThrow("message_parts", null, cv); + return updateOrInsertMessagePart(db, cv, part, INVALID_MESSAGE_PART_ID); } - private void partToContentValues(ContentValues cv, Part part) throws IOException, MessagingException { + private void renameTemporaryFile(File file, String messagePartId) { + File destination = localStore.getAttachmentFile(messagePartId); + if (!file.renameTo(destination)) { + Log.w(K9.LOG_TAG, "Couldn't rename temporary file " + file.getAbsolutePath() + + " to " + destination.getAbsolutePath()); + } + } + + private long updateOrInsertMessagePart(SQLiteDatabase db, ContentValues cv, Part part, long existingMessagePartId) + throws IOException, MessagingException { byte[] headerBytes = getHeaderBytes(part); cv.put("mime_type", part.getMimeType()); cv.put("header", headerBytes); cv.put("type", MessagePartType.UNKNOWN); + File file = null; Body body = part.getBody(); if (body instanceof Multipart) { multipartToContentValues(cv, (Multipart) body); } else if (body == null) { missingPartToContentValues(cv, part); } else { - leafPartToContentValues(cv, part, body); + file = leafPartToContentValues(cv, part, body); } + + long messagePartId; + if (existingMessagePartId != INVALID_MESSAGE_PART_ID) { + messagePartId = existingMessagePartId; + db.update("message_parts", cv, "id = ?", new String[] { Long.toString(messagePartId) }); + } else { + messagePartId = db.insertOrThrow("message_parts", null, cv); + } + + if (file != null) { + renameTemporaryFile(file, Long.toString(messagePartId)); + } + + return messagePartId; } private void multipartToContentValues(ContentValues cv, Multipart multipart) { @@ -1376,21 +1405,64 @@ public class LocalFolder extends Folder implements Serializable { cv.put("decoded_body_size", attachment.size); } - private void leafPartToContentValues(ContentValues cv, Part part, Body body) + private File leafPartToContentValues(ContentValues cv, Part part, Body body) throws MessagingException, IOException { AttachmentViewInfo attachment = LocalMessageExtractor.extractAttachmentInfo(part, PLACEHOLDER_URI); cv.put("display_name", attachment.displayName); - cv.put("data_location", DataLocation.IN_DATABASE); - byte[] bodyData = getBodyBytes(body); String encoding = getTransferEncoding(part); - long size = decodeAndCountBytes(bodyData, encoding, bodyData.length); - cv.put("decoded_body_size", size); + if (!(body instanceof SizeAware)) { + throw new IllegalStateException("Body needs to implement SizeAware"); + } + SizeAware sizeAwareBody = (SizeAware) body; + long fileSize = sizeAwareBody.getSize(); + + File file = null; + int dataLocation; + if (fileSize > MAX_BODY_SIZE_FOR_DATABASE) { + dataLocation = DataLocation.ON_DISK; + + file = writeBodyToDiskIfNecessary(part); + + long size = decodeAndCountBytes(file, encoding, fileSize); + cv.put("decoded_body_size", size); + } else { + dataLocation = DataLocation.IN_DATABASE; + + byte[] bodyData = getBodyBytes(body); + cv.put("data", bodyData); + + long size = decodeAndCountBytes(bodyData, encoding, bodyData.length); + cv.put("decoded_body_size", size); + } + cv.put("data_location", dataLocation); cv.put("encoding", encoding); - cv.put("data", bodyData); cv.put("content_id", part.getContentId()); + + return file; + } + + private File writeBodyToDiskIfNecessary(Part part) throws MessagingException, IOException { + Body body = part.getBody(); + if (body instanceof BinaryTempFileBody) { + return ((BinaryTempFileBody) body).getFile(); + } else { + return writeBodyToDisk(body); + } + } + + private File writeBodyToDisk(Body body) throws IOException, MessagingException { + File file = File.createTempFile("body", null, BinaryTempFileBody.getTempDirectory()); + OutputStream out = new FileOutputStream(file); + try { + body.writeTo(out); + } finally { + out.close(); + } + + return file; } private long decodeAndCountBytes(byte[] bodyData, String encoding, long fallbackValue) { @@ -1398,7 +1470,17 @@ public class LocalFolder extends Folder implements Serializable { return decodeAndCountBytes(rawInputStream, encoding, fallbackValue); } - private long decodeAndCountBytes(ByteArrayInputStream rawInputStream, String encoding, long fallbackValue) { + private long decodeAndCountBytes(File file, String encoding, long fallbackValue) + throws MessagingException, IOException { + InputStream inputStream = new FileInputStream(file); + try { + return decodeAndCountBytes(inputStream, encoding, fallbackValue); + } finally { + inputStream.close(); + } + } + + private long decodeAndCountBytes(InputStream rawInputStream, String encoding, long fallbackValue) { InputStream decodingInputStream = localStore.getDecodingInputStream(rawInputStream, encoding); try { CountingOutputStream countingOutputStream = new CountingOutputStream(); @@ -1464,7 +1546,7 @@ public class LocalFolder extends Folder implements Serializable { localStore.database.execute(false, new DbCallback() { @Override public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException { - String messagePartId; + long messagePartId; Cursor cursor = db.query("message_parts", new String[] { "id" }, "root = ? AND server_extra = ?", new String[] { Long.toString(message.getMessagePartId()), part.getServerExtra() }, @@ -1474,16 +1556,13 @@ public class LocalFolder extends Folder implements Serializable { throw new IllegalStateException("Message part not found"); } - messagePartId = cursor.getString(0); + messagePartId = cursor.getLong(0); } finally { cursor.close(); } try { - ContentValues cv = new ContentValues(); - partToContentValues(cv, part); - - db.update("message_parts", cv, "id = ?", new String[] { messagePartId }); + updateOrInsertMessagePart(db, new ContentValues(), part, messagePartId); } catch (Exception e) { Log.e(K9.LOG_TAG, "Error writing message part", e); } @@ -1686,16 +1765,13 @@ public class LocalFolder extends Folder implements Serializable { } private void deleteMessagePartsFromDisk(SQLiteDatabase db, long rootMessagePartId) { - File attachmentDirectory = StorageManager.getInstance(localStore.context) - .getAttachmentDirectory(getAccountUuid(), localStore.database.getStorageProviderId()); - Cursor cursor = db.query("message_parts", new String[] { "id" }, "root = ? AND data_location = " + DataLocation.ON_DISK, new String[] { Long.toString(rootMessagePartId) }, null, null, null); try { while (cursor.moveToNext()) { String messagePartId = cursor.getString(0); - File file = new File(attachmentDirectory, messagePartId); + File file = localStore.getAttachmentFile(messagePartId); if (file.exists()) { if (!file.delete() && K9.DEBUG) { Log.d(K9.LOG_TAG, "Couldn't delete message part file: " + file.getAbsolutePath()); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java index e50015d91..e19da4877 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -728,7 +728,7 @@ public class LocalStore extends Store implements Serializable { return rawInputStream; } - private File getAttachmentFile(String attachmentId) { + File getAttachmentFile(String attachmentId) { final StorageManager storageManager = StorageManager.getInstance(context); final File attachmentDirectory = storageManager.getAttachmentDirectory(uUid, database.getStorageProviderId()); return new File(attachmentDirectory, attachmentId); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileBody.java index 934b3bd68..5dea69d53 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileBody.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/TempFileBody.java @@ -7,11 +7,13 @@ import java.io.FileNotFoundException; import java.io.InputStream; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.SizeAware; + /** * An attachment whose contents are contained in a file. */ -public class TempFileBody extends BinaryAttachmentBody { +public class TempFileBody extends BinaryAttachmentBody implements SizeAware { private final File mFile; public TempFileBody(String filename) { @@ -26,4 +28,9 @@ public class TempFileBody extends BinaryAttachmentBody { return new ByteArrayInputStream(LocalStore.EMPTY_BYTE_ARRAY); } } + + @Override + public long getSize() { + return mFile.length(); + } }