diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java index 190ae378b..8531960ec 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java @@ -2,6 +2,7 @@ package com.fsck.k9.mail; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; public interface Part { diff --git a/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptor.java b/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptor.java index f15acad9d..54a75570c 100644 --- a/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptor.java +++ b/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptor.java @@ -7,13 +7,15 @@ import java.util.Stack; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; -import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MimeUtility; public class MessageDecryptor { private static final String MULTIPART_ENCRYPTED = "multipart/encrypted"; + private static final String PROTOCOL_PARAMETER = "protocol"; + private static final String APPLICATION_PGP_ENCRYPTED = "application/pgp-encrypted"; public static List findEncryptedParts(Part startPart) { List encryptedParts = new ArrayList(); @@ -38,4 +40,12 @@ public class MessageDecryptor { return encryptedParts; } + + public static boolean isPgpMimeEncryptedPart(Part part) { + //FIXME: Doesn't work right now because LocalMessage.getContentType() doesn't load headers from database +// String contentType = part.getContentType(); +// String protocol = MimeUtility.getHeaderParameter(contentType, PROTOCOL_PARAMETER); +// return APPLICATION_PGP_ENCRYPTED.equals(protocol); + return true; + } } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptStreamParser.java b/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptStreamParser.java new file mode 100644 index 000000000..d95ab2df0 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptStreamParser.java @@ -0,0 +1,200 @@ +package com.fsck.k9.mailstore; + + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Stack; + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +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.MimeMessage; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.MimeUtility; +import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.MimeException; +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; + + +public class DecryptStreamParser { + public static DecryptedBodyPart parse(Part multipartEncrypted, InputStream inputStream) throws MessagingException, IOException { + DecryptedBodyPart decryptedRootPart = new DecryptedBodyPart(multipartEncrypted); + + MimeConfig parserConfig = new MimeConfig(); + parserConfig.setMaxHeaderLen(-1); + parserConfig.setMaxLineLen(-1); + parserConfig.setMaxHeaderCount(-1); + + MimeStreamParser parser = new MimeStreamParser(parserConfig); + parser.setContentHandler(new PartBuilder(multipartEncrypted, decryptedRootPart)); + parser.setRecurse(); + + try { + parser.parse(new EOLConvertingInputStream(inputStream)); + } catch (MimeException e) { + throw new MessagingException("Failed to parse decrypted content", e); + } + + return decryptedRootPart; + } + + private static Body createBody(InputStream inputStream, String transferEncoding) throws IOException { + //TODO: only read parts we're going to display into memory + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + IOUtils.copy(inputStream, byteArrayOutputStream); + } finally { + byteArrayOutputStream.close(); + } + + byte[] data = byteArrayOutputStream.toByteArray(); + + return new BinaryMemoryBody(data, transferEncoding); + } + + + private static class PartBuilder implements ContentHandler { + private final Part multipartEncrypted; + private final DecryptedBodyPart decryptedRootPart; + private final Stack stack = new Stack(); + + public PartBuilder(Part multipartEncrypted, DecryptedBodyPart decryptedRootPart) throws MessagingException { + this.multipartEncrypted = multipartEncrypted; + this.decryptedRootPart = decryptedRootPart; + } + + @Override + public void startMessage() throws MimeException { + if (stack.isEmpty()) { + stack.push(decryptedRootPart); + } else { + Part part = (Part) stack.peek(); + + Message innerMessage = new DecryptedMimeMessage(multipartEncrypted); + part.setBody(innerMessage); + + stack.push(innerMessage); + } + } + + @Override + public void endMessage() throws MimeException { + stack.pop(); + } + + @Override + public void startBodyPart() throws MimeException { + try { + Multipart multipart = (Multipart) stack.peek(); + + BodyPart bodyPart = new DecryptedBodyPart(multipartEncrypted); + multipart.addBodyPart(bodyPart); + + stack.push(bodyPart); + } catch (MessagingException e) { + throw new MimeException(e); + } + } + + @Override + public void endBodyPart() throws MimeException { + stack.pop(); + } + + @Override + public void startHeader() throws MimeException { + // Do nothing + } + + @Override + public void field(Field parsedField) throws MimeException { + try { + String name = parsedField.getName(); + String raw = parsedField.getRaw().toString(); + + Part part = (Part) stack.peek(); + part.addRawHeader(name, raw); + } catch (MessagingException e) { + throw new MimeException(e); + } + } + + @Override + public void endHeader() throws MimeException { + // Do nothing + } + + @Override + public void preamble(InputStream is) throws MimeException, IOException { + // Do nothing + } + + @Override + public void epilogue(InputStream is) throws MimeException, IOException { + // Do nothing + } + + @Override + public void startMultipart(BodyDescriptor bd) throws MimeException { + Part part = (Part) stack.peek(); + try { + String contentType = part.getContentType(); + String mimeType = MimeUtility.getHeaderParameter(contentType, null); + String boundary = MimeUtility.getHeaderParameter(contentType, "boundary"); + + MimeMultipart multipart = new MimeMultipart(mimeType, boundary); + part.setBody(multipart); + + stack.push(multipart); + } catch (MessagingException e) { + throw new MimeException(e); + } + } + + @Override + public void endMultipart() throws MimeException { + stack.pop(); + } + + @Override + public void body(BodyDescriptor bd, InputStream inputStream) throws MimeException, IOException { + Part part = (Part) stack.peek(); + + String transferEncoding = bd.getTransferEncoding(); + Body body = createBody(inputStream, transferEncoding); + + part.setBody(body); + } + + @Override + public void raw(InputStream is) throws MimeException, IOException { + throw new IllegalStateException("Not implemented"); + } + } + + public static class DecryptedBodyPart extends MimeBodyPart { + private final Part multipartEncrypted; + + public DecryptedBodyPart(Part multipartEncrypted) throws MessagingException { + this.multipartEncrypted = multipartEncrypted; + } + } + + public static class DecryptedMimeMessage extends MimeMessage { + private final Part multipartEncrypted; + + public DecryptedMimeMessage(Part multipartEncrypted) { + this.multipartEncrypted = multipartEncrypted; + } + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java index 137435394..64b1a3ce0 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessageExtractor.java @@ -442,7 +442,8 @@ public class LocalMessageExtractor { return new AttachmentViewInfo(mimeType, displayName, size, uri, firstClassAttachment, part); } else { - throw new IllegalStateException("Not supported yet"); + //FIXME: The content provider URI thing needs to be reworked + return extractAttachmentInfo(part, null); } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java index 61ff0c9be..943b4fd83 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.java @@ -1,7 +1,13 @@ package com.fsck.k9.ui.messageview; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.ArrayDeque; import java.util.Collections; +import java.util.Deque; +import java.util.List; import java.util.Locale; import android.app.Activity; @@ -14,6 +20,7 @@ import android.content.Context; import android.content.Intent; import android.content.Loader; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; @@ -27,6 +34,7 @@ import android.view.ViewGroup; import android.widget.Toast; import com.fsck.k9.Account; +import com.fsck.k9.Identity; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.R; @@ -34,21 +42,35 @@ import com.fsck.k9.activity.ChooseFolder; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.controller.MessagingController; import com.fsck.k9.controller.MessagingListener; +import com.fsck.k9.crypto.MessageDecryptor; +import com.fsck.k9.crypto.OpenPgpApiHelper; import com.fsck.k9.crypto.PgpData; import com.fsck.k9.fragment.ConfirmationDialogFragment; import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener; import com.fsck.k9.fragment.ProgressDialogFragment; import com.fsck.k9.helper.FileBrowserHelper; import com.fsck.k9.helper.FileBrowserHelper.FileBrowserFailOverCallback; +import com.fsck.k9.helper.IdentityHelper; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; import com.fsck.k9.mailstore.AttachmentViewInfo; +import com.fsck.k9.mailstore.DecryptStreamParser; +import com.fsck.k9.mailstore.DecryptStreamParser.DecryptedBodyPart; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.MessageViewInfo; import com.fsck.k9.ui.message.DecodeMessageLoader; import com.fsck.k9.ui.message.LocalMessageLoader; import com.fsck.k9.view.MessageHeader; +import org.openintents.openpgp.IOpenPgpService; import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; +import org.openintents.openpgp.util.OpenPgpServiceConnection; +import org.openintents.openpgp.util.OpenPgpServiceConnection.OnBound; public class MessageViewFragment extends Fragment implements ConfirmationDialogFragmentListener, @@ -109,6 +131,9 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF private LoaderCallbacks decodeMessageLoaderCallback = new DecodeMessageLoaderCallback(); private MessageViewInfo messageViewInfo; private AttachmentViewInfo currentAttachmentViewInfo; + private Deque partsToDecrypt; + private OpenPgpApi openPgpApi; + private Part currentlyDecryptingPart; @Override @@ -223,11 +248,177 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF if (message.isBodyMissing()) { startDownloadingMessageBody(message); + } else { + decryptMessagePartsIfNecessary(message); + } + } + + private void decryptMessagePartsIfNecessary(LocalMessage message) { + List encryptedParts = MessageDecryptor.findEncryptedParts(message); + if (!encryptedParts.isEmpty()) { + partsToDecrypt = new ArrayDeque(encryptedParts); + decryptNextPartOrStartExtractingTextAndAttachments(); } else { startExtractingTextAndAttachments(message); } } + private void decryptNextPartOrStartExtractingTextAndAttachments() { + if (partsToDecrypt.isEmpty()) { + startExtractingTextAndAttachments(mMessage); + return; + } + + Part part = partsToDecrypt.peekFirst(); + if (MessageDecryptor.isPgpMimeEncryptedPart(part)) { + startDecryptingPart(part); + } else { + // Note: We currently only support PGP/MIME multipart/encrypted parts + + partsToDecrypt.removeFirst(); + decryptNextPartOrStartExtractingTextAndAttachments(); + } + } + + private void startDecryptingPart(Part part) { + Multipart multipart = (Multipart) part.getBody(); + if (multipart == null) { + throw new RuntimeException("Downloading missing parts before decryption isn't supported yet"); + } + + if (!isBoundToCryptoProviderService()) { + connectToCryptoProviderService(); + } else { + decryptPart(part); + } + } + + private boolean isBoundToCryptoProviderService() { + return openPgpApi != null; + } + + private void connectToCryptoProviderService() { + String openPgpProvider = mAccount.getOpenPgpProvider(); + new OpenPgpServiceConnection(getContext(), openPgpProvider, + new OnBound() { + @Override + public void onBound(IOpenPgpService service) { + openPgpApi = new OpenPgpApi(getContext(), service); + + decryptNextPartOrStartExtractingTextAndAttachments(); + } + + @Override + public void onError(Exception e) { + Log.e(K9.LOG_TAG, "Couldn't connect to OpenPgpService", e); + } + }).bindToService(); + } + + private void decryptPart(Part part) { + currentlyDecryptingPart = part; + decryptVerify(new Intent()); + } + + private void decryptVerify(Intent intent) { + intent.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + + Identity identity = IdentityHelper.getRecipientIdentityFromMessage(mAccount, mMessage); + String accountName = OpenPgpApiHelper.buildAccountName(identity); + intent.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, accountName); + + try { + PipedInputStream pipedInputStream = getPipedInputStreamForEncryptedData(); + PipedOutputStream decryptedOutputStream = getPipedOutputStreamForDecryptedData(); + + openPgpApi.executeApiAsync(intent, pipedInputStream, decryptedOutputStream, new IOpenPgpCallback() { + @Override + public void onReturn(Intent result) { + //TODO: check result code + //TODO: signal to AsyncTask in getPipedOutputStreamForDecryptedData() that we have a result code + //TODO: handle RESULT_INTENT + } + }); + } catch (IOException e) { + Log.e(K9.LOG_TAG, "IOException", e); + } + } + + private PipedInputStream getPipedInputStreamForEncryptedData() throws IOException { + PipedInputStream pipedInputStream = new PipedInputStream(); + + final PipedOutputStream out = new PipedOutputStream(pipedInputStream); + new Thread(new Runnable() { + @Override + public void run() { + try { + Multipart multipartEncryptedMultipart = (Multipart) currentlyDecryptingPart.getBody(); + BodyPart encryptionPayloadPart = multipartEncryptedMultipart.getBodyPart(1); + Body encryptionPayloadBody = encryptionPayloadPart.getBody(); + encryptionPayloadBody.writeTo(out); + } catch (Exception e) { + Log.e(K9.LOG_TAG, "Exception while writing message to crypto provider", e); + } + } + }).start(); + + return pipedInputStream; + } + + private PipedOutputStream getPipedOutputStreamForDecryptedData() throws IOException { + PipedOutputStream decryptedOutputStream = new PipedOutputStream(); + final PipedInputStream decryptedInputStream = new PipedInputStream(decryptedOutputStream); + new AsyncTask() { + @Override + protected DecryptedBodyPart doInBackground(Void... params) { + try { + DecryptedBodyPart decryptedPart = + DecryptStreamParser.parse(currentlyDecryptingPart, decryptedInputStream); + + //TODO: wait for IOpenPgpCallback.onReturn() to get the result code and only use + // decryptedPart when the decryption was successful + + return decryptedPart; + } catch (Exception 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 + } + + return null; + } + + @Override + protected void onPostExecute(DecryptedBodyPart decryptedPart) { + if (decryptedPart == null) { + onDecryptionFailed(); + } else { + onDecryptionSuccess(decryptedPart); + } + } + }.execute(); + return decryptedOutputStream; + } + + private void onDecryptionSuccess(DecryptedBodyPart decryptedPart) { + addDecryptedPartToMessage(decryptedPart); + onDecryptionFinished(); + } + + private void addDecryptedPartToMessage(DecryptedBodyPart decryptedPart) { + Multipart multipart = (Multipart) currentlyDecryptingPart.getBody(); + multipart.addBodyPart(decryptedPart); + } + + private void onDecryptionFailed() { + //TODO: display error to user? + onDecryptionFinished(); + } + + private void onDecryptionFinished() { + partsToDecrypt.removeFirst(); + decryptNextPartOrStartExtractingTextAndAttachments(); + } + private void onLoadMessageFromDatabaseFailed() { mMessageView.showStatusMessage(mContext.getString(R.string.status_invalid_id_error)); }