package com.fsck.k9.message; import java.util.Date; import java.util.List; import java.util.Locale; import android.content.Context; import com.fsck.k9.Account.QuoteStyle; import com.fsck.k9.Identity; import com.fsck.k9.K9; import com.fsck.k9.R; import com.fsck.k9.activity.MessageReference; import com.fsck.k9.activity.misc.Attachment; import com.fsck.k9.crypto.PgpData; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mailstore.TempFileBody; import com.fsck.k9.mailstore.TempFileMessageBody; import org.apache.james.mime4j.codec.EncoderUtil; import org.apache.james.mime4j.util.MimeUtil; public class MessageBuilder { private final Context context; private String subject; private Address[] to; private Address[] cc; private Address[] bcc; private String inReplyTo; private String references; private boolean requestReadReceipt; private Identity identity; private SimpleMessageFormat messageFormat; private String text; private PgpData pgpData; private List attachments; private String signature; private QuoteStyle quoteStyle; private QuotedTextMode quotedTextMode; private String quotedText; private InsertableHtmlContent quotedHtmlContent; private boolean isReplyAfterQuote; private boolean isSignatureBeforeQuotedText; private boolean identityChanged; private boolean signatureChanged; private int cursorPosition; private MessageReference messageReference; private boolean isDraft; public MessageBuilder(Context context) { this.context = context; } /** * Build the final message to be sent (or saved). If there is another message quoted in this one, it will be baked * into the final message here. */ public MimeMessage build() throws MessagingException { //FIXME: check arguments MimeMessage message = new MimeMessage(); message.addSentDate(new Date(), K9.hideTimeZone()); Address from = new Address(identity.getEmail(), identity.getName()); message.setFrom(from); message.setRecipients(RecipientType.TO, to); message.setRecipients(RecipientType.CC, cc); message.setRecipients(RecipientType.BCC, bcc); message.setSubject(subject); if (requestReadReceipt) { message.setHeader("Disposition-Notification-To", from.toEncodedString()); message.setHeader("X-Confirm-Reading-To", from.toEncodedString()); message.setHeader("Return-Receipt-To", from.toEncodedString()); } if (!K9.hideUserAgent()) { message.setHeader("User-Agent", context.getString(R.string.message_header_mua)); } final String replyTo = identity.getReplyTo(); if (replyTo != null) { message.setReplyTo(new Address[] { new Address(replyTo) }); } if (inReplyTo != null) { message.setInReplyTo(inReplyTo); } if (references != null) { message.setReferences(references); } // Build the body. // TODO FIXME - body can be either an HTML or Text part, depending on whether we're in // HTML mode or not. Should probably fix this so we don't mix up html and text parts. TextBody body; if (pgpData.getEncryptedData() != null) { String text = pgpData.getEncryptedData(); body = new TextBody(text); } else { body = buildText(isDraft); } // text/plain part when messageFormat == MessageFormat.HTML TextBody bodyPlain = null; final boolean hasAttachments = !attachments.isEmpty(); if (messageFormat == SimpleMessageFormat.HTML) { // HTML message (with alternative text part) // This is the compiled MIME part for an HTML message. MimeMultipart composedMimeMessage = new MimeMultipart(); composedMimeMessage.setSubType("alternative"); // Let the receiver select either the text or the HTML part. composedMimeMessage.addBodyPart(new MimeBodyPart(body, "text/html")); bodyPlain = buildText(isDraft, SimpleMessageFormat.TEXT); composedMimeMessage.addBodyPart(new MimeBodyPart(bodyPlain, "text/plain")); if (hasAttachments) { // If we're HTML and have attachments, we have a MimeMultipart container to hold the // whole message (mp here), of which one part is a MimeMultipart container // (composedMimeMessage) with the user's composed messages, and subsequent parts for // the attachments. MimeMultipart mp = new MimeMultipart(); mp.addBodyPart(new MimeBodyPart(composedMimeMessage)); addAttachmentsToMessage(mp); MimeMessageHelper.setBody(message, mp); } else { // If no attachments, our multipart/alternative part is the only one we need. MimeMessageHelper.setBody(message, composedMimeMessage); } } else if (messageFormat == SimpleMessageFormat.TEXT) { // Text-only message. if (hasAttachments) { MimeMultipart mp = new MimeMultipart(); mp.addBodyPart(new MimeBodyPart(body, "text/plain")); addAttachmentsToMessage(mp); MimeMessageHelper.setBody(message, mp); } else { // No attachments to include, just stick the text body in the message and call it good. MimeMessageHelper.setBody(message, body); } } // If this is a draft, add metadata for thawing. if (isDraft) { // Add the identity to the message. message.addHeader(K9.IDENTITY_HEADER, buildIdentityHeader(body, bodyPlain)); } message.generateMessageId(); return message; } public TextBody buildText() { return buildText(isDraft, messageFormat); } private String buildIdentityHeader(TextBody body, TextBody bodyPlain) { return new IdentityHeaderBuilder() .setCursorPosition(cursorPosition) .setIdentity(identity) .setIdentityChanged(identityChanged) .setMessageFormat(messageFormat) .setMessageReference(messageReference) .setQuotedHtmlContent(quotedHtmlContent) .setQuoteStyle(quoteStyle) .setQuoteTextMode(quotedTextMode) .setSignature(signature) .setSignatureChanged(signatureChanged) .setBody(body) .setBodyPlain(bodyPlain) .build(); } /** * Add attachments as parts into a MimeMultipart container. * @param mp MimeMultipart container in which to insert parts. * @throws MessagingException */ private void addAttachmentsToMessage(final MimeMultipart mp) throws MessagingException { Body body; for (Attachment attachment : attachments) { if (attachment.state != Attachment.LoadingState.COMPLETE) { continue; } String contentType = attachment.contentType; if (MimeUtil.isMessage(contentType)) { body = new TempFileMessageBody(attachment.filename); } else { body = new TempFileBody(attachment.filename); } MimeBodyPart bp = new MimeBodyPart(body); /* * Correctly encode the filename here. Otherwise the whole * header value (all parameters at once) will be encoded by * MimeHeader.writeTo(). */ bp.addHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\r\n name=\"%s\"", contentType, EncoderUtil.encodeIfNecessary(attachment.name, EncoderUtil.Usage.WORD_ENTITY, 7))); bp.setEncoding(MimeUtility.getEncodingforType(contentType)); /* * TODO: Oh the joys of MIME... * * From RFC 2183 (The Content-Disposition Header Field): * "Parameter values longer than 78 characters, or which * contain non-ASCII characters, MUST be encoded as specified * in [RFC 2184]." * * Example: * * Content-Type: application/x-stuff * title*1*=us-ascii'en'This%20is%20even%20more%20 * title*2*=%2A%2A%2Afun%2A%2A%2A%20 * title*3="isn't it!" */ bp.addHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, String.format(Locale.US, "attachment;\r\n filename=\"%s\";\r\n size=%d", attachment.name, attachment.size)); mp.addBodyPart(bp); } } /** * Build the Body that will contain the text of the message. We'll decide where to * include it later. Draft messages are treated somewhat differently in that signatures are not * appended and HTML separators between composed text and quoted text are not added. * @param isDraft If we should build a message that will be saved as a draft (as opposed to sent). */ private TextBody buildText(boolean isDraft) { return buildText(isDraft, messageFormat); } /** * Build the {@link Body} that will contain the text of the message. * *

* Draft messages are treated somewhat differently in that signatures are not appended and HTML * separators between composed text and quoted text are not added. *

* * @param isDraft * If {@code true} we build a message that will be saved as a draft (as opposed to * sent). * @param simpleMessageFormat * Specifies what type of message to build ({@code text/plain} vs. {@code text/html}). * * @return {@link TextBody} instance that contains the entered text and possibly the quoted * original message. */ private TextBody buildText(boolean isDraft, SimpleMessageFormat simpleMessageFormat) { String messageText = text; TextBodyBuilder textBodyBuilder = new TextBodyBuilder(messageText); /* * Find out if we need to include the original message as quoted text. * * We include the quoted text in the body if the user didn't choose to * hide it. We always include the quoted text when we're saving a draft. * That's so the user is able to "un-hide" the quoted text if (s)he * opens a saved draft. */ boolean includeQuotedText = (isDraft || quotedTextMode == QuotedTextMode.SHOW); boolean isReplyAfterQuote = (quoteStyle == QuoteStyle.PREFIX && this.isReplyAfterQuote); textBodyBuilder.setIncludeQuotedText(false); if (includeQuotedText) { if (simpleMessageFormat == SimpleMessageFormat.HTML && quotedHtmlContent != null) { textBodyBuilder.setIncludeQuotedText(true); textBodyBuilder.setQuotedTextHtml(quotedHtmlContent); textBodyBuilder.setReplyAfterQuote(isReplyAfterQuote); } if (simpleMessageFormat == SimpleMessageFormat.TEXT && quotedText.length() > 0) { textBodyBuilder.setIncludeQuotedText(true); textBodyBuilder.setQuotedText(quotedText); textBodyBuilder.setReplyAfterQuote(isReplyAfterQuote); } } textBodyBuilder.setInsertSeparator(!isDraft); boolean useSignature = (!isDraft && identity.getSignatureUse()); if (useSignature) { textBodyBuilder.setAppendSignature(true); textBodyBuilder.setSignature(signature); textBodyBuilder.setSignatureBeforeQuotedText(isSignatureBeforeQuotedText); } else { textBodyBuilder.setAppendSignature(false); } TextBody body; if (simpleMessageFormat == SimpleMessageFormat.HTML) { body = textBodyBuilder.buildTextHtml(); } else { body = textBodyBuilder.buildTextPlain(); } return body; } public MessageBuilder setSubject(String subject) { this.subject = subject; return this; } public MessageBuilder setTo(Address[] to) { this.to = to; return this; } public MessageBuilder setCc(Address[] cc) { this.cc = cc; return this; } public MessageBuilder setBcc(Address[] bcc) { this.bcc = bcc; return this; } public MessageBuilder setInReplyTo(String inReplyTo) { this.inReplyTo = inReplyTo; return this; } public MessageBuilder setReferences(String references) { this.references = references; return this; } public MessageBuilder setRequestReadReceipt(boolean requestReadReceipt) { this.requestReadReceipt = requestReadReceipt; return this; } public MessageBuilder setIdentity(Identity identity) { this.identity = identity; return this; } public MessageBuilder setMessageFormat(SimpleMessageFormat messageFormat) { this.messageFormat = messageFormat; return this; } public MessageBuilder setText(String text) { this.text = text; return this; } public MessageBuilder setPgpData(PgpData pgpData) { this.pgpData = pgpData; return this; } public MessageBuilder setAttachments(List attachments) { this.attachments = attachments; return this; } public MessageBuilder setSignature(String signature) { this.signature = signature; return this; } public MessageBuilder setQuoteStyle(QuoteStyle quoteStyle) { this.quoteStyle = quoteStyle; return this; } public MessageBuilder setQuotedTextMode(QuotedTextMode quotedTextMode) { this.quotedTextMode = quotedTextMode; return this; } public MessageBuilder setQuotedText(String quotedText) { this.quotedText = quotedText; return this; } public MessageBuilder setQuotedHtmlContent(InsertableHtmlContent quotedHtmlContent) { this.quotedHtmlContent = quotedHtmlContent; return this; } public MessageBuilder setReplyAfterQuote(boolean isReplyAfterQuote) { this.isReplyAfterQuote = isReplyAfterQuote; return this; } public MessageBuilder setSignatureBeforeQuotedText(boolean isSignatureBeforeQuotedText) { this.isSignatureBeforeQuotedText = isSignatureBeforeQuotedText; return this; } public MessageBuilder setIdentityChanged(boolean identityChanged) { this.identityChanged = identityChanged; return this; } public MessageBuilder setSignatureChanged(boolean signatureChanged) { this.signatureChanged = signatureChanged; return this; } public MessageBuilder setCursorPosition(int cursorPosition) { this.cursorPosition = cursorPosition; return this; } public MessageBuilder setMessageReference(MessageReference messageReference) { this.messageReference = messageReference; return this; } public MessageBuilder setDraft(boolean isDraft) { this.isDraft = isDraft; return this; } }