WIP mime parsing

This commit is contained in:
Dominik Schürmann
2015-08-10 14:35:15 +02:00
parent 7004d129a8
commit dbaf7070ea
8 changed files with 610 additions and 42 deletions

View File

@@ -0,0 +1,360 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.operations;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.apache.james.mime4j.dom.BinaryBody;
import org.apache.james.mime4j.dom.Body;
import org.apache.james.mime4j.dom.Entity;
import org.apache.james.mime4j.dom.Message;
import org.apache.james.mime4j.dom.MessageBuilder;
import org.apache.james.mime4j.dom.Multipart;
import org.apache.james.mime4j.dom.TextBody;
import org.apache.james.mime4j.dom.address.Mailbox;
import org.apache.james.mime4j.dom.address.MailboxList;
import org.apache.james.mime4j.dom.field.AddressListField;
import org.apache.james.mime4j.dom.field.ContentTypeField;
import org.apache.james.mime4j.dom.field.DateTimeField;
import org.apache.james.mime4j.dom.field.UnstructuredField;
import org.apache.james.mime4j.field.address.AddressFormatter;
import org.apache.james.mime4j.message.BodyPart;
import org.apache.james.mime4j.message.DefaultMessageBuilder;
import org.apache.james.mime4j.message.MessageImpl;
import org.apache.james.mime4j.stream.Field;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.operations.results.MimeParsingResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.pgp.Progressable;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
import org.sufficientlysecure.keychain.service.MimeParsingParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.util.Log;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
public class MimeParsingOperation extends BaseOperation<MimeParsingParcel> {
public ArrayList<Uri> mTempUris;
public MimeParsingOperation(Context context, ProviderHelper providerHelper, Progressable progressable) {
super(context, providerHelper, progressable);
}
@NonNull
@Override
public MimeParsingResult execute(MimeParsingParcel parcel,
CryptoInputParcel cryptoInputParcel) {
OperationResult.OperationLog log = new OperationResult.OperationLog();
log.add(OperationResult.LogType.MSG_MIME_PARSING, 0);
mTempUris = new ArrayList<>();
try {
InputStream in = mContext.getContentResolver().openInputStream(parcel.getInputUri());
final MessageBuilder builder = new DefaultMessageBuilder();
final Message message = builder.parseMessage(in);
SimpleTreeNode root = createNode(message);
traverseTree(root);
log.add(OperationResult.LogType.MSG_MIME_PARSING_SUCCESS, 1);
} catch (Exception e) {
Log.e(Constants.TAG, "Mime parsing error", e);
log.add(OperationResult.LogType.MSG_MIME_PARSING_ERROR, 1);
}
return new MimeParsingResult(MimeParsingResult.RESULT_OK, log,
mTempUris);
}
private void traverseTree(SimpleTreeNode node) {
if (node.isLeaf()) {
parseAndSaveAsUris(node);
return;
}
for (SimpleTreeNode child : node.children) {
traverseTree(child);
}
}
/**
* Wraps an Object and associates it with a text. All message parts
* (headers, bodies, multiparts, body parts) will be wrapped in
* ObjectWrapper instances before they are added to the JTree instance.
*/
public static class ObjectWrapper {
private String text = "";
private Object object = null;
public ObjectWrapper(String text, Object object) {
this.text = text;
this.object = object;
}
@Override
public String toString() {
return text;
}
public Object getObject() {
return object;
}
}
// /**
// * Create a node given a Multipart body.
// * Add the Preamble, all Body parts and the Epilogue to the node.
// *
// * @return the root node of the tree.
// */
// private DefaultMutableTreeNode createNode(Header header) {
// DefaultMutableTreeNode node = new DefaultMutableTreeNode(
// new ObjectWrapper("Header", header));
//
// for (Field field : header.getFields()) {
// String name = field.getName();
//
// node.add(new DefaultMutableTreeNode(new ObjectWrapper(name, field)));
// }
//
// return node;
// }
/**
* Create a node given a Multipart body.
* Add the Preamble, all Body parts and the Epilogue to the node.
*
* @param multipart the Multipart.
* @return the root node of the tree.
*/
private SimpleTreeNode createNode(Multipart multipart) {
SimpleTreeNode node = new SimpleTreeNode(
new ObjectWrapper("Multipart", multipart));
// node.add(new DefaultMutableTreeNode(
// new ObjectWrapper("Preamble", multipart.getPreamble())));
for (Entity part : multipart.getBodyParts()) {
node.add(createNode(part));
}
// node.add(new DefaultMutableTreeNode(
// new ObjectWrapper("Epilogue", multipart.getEpilogue())));
return node;
}
/**
* Creates the tree nodes given a MIME entity (either a Message or
* a BodyPart).
*
* @param entity the entity.
* @return the root node of the tree displaying the specified entity and
* its children.
*/
private SimpleTreeNode createNode(Entity entity) {
/*
* Create the root node for the entity. It's either a
* Message or a Body part.
*/
String type = "Message";
if (entity instanceof BodyPart) {
type = "Body part";
}
SimpleTreeNode node = new SimpleTreeNode(
new ObjectWrapper(type, entity));
/*
* Add the node encapsulating the entity Header.
*/
// node.add(createNode(entity.getHeader()));
Body body = entity.getBody();
if (body instanceof Multipart) {
/*
* The body of the entity is a Multipart.
*/
node.add(createNode((Multipart) body));
} else if (body instanceof MessageImpl) {
/*
* The body is another Message.
*/
node.add(createNode((MessageImpl) body));
} else {
/*
* Discrete Body (either of type TextBody or BinaryBody).
*/
type = "Text body";
if (body instanceof BinaryBody) {
type = "Binary body";
}
type += " (" + entity.getMimeType() + ")";
node.add(new SimpleTreeNode(new ObjectWrapper(type, body)));
}
return node;
}
public void parseAndSaveAsUris(SimpleTreeNode node) {
Object o = ((ObjectWrapper) node.getUserObject()).getObject();
if (o instanceof TextBody) {
/*
* A text body. Display its contents.
*/
TextBody body = (TextBody) o;
StringBuilder sb = new StringBuilder();
try {
Reader r = body.getReader();
int c;
while ((c = r.read()) != -1) {
sb.append((char) c);
}
} catch (IOException ex) {
ex.printStackTrace();
}
Log.d(Constants.TAG, "text: " + sb.toString());
// textView.setText(sb.toString());
Uri tempUri = null;
try {
tempUri = TemporaryStorageProvider.createFile(mContext, "text", "text/plain");
OutputStream outStream = mContext.getContentResolver().openOutputStream(tempUri);
body.writeTo(outStream);
outStream.close();
} catch (IOException e) {
Log.e(Constants.TAG, "error mime parsing", e);
}
mTempUris.add(tempUri);
} else if (o instanceof BinaryBody) {
/*
* A binary body. Display its MIME type and length in bytes.
*/
BinaryBody body = (BinaryBody) o;
int size = 0;
try {
InputStream is = body.getInputStream();
while ((is.read()) != -1) {
size++;
}
} catch (IOException ex) {
ex.printStackTrace();
}
Log.d(Constants.TAG, "Binary body\n"
+ "MIME type: "
+ body.getParent().getMimeType() + "\n"
+ "Size of decoded data: " + size + " bytes");
} else if (o instanceof ContentTypeField) {
/*
* Content-Type field.
*/
ContentTypeField field = (ContentTypeField) o;
StringBuilder sb = new StringBuilder();
sb.append("MIME type: ").append(field.getMimeType()).append("\n");
for (Map.Entry<String, String> entry : field.getParameters().entrySet()) {
sb.append(entry.getKey()).append(" = ").append(entry.getValue()).append("\n");
}
Log.d(Constants.TAG, sb.toString());
} else if (o instanceof AddressListField) {
/*
* An address field (From, To, Cc, etc)
*/
AddressListField field = (AddressListField) o;
MailboxList list = field.getAddressList().flatten();
StringBuilder sb = new StringBuilder();
for (Mailbox mailbox : list) {
sb.append(AddressFormatter.DEFAULT.format(mailbox, false)).append("\n");
}
Log.d(Constants.TAG, sb.toString());
} else if (o instanceof DateTimeField) {
Date date = ((DateTimeField) o).getDate();
Log.d(Constants.TAG, date.toString());
} else if (o instanceof UnstructuredField) {
Log.d(Constants.TAG, ((UnstructuredField) o).getValue());
} else if (o instanceof Field) {
Log.d(Constants.TAG, ((Field) o).getBody());
} else {
/*
* The Object should be a Header or a String containing a
* Preamble or Epilogue.
*/
Log.d(Constants.TAG, o.toString());
}
}
public class SimpleTreeNode {
private SimpleTreeNode parent;
private Object userObject;
private ArrayList<SimpleTreeNode> children;
protected SimpleTreeNode(Object userObject) {
this.parent = null;
this.userObject = userObject;
this.children = new ArrayList<>();
}
protected Object getUserObject() {
return userObject;
}
protected void setUserObject(Object userObject) {
this.userObject = userObject;
}
public void add(SimpleTreeNode newChild) {
newChild.parent = this;
children.add(newChild);
}
public SimpleTreeNode getParent() {
return parent;
}
public boolean isLeaf() {
return children.isEmpty();
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.operations.results;
import android.net.Uri;
import android.os.Parcel;
import java.util.ArrayList;
public class MimeParsingResult extends OperationResult {
public final ArrayList<Uri> mTemporaryUris;
public ArrayList<Uri> getTemporaryUris() {
return mTemporaryUris;
}
public MimeParsingResult(int result, OperationLog log, ArrayList<Uri> temporaryUris) {
super(result, log);
mTemporaryUris = temporaryUris;
}
protected MimeParsingResult(Parcel in) {
super(in);
mTemporaryUris = in.createTypedArrayList(Uri.CREATOR);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeTypedList(mTemporaryUris);
}
public static final Creator<MimeParsingResult> CREATOR = new Creator<MimeParsingResult>() {
@Override
public MimeParsingResult createFromParcel(Parcel in) {
return new MimeParsingResult(in);
}
@Override
public MimeParsingResult[] newArray(int size) {
return new MimeParsingResult[size];
}
};
}

View File

@@ -787,6 +787,11 @@ public abstract class OperationResult implements Parcelable {
MSG_EXPORT_LOG_EXPORT_ERROR_FOPEN(LogLevel.ERROR,R.string.msg_export_log_error_fopen),
MSG_EXPORT_LOG_EXPORT_ERROR_WRITING(LogLevel.ERROR,R.string.msg_export_log_error_writing),
MSG_EXPORT_LOG_EXPORT_SUCCESS (LogLevel.OK, R.string.msg_export_log_success),
// mim parsing
MSG_MIME_PARSING(LogLevel.START,R.string.msg_mime_parsing_start),
MSG_MIME_PARSING_ERROR(LogLevel.ERROR,R.string.msg_mime_parsing_error),
MSG_MIME_PARSING_SUCCESS(LogLevel.OK,R.string.msg_mime_parsing_success),
;
public final int mMsgId;

View File

@@ -36,6 +36,7 @@ import org.sufficientlysecure.keychain.operations.EditKeyOperation;
import org.sufficientlysecure.keychain.operations.ExportOperation;
import org.sufficientlysecure.keychain.operations.ImportOperation;
import org.sufficientlysecure.keychain.operations.KeybaseVerificationOperation;
import org.sufficientlysecure.keychain.operations.MimeParsingOperation;
import org.sufficientlysecure.keychain.operations.PromoteKeyOperation;
import org.sufficientlysecure.keychain.operations.RevokeOperation;
import org.sufficientlysecure.keychain.operations.SignEncryptOperation;
@@ -137,6 +138,9 @@ public class KeychainService extends Service implements Progressable {
} else if (inputParcel instanceof KeybaseVerificationParcel) {
op = new KeybaseVerificationOperation(outerThis, new ProviderHelper(outerThis),
outerThis);
} else if (inputParcel instanceof MimeParsingParcel) {
op = new MimeParsingOperation(outerThis, new ProviderHelper(outerThis),
outerThis);
} else {
throw new AssertionError("Unrecognized input parcel in KeychainService!");
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.service;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
public class MimeParsingParcel implements Parcelable {
private Uri mInputUri;
private Uri mOutputUri;
public MimeParsingParcel() {
}
public MimeParsingParcel(Uri inputUri, Uri outputUri) {
mInputUri = inputUri;
mOutputUri = outputUri;
}
MimeParsingParcel(Parcel source) {
// we do all of those here, so the PgpSignEncryptInput class doesn't have to be parcelable
mInputUri = source.readParcelable(getClass().getClassLoader());
mOutputUri = source.readParcelable(getClass().getClassLoader());
}
public Uri getInputUri() {
return mInputUri;
}
public Uri getOutputUri() {
return mOutputUri;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(mInputUri, 0);
dest.writeParcelable(mOutputUri, 0);
}
public static final Creator<MimeParsingParcel> CREATOR = new Creator<MimeParsingParcel>() {
public MimeParsingParcel createFromParcel(final Parcel source) {
return new MimeParsingParcel(source);
}
public MimeParsingParcel[] newArray(final int size) {
return new MimeParsingParcel[size];
}
};
}

View File

@@ -28,7 +28,6 @@ import android.app.Activity;
import android.content.ClipDescription;
import android.content.Context;
import android.content.Intent;
import android.content.pm.LabeledIntent;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.Point;
@@ -38,7 +37,6 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -58,14 +56,16 @@ import android.widget.ViewAnimator;
import org.openintents.openpgp.OpenPgpMetadata;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
import org.sufficientlysecure.keychain.operations.results.MimeParsingResult;
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.TemporaryStorageProvider;
// this import NEEDS to be above the ViewModel one, or it won't compile! (as of 06/06/15)
import org.sufficientlysecure.keychain.service.MimeParsingParcel;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.StatusHolder;
import org.sufficientlysecure.keychain.ui.DecryptListFragment.DecryptFilesAdapter.ViewModel;
@@ -432,45 +432,47 @@ public class DecryptListFragment
// OpenKeychain's internal viewer
if ("text/plain".equals(metadata.getMimeType())) {
parseMime(outputUri);
// this is a significant i/o operation, use an asynctask
new AsyncTask<Void,Void,Intent>() {
@Override
protected Intent doInBackground(Void... params) {
Activity activity = getActivity();
if (activity == null) {
return null;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(outputUri, "text/plain");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
return intent;
}
@Override
protected void onPostExecute(Intent intent) {
// for result so we can possibly get a snackbar error from internal viewer
Activity activity = getActivity();
if (intent == null || activity == null) {
return;
}
LabeledIntent internalIntent = new LabeledIntent(
new Intent(intent)
.setClass(activity, DisplayTextActivity.class)
.putExtra(DisplayTextActivity.EXTRA_METADATA, result),
BuildConfig.APPLICATION_ID, R.string.view_internal, R.drawable.ic_launcher);
Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,
new Parcelable[] { internalIntent });
activity.startActivity(chooserIntent);
}
}.execute();
// new AsyncTask<Void,Void,Intent>() {
//
// @Override
// protected Intent doInBackground(Void... params) {
//
// Activity activity = getActivity();
// if (activity == null) {
// return null;
// }
//
// Intent intent = new Intent(Intent.ACTION_VIEW);
// intent.setDataAndType(outputUri, "text/plain");
// intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// return intent;
// }
//
// @Override
// protected void onPostExecute(Intent intent) {
// // for result so we can possibly get a snackbar error from internal viewer
// Activity activity = getActivity();
// if (intent == null || activity == null) {
// return;
// }
//
// LabeledIntent internalIntent = new LabeledIntent(
// new Intent(intent)
// .setClass(activity, DisplayTextActivity.class)
// .putExtra(DisplayTextActivity.EXTRA_METADATA, result),
// BuildConfig.APPLICATION_ID, R.string.view_internal, R.drawable.ic_launcher);
//
// Intent chooserIntent = Intent.createChooser(intent, getString(R.string.intent_show));
// chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,
// new Parcelable[] { internalIntent });
//
// activity.startActivity(chooserIntent);
// }
//
// }.execute();
} else {
Intent intent = new Intent(Intent.ACTION_VIEW);
@@ -484,6 +486,52 @@ public class DecryptListFragment
}
private void parseMime(final Uri inputUri) {
CryptoOperationHelper.Callback<MimeParsingParcel, MimeParsingResult> callback
= new CryptoOperationHelper.Callback<MimeParsingParcel, MimeParsingResult>() {
@Override
public MimeParsingParcel createOperationInput() {
return new MimeParsingParcel(inputUri, null);
}
@Override
public void onCryptoOperationSuccess(MimeParsingResult result) {
handleResult(result);
}
@Override
public void onCryptoOperationCancelled() {
}
@Override
public void onCryptoOperationError(MimeParsingResult result) {
handleResult(result);
}
public void handleResult(MimeParsingResult result) {
// TODO: merge with other log
// saveKeyResult.getLog().add(result, 0);
mOutputUris = new HashMap<>(result.getTemporaryUris().size());
for (Uri tempUri : result.getTemporaryUris()) {
// TODO: use same inputUri for all?
mOutputUris.put(inputUri, tempUri);
}
}
@Override
public boolean onCryptoSetProgress(String msg, int progress, int max) {
return false;
}
};
CryptoOperationHelper mimeParsingHelper = new CryptoOperationHelper<>(3, this, callback, R.string.progress_uploading);
mimeParsingHelper.cryptoOperation();
}
@Override
public PgpDecryptVerifyInputParcel createOperationInput() {