Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 09ce55e7 authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #5330 from plan3d/issue_2490

Include inline attachments in forwarded message
parents 1809f4a1 68fd9cf8
Loading
Loading
Loading
Loading
+38 −4
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ package com.fsck.k9.message;

import java.util.Date;
import java.util.List;
import java.util.Map;

import android.app.Activity;
import android.app.PendingIntent;
@@ -56,6 +57,7 @@ public abstract class MessageBuilder {
    private SimpleMessageFormat messageFormat;
    private String text;
    private List<Attachment> attachments;
    private Map<String, Attachment> inlineAttachments;
    private String signature;
    private QuoteStyle quoteStyle;
    private QuotedTextMode quotedTextMode;
@@ -169,7 +171,16 @@ public abstract class MessageBuilder {
            // Let the receiver select either the text or the HTML part.
            bodyPlain = buildText(isDraft, SimpleMessageFormat.TEXT);
            composedMimeMessage.addBodyPart(MimeBodyPart.create(bodyPlain, "text/plain"));
            composedMimeMessage.addBodyPart(MimeBodyPart.create(body, "text/html"));
            MimeBodyPart htmlPart = MimeBodyPart.create(body, "text/html");
            if (inlineAttachments != null && inlineAttachments.size() > 0) {
                MimeMultipart htmlPartWithInlineImages = new MimeMultipart("multipart/related",
                        boundaryGenerator.generateBoundary());
                htmlPartWithInlineImages.addBodyPart(htmlPart);
                addInlineAttachmentsToMessage(htmlPartWithInlineImages);
                composedMimeMessage.addBodyPart(MimeBodyPart.create(htmlPartWithInlineImages));
            } else {
                composedMimeMessage.addBodyPart(htmlPart);
            }

            if (hasAttachments) {
                // If we're HTML and have attachments, we have a MimeMultipart container to hold the
@@ -236,7 +247,25 @@ public abstract class MessageBuilder {
            MimeBodyPart bp = MimeBodyPart.create(body);

            addContentType(bp, attachment.getContentType(), attachment.getName());
            addContentDisposition(bp, attachment.getName(), attachment.getSize());
            addContentDisposition(bp, "attachment", attachment.getName(), attachment.getSize());

            mp.addBodyPart(bp);
        }
    }

    private void addInlineAttachmentsToMessage(final MimeMultipart mp) throws MessagingException {
        for (String cid : inlineAttachments.keySet()) {
            Attachment attachment = inlineAttachments.get(cid);
            if (attachment.getState() != Attachment.LoadingState.COMPLETE) {
                continue;
            }

            Body body = new TempFileBody(attachment.getFileName());
            MimeBodyPart bp = MimeBodyPart.create(body);

            addContentType(bp, attachment.getContentType(), attachment.getName());
            addContentDisposition(bp, "inline", attachment.getName(), attachment.getSize());
            bp.addHeader(MimeHeader.HEADER_CONTENT_ID, cid);

            mp.addBodyPart(bp);
        }
@@ -251,8 +280,8 @@ public abstract class MessageBuilder {
        }
    }

    private void addContentDisposition(MimeBodyPart bodyPart, String fileName, Long size) {
        String value = Headers.contentDisposition("attachment", fileName, size);
    private void addContentDisposition(MimeBodyPart bodyPart, String disposition, String fileName, Long size) {
        String value = Headers.contentDisposition(disposition, fileName, size);
        bodyPart.addHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, value);
    }

@@ -401,6 +430,11 @@ public abstract class MessageBuilder {
        return this;
    }

    public MessageBuilder setInlineAttachments(Map<String, Attachment> attachments) {
        this.inlineAttachments = attachments;
        return this;
    }

    public MessageBuilder setSignature(String signature) {
        this.signature = signature;
        return this;
+30 −0
Original line number Diff line number Diff line
@@ -9,7 +9,9 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fsck.k9.Account.QuoteStyle;
import com.fsck.k9.CoreResourceProvider;
@@ -23,6 +25,7 @@ import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MessageIdGenerator;
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.message.MessageBuilder.Callback;
@@ -275,6 +278,33 @@ public class MessageBuilderTest extends RobolectricTest {
        assertEquals("text/html", parts.get(1).getMimeType());
    }

    @Test
    public void build_usingHtmlFormatWithInlineAttachment_shouldUseMultipartAlternativeInCorrectOrder() throws Exception {
        String contentId = "contentId";
        String attachmentMimeType = "image/png";

        Map<String, Attachment> inlineAttachments = new HashMap<>();
        inlineAttachments.put(contentId, createAttachmentWithContent(attachmentMimeType, "1x1.png",
                "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"));
        MessageBuilder messageBuilder = createHtmlMessageBuilder().setInlineAttachments(inlineAttachments);

        messageBuilder.buildAsync(callback);

        MimeMessage message = getMessageFromCallback();
        assertEquals(MimeMultipart.class, message.getBody().getClass());
        assertEquals("multipart/alternative", ((MimeMultipart) message.getBody()).getMimeType());
        List<BodyPart> parts =  ((MimeMultipart) message.getBody()).getBodyParts();
        //RFC 2046 - 5.1.4. - Best type is last displayable
        assertEquals("text/plain", parts.get(0).getMimeType());
        assertEquals("multipart/related", parts.get(1).getMimeType());
        List<BodyPart> partWithInlineAttachment =  ((MimeMultipart) parts.get(1).getBody()).getBodyParts();
        assertEquals("text/html", partWithInlineAttachment.get(0).getMimeType());
        assertEquals(attachmentMimeType, partWithInlineAttachment.get(1).getMimeType());
        String[] attachmentHeaders = partWithInlineAttachment.get(1).getHeader(MimeHeader.HEADER_CONTENT_ID);
        assertEquals(1, attachmentHeaders.length);
        assertEquals(contentId, attachmentHeaders[0]);
    }

    @Test
    public void build_withMessageAttachment_shouldAttachAsMessageRfc822() throws Exception {
        MessageBuilder messageBuilder = createSimpleMessageBuilder();
+1 −0
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ dependencies {
    testImplementation project(':app:storage')
    testImplementation project(':app:testing')
    testImplementation "org.robolectric:robolectric:${versions.robolectric}"
    testImplementation "androidx.test:core:${versions.androidxTestCore}"
    testImplementation "junit:junit:${versions.junit}"
    testImplementation "com.google.truth:truth:${versions.truth}"
    testImplementation "org.mockito:mockito-core:${versions.mockito}"
+2 −1
Original line number Diff line number Diff line
@@ -713,6 +713,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
                .setMessageFormat(currentMessageFormat)
                .setText(CrLfConverter.toCrLf(messageContentView.getText()))
                .setAttachments(attachmentPresenter.getAttachments())
                .setInlineAttachments(attachmentPresenter.getInlineAttachments())
                .setSignature(CrLfConverter.toCrLf(signatureView.getText()))
                .setSignatureBeforeQuotedText(account.isSignatureBeforeQuotedText())
                .setIdentityChanged(identityChanged)
@@ -1369,7 +1370,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
        }

        if (!relatedMessageProcessed) {
            attachmentPresenter.loadNonInlineAttachments(messageViewInfo);
            attachmentPresenter.loadAllAvailableAttachments(messageViewInfo);
        }

        // Decode the identity header when loading a draft.
+69 −9
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ package com.fsck.k9.activity.compose;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import android.app.Activity;
import android.content.ClipData;
@@ -19,6 +20,7 @@ import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState;
import com.fsck.k9.activity.loader.AttachmentContentLoader;
import com.fsck.k9.activity.loader.AttachmentInfoLoader;
import com.fsck.k9.activity.misc.Attachment;
import com.fsck.k9.activity.misc.InlineAttachment;
import com.fsck.k9.controller.MessageReference;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.AttachmentViewInfo;
@@ -46,7 +48,8 @@ public class AttachmentPresenter {
    private final AttachmentsChangedListener listener;

    // persistent state
    private LinkedHashMap<Uri, Attachment> attachments;
    private final LinkedHashMap<Uri, Attachment> attachments;
    private final LinkedHashMap<Uri, InlineAttachment> inlineAttachments;
    private int nextLoaderId = 0;
    private WaitingAction actionToPerformAfterWaiting = WaitingAction.NONE;

@@ -59,6 +62,7 @@ public class AttachmentPresenter {
        this.listener = listener;

        attachments = new LinkedHashMap<>();
        inlineAttachments = new LinkedHashMap<>();
    }

    public void onSaveInstanceState(Bundle outState) {
@@ -122,6 +126,14 @@ public class AttachmentPresenter {
        return new ArrayList<>(attachments.values());
    }

    public Map<String, com.fsck.k9.message.Attachment> getInlineAttachments() {
        Map<String, com.fsck.k9.message.Attachment> result = new LinkedHashMap<>();
        for (InlineAttachment attachment : inlineAttachments.values()) {
            result.put(attachment.getContentId(), attachment.getAttachment());
        }
        return result;
    }

    public void onClickAddAttachment(RecipientPresenter recipientPresenter) {
        ComposeCryptoStatus currentCachedCryptoStatus = recipientPresenter.getCurrentCachedCryptoStatus();
        if (currentCachedCryptoStatus == null) {
@@ -159,6 +171,24 @@ public class AttachmentPresenter {
        addAttachment(uri, contentType, false, false);
    }

    private void addInlineAttachment(AttachmentViewInfo attachmentViewInfo) {
        if (inlineAttachments.containsKey(attachmentViewInfo.internalUri)) {
            throw new IllegalStateException("Received the same attachmentViewInfo twice!");
        }

        int loaderId = getNextFreeLoaderId();
        Attachment attachment = Attachment.createAttachment(
                attachmentViewInfo.internalUri, loaderId, attachmentViewInfo.mimeType, true, true);
        attachment = attachment.deriveWithMetadataLoaded(
                attachmentViewInfo.mimeType, attachmentViewInfo.displayName, attachmentViewInfo.size);

        inlineAttachments.put(attachment.uri, new InlineAttachment(attachmentViewInfo.part.getContentId(), attachment));

        Bundle bundle = new Bundle();
        bundle.putParcelable(LOADER_ARG_ATTACHMENT, attachment.uri);
        loaderManager.initLoader(attachment.loaderId, bundle, mInlineAttachmentContentLoaderCallback);
    }

    private void addInternalAttachment(Uri uri, String contentType, boolean allowMessageType) {
        addAttachment(uri, contentType, allowMessageType, true);
    }
@@ -174,25 +204,26 @@ public class AttachmentPresenter {
        addAttachmentAndStartLoader(attachment);
    }

    public boolean loadNonInlineAttachments(MessageViewInfo messageViewInfo) {
    public boolean loadAllAvailableAttachments(MessageViewInfo messageViewInfo) {
        boolean allPartsAvailable = true;

        for (AttachmentViewInfo attachmentViewInfo : messageViewInfo.attachments) {
            if (attachmentViewInfo.isContentAvailable()) {
                if (attachmentViewInfo.inlineAttachment) {
                continue;
                    addInlineAttachment(attachmentViewInfo);
                } else {
                    addInternalAttachment(attachmentViewInfo);
                }
            if (!attachmentViewInfo.isContentAvailable()) {
            } else {
                allPartsAvailable = false;
                continue;
            }
            addInternalAttachment(attachmentViewInfo);
        }

        return allPartsAvailable;
    }

    public void processMessageToForward(MessageViewInfo messageViewInfo) {
        boolean isMissingParts = !loadNonInlineAttachments(messageViewInfo);
        boolean isMissingParts = !loadAllAvailableAttachments(messageViewInfo);
        if (isMissingParts) {
            attachmentMvpView.showMissingAttachmentsPartialMessageWarning();
        }
@@ -313,6 +344,35 @@ public class AttachmentPresenter {
                }
            };

    private LoaderManager.LoaderCallbacks<Attachment> mInlineAttachmentContentLoaderCallback =
            new LoaderManager.LoaderCallbacks<Attachment>() {
                @Override
                public Loader<Attachment> onCreateLoader(int id, Bundle args) {
                    Uri uri = args.getParcelable(LOADER_ARG_ATTACHMENT);
                    return new AttachmentContentLoader(context, inlineAttachments.get(uri).getAttachment());
                }

                @Override
                public void onLoadFinished(Loader<Attachment> loader, Attachment attachment) {
                    int loaderId = loader.getId();
                    loaderManager.destroyLoader(loaderId);

                    if (attachment.state == Attachment.LoadingState.COMPLETE) {
                        inlineAttachments.put(attachment.uri, new InlineAttachment(
                                inlineAttachments.get(attachment.uri).getContentId(), attachment));
                    } else {
                        inlineAttachments.remove(attachment.uri);
                    }

                    postPerformStalledAction();
                }

                @Override
                public void onLoaderReset(Loader<Attachment> loader) {
                    // nothing to do
                }
            };

    private void postPerformStalledAction() {
        new Handler().post(new Runnable() {
            @Override
Loading