diff --git a/src/com/fsck/k9/mail/internet/InsertableHtmlContent.java b/src/com/fsck/k9/activity/InsertableHtmlContent.java similarity index 98% rename from src/com/fsck/k9/mail/internet/InsertableHtmlContent.java rename to src/com/fsck/k9/activity/InsertableHtmlContent.java index fabcc6082..851f0ca9a 100644 --- a/src/com/fsck/k9/mail/internet/InsertableHtmlContent.java +++ b/src/com/fsck/k9/activity/InsertableHtmlContent.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.internet; +package com.fsck.k9.activity; import java.io.Serializable; @@ -12,7 +12,7 @@ import java.io.Serializable; * * TODO: This container should also have a text part, along with its insertion point. Or maybe a generic InsertableContent and maintain one each for Html and Text? */ -public class InsertableHtmlContent implements Serializable { +class InsertableHtmlContent implements Serializable { private static final long serialVersionUID = 2397327034L; // Default to a headerInsertionPoint at the beginning of the message. private int headerInsertionPoint = 0; diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index 30f3e11cb..ffdc95878 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -90,13 +90,12 @@ import com.fsck.k9.fragment.ProgressDialogFragment; import com.fsck.k9.helper.ContactItem; import com.fsck.k9.helper.Contacts; import com.fsck.k9.mail.filter.Base64; -import com.fsck.k9.mail.internet.HtmlConverter; +import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.helper.IdentityHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.Flag; -import com.fsck.k9.mail.internet.InsertableHtmlContent; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessagingException; @@ -108,7 +107,6 @@ import com.fsck.k9.mail.internet.MimeMessage; 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.mail.internet.TextBodyBuilder; import com.fsck.k9.mailstore.LocalAttachmentBody; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.TempFileBody; @@ -2957,10 +2955,10 @@ public class MessageCompose extends K9Activity implements OnClickListener, if (messageFormat == MessageFormat.HTML) { - Part part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + Part part = message.findFirstPartByMimeType("text/html"); if (part != null) { // Shouldn't happen if we were the one who saved it. mQuotedTextFormat = SimpleMessageFormat.HTML; - String text = MimeUtility.getTextFromPart(part); + String text = part.getText(); if (K9.DEBUG) { Log.d(K9.LOG_TAG, "Loading message with offset " + bodyOffset + ", length " + bodyLength + ". Text length is " + text.length() + "."); } @@ -3023,9 +3021,9 @@ public class MessageCompose extends K9Activity implements OnClickListener, */ private void processSourceMessageText(Message message, Integer bodyOffset, Integer bodyLength, boolean viewMessageContent) throws MessagingException { - Part textPart = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + Part textPart = message.findFirstPartByMimeType("text/plain"); if (textPart != null) { - String text = MimeUtility.getTextFromPart(textPart); + String text = textPart.getText(); if (K9.DEBUG) { Log.d(K9.LOG_TAG, "Loading message with offset " + bodyOffset + ", length " + bodyLength + ". Text length is " + text.length() + "."); } @@ -3093,7 +3091,7 @@ public class MessageCompose extends K9Activity implements OnClickListener, // Figure out which message format to use for the quoted text by looking if the source // message contains a text/html part. If it does, we use that. mQuotedTextFormat = - (MimeUtility.findFirstPartByMimeType(mSourceMessage, "text/html") == null) ? + (mSourceMessage.findFirstPartByMimeType("text/html") == null) ? SimpleMessageFormat.TEXT : SimpleMessageFormat.HTML; } else { mQuotedTextFormat = SimpleMessageFormat.HTML; @@ -3223,37 +3221,37 @@ public class MessageCompose extends K9Activity implements OnClickListener, Part part; if (format == SimpleMessageFormat.HTML) { // HTML takes precedence, then text. - part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + part = message.findFirstPartByMimeType("text/html"); if (part != null) { if (K9.DEBUG) { Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, HTML found."); } - return MimeUtility.getTextFromPart(part); + return part.getText(); } - part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + part = message.findFirstPartByMimeType("text/plain"); if (part != null) { if (K9.DEBUG) { Log.d(K9.LOG_TAG, "getBodyTextFromMessage: HTML requested, text found."); } - return HtmlConverter.textToHtml(MimeUtility.getTextFromPart(part)); + return HtmlConverter.textToHtml(part.getText()); } } else if (format == SimpleMessageFormat.TEXT) { // Text takes precedence, then html. - part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + part = message.findFirstPartByMimeType("text/plain"); if (part != null) { if (K9.DEBUG) { Log.d(K9.LOG_TAG, "getBodyTextFromMessage: Text requested, text found."); } - return MimeUtility.getTextFromPart(part); + return part.getText(); } - part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + part = message.findFirstPartByMimeType("text/html"); if (part != null) { if (K9.DEBUG) { Log.d(K9.LOG_TAG, "getBodyTextFromMessage: Text requested, HTML found."); } - return HtmlConverter.htmlToText(MimeUtility.getTextFromPart(part)); + return HtmlConverter.htmlToText(part.getText()); } } diff --git a/src/com/fsck/k9/mail/internet/TextBodyBuilder.java b/src/com/fsck/k9/activity/TextBodyBuilder.java similarity index 98% rename from src/com/fsck/k9/mail/internet/TextBodyBuilder.java rename to src/com/fsck/k9/activity/TextBodyBuilder.java index 962a80e14..4dc0a0666 100644 --- a/src/com/fsck/k9/mail/internet/TextBodyBuilder.java +++ b/src/com/fsck/k9/activity/TextBodyBuilder.java @@ -1,12 +1,14 @@ -package com.fsck.k9.mail.internet; +package com.fsck.k9.activity; import android.text.TextUtils; import android.util.Log; import com.fsck.k9.K9; import com.fsck.k9.mail.Body; +import com.fsck.k9.helper.HtmlConverter; +import com.fsck.k9.mail.internet.TextBody; -public class TextBodyBuilder { +class TextBodyBuilder { private boolean mIncludeQuotedText = true; private boolean mReplyAfterQuote = false; private boolean mSignatureBeforeQuotedText = false; diff --git a/src/com/fsck/k9/activity/setup/WelcomeMessage.java b/src/com/fsck/k9/activity/setup/WelcomeMessage.java index e40a95eb9..9c30bb0c2 100644 --- a/src/com/fsck/k9/activity/setup/WelcomeMessage.java +++ b/src/com/fsck/k9/activity/setup/WelcomeMessage.java @@ -12,7 +12,7 @@ import android.widget.TextView; import com.fsck.k9.R; import com.fsck.k9.activity.Accounts; import com.fsck.k9.activity.K9Activity; -import com.fsck.k9.mail.internet.HtmlConverter; +import com.fsck.k9.helper.HtmlConverter; /** * Displays a welcome message when no accounts have been created yet. diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 1e7e98885..76c151c8a 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -1744,7 +1744,7 @@ public class MessagingController implements Runnable { * right now, attachments will be left for later. */ - Set viewables = MimeUtility.collectTextParts(message); + Set viewables = message.collectTextParts(); /* * Now download the parts we're interested in storing. @@ -3197,7 +3197,7 @@ public class MessagingController implements Runnable { try { LocalStore localStore = account.getLocalStore(); - List attachments = MimeUtility.collectAttachments(message); + List attachments = message.collectAttachments(); for (Part attachment : attachments) { attachment.setBody(null); } @@ -4244,13 +4244,12 @@ public class MessagingController implements Runnable { try { Intent msg = new Intent(Intent.ACTION_SEND); String quotedText = null; - Part part = MimeUtility.findFirstPartByMimeType(message, - "text/plain"); + Part part = message.findFirstPartByMimeType("text/plain"); if (part == null) { - part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + part = message.findFirstPartByMimeType("text/html"); } if (part != null) { - quotedText = MimeUtility.getTextFromPart(part); + quotedText = part.getText(); } if (quotedText != null) { msg.putExtra(Intent.EXTRA_TEXT, quotedText); diff --git a/src/com/fsck/k9/crypto/CryptoHelper.java b/src/com/fsck/k9/crypto/CryptoHelper.java index 3ce2787c7..9bc421127 100644 --- a/src/com/fsck/k9/crypto/CryptoHelper.java +++ b/src/com/fsck/k9/crypto/CryptoHelper.java @@ -32,12 +32,12 @@ public class CryptoHelper { public boolean isEncrypted(Message message) { String data = null; try { - Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + Part part = message.findFirstPartByMimeType("text/plain"); if (part == null) { - part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + part = message.findFirstPartByMimeType("text/html"); } if (part != null) { - data = MimeUtility.getTextFromPart(part); + data = part.getText(); } } catch (MessagingException e) { // guess not... @@ -55,12 +55,12 @@ public class CryptoHelper { public boolean isSigned(Message message) { String data = null; try { - Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + Part part = message.findFirstPartByMimeType("text/plain"); if (part == null) { - part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + part = message.findFirstPartByMimeType("text/html"); } if (part != null) { - data = MimeUtility.getTextFromPart(part); + data = part.getText(); } } catch (MessagingException e) { // guess not... diff --git a/src/com/fsck/k9/mail/internet/HtmlConverter.java b/src/com/fsck/k9/helper/HtmlConverter.java similarity index 99% rename from src/com/fsck/k9/mail/internet/HtmlConverter.java rename to src/com/fsck/k9/helper/HtmlConverter.java index 8d28085c1..28bf875b4 100644 --- a/src/com/fsck/k9/mail/internet/HtmlConverter.java +++ b/src/com/fsck/k9/helper/HtmlConverter.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.internet; +package com.fsck.k9.helper; import android.text.*; import android.text.Html.TagHandler; diff --git a/src/com/fsck/k9/mail/BodyPart.java b/src/com/fsck/k9/mail/BodyPart.java index 1118304ba..84e4324f5 100644 --- a/src/com/fsck/k9/mail/BodyPart.java +++ b/src/com/fsck/k9/mail/BodyPart.java @@ -1,6 +1,9 @@ package com.fsck.k9.mail; +import com.fsck.k9.mail.internet.MessageExtractor; +import com.fsck.k9.mail.internet.MimeUtility; + public abstract class BodyPart implements Part { private Multipart mParent; @@ -15,5 +18,23 @@ public abstract class BodyPart implements Part { public abstract void setEncoding(String encoding) throws MessagingException; @Override - public abstract void setUsing7bitTransport() throws MessagingException; + public String getContentDisposition() { + try { + String disposition = getDisposition(); + if (disposition != null) { + return MimeUtility.getHeaderParameter(disposition, null); + } + } catch (MessagingException e) { /* ignore */ } + return null; + } + + @Override + public String getText() { + return MessageExtractor.getTextFromPart(this); + } + + @Override + public Part findFirstPartByMimeType(String mimeType) throws MessagingException { + return MimeUtility.findFirstPartByMimeType(this, mimeType); + } } diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 24a29af5f..f48eaf559 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -2,9 +2,11 @@ package com.fsck.k9.mail; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.EnumSet; +import java.util.List; import java.util.Set; import android.util.Log; @@ -12,6 +14,8 @@ import android.util.Log; import com.fsck.k9.K9; import com.fsck.k9.mail.filter.CountingOutputStream; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; +import com.fsck.k9.mail.internet.MessageExtractor; +import com.fsck.k9.mail.internet.MimeUtility; public abstract class Message implements Part, CompositeBody { @@ -265,4 +269,69 @@ public abstract class Message implements Part, CompositeBody { */ @Override public abstract Message clone(); + + /** + * Get the value of the {@code Content-Disposition} header. + * @return The value of the {@code Content-Disposition} header if available. {@code null}, otherwise. + */ + public String getContentDisposition() { + try { + String disposition = getDisposition(); + if (disposition != null) { + return MimeUtility.getHeaderParameter(disposition, null); + } + } + catch (MessagingException e) { /* ignore */ } + return null; + } + + @Override + public String getText() { + return MessageExtractor.getTextFromPart(this); + } + + @Override + public Part findFirstPartByMimeType(String mimeType) throws MessagingException { + return MimeUtility.findFirstPartByMimeType(this, mimeType); + } + + /** + * Collect attachment parts of a message. + * + * @param message + * The message to collect the attachment parts from. + * + * @return A list of parts regarded as attachments. + * + * @throws MessagingException + * In case of an error. + */ + public List collectAttachments() throws MessagingException { + try { + List attachments = new ArrayList(); + MessageExtractor.getViewables(this, attachments); + return attachments; + } catch (Exception e) { + throw new MessagingException("Couldn't collect attachment parts", e); + } + } + + /** + * Collect the viewable textual parts of a message. + * + * @param message + * The message to extract the viewable parts from. + * + * @return A set of viewable parts of the message. + * + * @throws MessagingException + * In case of an error. + */ + public Set collectTextParts() throws MessagingException { + try { + return MessageExtractor.getTextParts(this); + } catch (Exception e) { + throw new MessagingException("Couldn't extract viewable parts", e); + } + } } diff --git a/src/com/fsck/k9/mail/Multipart.java b/src/com/fsck/k9/mail/Multipart.java index 567b0902d..8bcba92b3 100644 --- a/src/com/fsck/k9/mail/Multipart.java +++ b/src/com/fsck/k9/mail/Multipart.java @@ -7,7 +7,7 @@ import java.util.List; import org.apache.james.mime4j.util.MimeUtil; -import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.internet.CharsetSupport; import com.fsck.k9.mail.internet.TextBody; public abstract class Multipart implements CompositeBody { @@ -64,7 +64,7 @@ public abstract class Multipart implements CompositeBody { BodyPart part = mParts.get(0); Body body = part.getBody(); if (body instanceof TextBody) { - MimeUtility.setCharset(charset, part); + CharsetSupport.setCharset(charset, part); ((TextBody)body).setCharset(charset); } } diff --git a/src/com/fsck/k9/mail/Part.java b/src/com/fsck/k9/mail/Part.java index 38c1ad9da..c890cd56e 100644 --- a/src/com/fsck/k9/mail/Part.java +++ b/src/com/fsck/k9/mail/Part.java @@ -5,29 +5,41 @@ import java.io.IOException; import java.io.OutputStream; public interface Part { - public void addHeader(String name, String value) throws MessagingException; + void addHeader(String name, String value) throws MessagingException; - public void removeHeader(String name) throws MessagingException; + void removeHeader(String name) throws MessagingException; - public void setHeader(String name, String value) throws MessagingException; + void setHeader(String name, String value) throws MessagingException; - public Body getBody(); + Body getBody(); - public String getContentType() throws MessagingException; + String getContentType() throws MessagingException; - public String getDisposition() throws MessagingException; + String getDisposition() throws MessagingException; - public String getContentId() throws MessagingException; + String getContentDisposition(); - public String[] getHeader(String name) throws MessagingException; + String getContentId() throws MessagingException; - public boolean isMimeType(String mimeType) throws MessagingException; + String[] getHeader(String name) throws MessagingException; - public String getMimeType() throws MessagingException; + boolean isMimeType(String mimeType) throws MessagingException; - public void setBody(Body body) throws MessagingException; + String getMimeType() throws MessagingException; - public void writeTo(OutputStream out) throws IOException, MessagingException; + void setBody(Body body) throws MessagingException; + + void writeTo(OutputStream out) throws IOException, MessagingException; + + /** + * Reads the Part's body and returns a String based on any charset conversion that needed + * to be done. Note, this does not return a text representation of HTML. + * @return a String containing the converted text in the body, or null if there was no text + * or an error during conversion. + */ + String getText(); + + Part findFirstPartByMimeType(String mimeType) throws MessagingException; /** * Called just prior to transmission, once the type of transport is known to @@ -41,5 +53,5 @@ public interface Part { * */ //TODO perhaps it would be clearer to use a flag "force7bit" in writeTo - public abstract void setUsing7bitTransport() throws MessagingException; + void setUsing7bitTransport() throws MessagingException; } diff --git a/src/com/fsck/k9/mail/internet/CharsetSupport.java b/src/com/fsck/k9/mail/internet/CharsetSupport.java new file mode 100644 index 000000000..5346566f3 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/CharsetSupport.java @@ -0,0 +1,1121 @@ +package com.fsck.k9.mail.internet; + +import android.util.Log; + +import com.fsck.k9.K9; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.util.Locale; + +import static com.fsck.k9.mail.internet.JisSupport.SHIFT_JIS; + +public class CharsetSupport { + /** + * Table for character set fall-back. + * + * Table format: unsupported charset (regular expression), fall-back charset + */ + private static final String[][] CHARSET_FALLBACK_MAP = new String[][] { + // Some Android versions don't support KOI8-U + {"koi8-u", "koi8-r"}, + {"iso-2022-jp-[\\d]+", "iso-2022-jp"}, + // Default fall-back is US-ASCII + {".*", "US-ASCII"} + }; + + + public static void setCharset(String charset, Part part) throws MessagingException { + part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, + part.getMimeType() + ";\r\n charset=" + getExternalCharset(charset)); + } + + + public static String getCharsetFromAddress(String address) { + String variant = JisSupport.getJisVariantFromAddress(address); + if (variant != null) { + String charset = "x-" + variant + "-shift_jis-2007"; + if (Charset.isSupported(charset)) + return charset; + } + + return "UTF-8"; + } + + static String getExternalCharset(String charset) { + if (JisSupport.isShiftJis(charset)) { + return SHIFT_JIS; + } else { + return charset; + } + } + + static String fixupCharset(String charset, Message message) throws MessagingException { + if (charset == null || "0".equals(charset)) + charset = "US-ASCII"; // No encoding, so use us-ascii, which is the standard. + + charset = charset.toLowerCase(Locale.US); + if (charset.equals("cp932")) + charset = SHIFT_JIS; + + if (charset.equals(SHIFT_JIS) || charset.equals("iso-2022-jp")) { + String variant = JisSupport.getJisVariantFromMessage(message); + if (variant != null) + charset = "x-" + variant + "-" + charset + "-2007"; + } + return charset; + } + + + static String readToString(InputStream in, String charset) throws IOException { + boolean isIphoneString = false; + + // iso-2022-jp variants are supported by no versions as of Dec 2010. + if (charset.length() > 19 && charset.startsWith("x-") && + charset.endsWith("-iso-2022-jp-2007") && !Charset.isSupported(charset)) { + in = new Iso2022JpToShiftJisInputStream(in); + charset = "x-" + charset.substring(2, charset.length() - 17) + "-shift_jis-2007"; + } + + // shift_jis variants are supported by Eclair and later. + if (JisSupport.isShiftJis(charset) && !Charset.isSupported(charset)) { + // If the JIS variant is iPhone, map the Unicode private use area in iPhone to the one in Android after + // converting the character set from the standard Shift JIS to Unicode. + if (charset.substring(2, charset.length() - 15).equals("iphone")) + isIphoneString = true; + + charset = SHIFT_JIS; + } + + /* + * See if there is conversion from the MIME charset to the Java one. + * this function may also throw an exception if the charset name is not known + */ + boolean supported; + try { + supported = Charset.isSupported(charset); + } catch (IllegalCharsetNameException e) { + supported = false; + } + + for (String[] rule: CHARSET_FALLBACK_MAP) { + if (supported) { + break; + } + + if (charset.matches(rule[0])) { + Log.e(K9.LOG_TAG, "I don't know how to deal with the charset " + charset + + ". Falling back to " + rule[1]); + charset = rule[1]; + try { + supported = Charset.isSupported(charset); + } catch (IllegalCharsetNameException e) { + supported = false; + } + } + } + + /* + * Convert and return as new String + */ + String str = IOUtils.toString(in, charset); + + if (isIphoneString) + str = importStringFromIphone(str); + return str; + } + + private static String importStringFromIphone(String str) { + StringBuilder buff = new StringBuilder(str.length()); + for (int i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) { + int codePoint = str.codePointAt(i); + buff.appendCodePoint(importCodePointFromIphone(codePoint)); + } + return buff.toString(); + } + + private static int importCodePointFromIphone(int codePoint) { + switch (codePoint) { + case 0xE001: + return 0xFE19B; + case 0xE002: + return 0xFE19C; + case 0xE003: + return 0xFE823; + case 0xE004: + return 0xFE19D; + case 0xE005: + return 0xFE19E; + case 0xE006: + return 0xFE4CF; + case 0xE007: + return 0xFE4CD; + case 0xE008: + return 0xFE4EF; + case 0xE009: + return 0xFE523; + case 0xE00A: + return 0xFE525; + case 0xE00B: + return 0xFE528; + case 0xE00C: + return 0xFE538; + case 0xE00D: + return 0xFEB96; + case 0xE00E: + return 0xFEB97; + case 0xE00F: + return 0xFEB98; + case 0xE010: + return 0xFEB93; + case 0xE011: + return 0xFEB94; + case 0xE012: + return 0xFEB95; + case 0xE013: + return 0xFE7D5; + case 0xE014: + return 0xFE7D2; + case 0xE015: + return 0xFE7D3; + case 0xE016: + return 0xFE7D1; + case 0xE017: + return 0xFE7DA; + case 0xE018: + return 0xFE7D4; + case 0xE019: + return 0xFE1BD; + case 0xE01A: + return 0xFE1BE; + case 0xE01B: + return 0xFE7E4; + case 0xE01C: + return 0xFE7EA; + case 0xE01D: + return 0xFE7E9; + case 0xE01E: + return 0xFE7DF; + case 0xE01F: + return 0xFE7E3; + case 0xE020: + return 0xFEB09; + case 0xE021: + return 0xFEB04; + case 0xE022: + return 0xFEB0C; + case 0xE023: + return 0xFEB0E; + case 0xE024: + return 0xFE01E; + case 0xE025: + return 0xFE01F; + case 0xE026: + return 0xFE020; + case 0xE027: + return 0xFE021; + case 0xE028: + return 0xFE022; + case 0xE029: + return 0xFE023; + case 0xE02A: + return 0xFE024; + case 0xE02B: + return 0xFE025; + case 0xE02C: + return 0xFE026; + case 0xE02D: + return 0xFE027; + case 0xE02E: + return 0xFE028; + case 0xE02F: + return 0xFE029; + case 0xE030: + return 0xFE040; + case 0xE031: + return 0xFE4D2; + case 0xE032: + return 0xFE041; + case 0xE033: + return 0xFE512; + case 0xE034: + return 0xFE825; + case 0xE035: + return 0xFE826; + case 0xE036: + return 0xFE4B0; + case 0xE037: + return 0xFE4BB; + case 0xE038: + return 0xFE4B2; + case 0xE039: + return 0xFE7EC; + case 0xE03A: + return 0xFE7F5; + case 0xE03B: + return 0xFE4C3; + case 0xE03C: + return 0xFE800; + case 0xE03D: + return 0xFE801; + case 0xE03E: + return 0xFE813; + case 0xE03F: + return 0xFEB82; + case 0xE040: + return 0xFE815; + case 0xE041: + return 0xFE816; + case 0xE042: + return 0xFE818; + case 0xE043: + return 0xFE980; + case 0xE044: + return 0xFE982; + case 0xE045: + return 0xFE981; + case 0xE046: + return 0xFE962; + case 0xE047: + return 0xFE983; + case 0xE048: + return 0xFE003; + case 0xE049: + return 0xFE001; + case 0xE04A: + return 0xFE000; + case 0xE04B: + return 0xFE002; + case 0xE04C: + return 0xFE014; + case 0xE04D: + return 0xFE009; + case 0xE04E: + return 0xFE1AF; + case 0xE04F: + return 0xFE1B8; + case 0xE050: + return 0xFE1C0; + case 0xE051: + return 0xFE1C1; + case 0xE052: + return 0xFE1B7; + case 0xE053: + return 0xFE1C2; + case 0xE054: + return 0xFE1C3; + case 0xE055: + return 0xFE1BC; + case 0xE056: + return 0xFE335; + case 0xE057: + return 0xFE330; + case 0xE058: + return 0xFE323; + case 0xE059: + return 0xFE320; + case 0xE05A: + return 0xFE4F4; + case 0xE101: + return 0xFE52D; + case 0xE102: + return 0xFE52E; + case 0xE103: + return 0xFE52B; + case 0xE104: + return 0xFE526; + case 0xE105: + return 0xFE329; + case 0xE106: + return 0xFE327; + case 0xE107: + return 0xFE341; + case 0xE108: + return 0xFE344; + case 0xE109: + return 0xFE1C4; + case 0xE10A: + return 0xFE1C5; + case 0xE10B: + return 0xFE1BF; + case 0xE10C: + return 0xFE1B0; + case 0xE10D: + return 0xFE7ED; + case 0xE10E: + return 0xFE4D1; + case 0xE10F: + return 0xFEB56; + case 0xE110: + return 0xFE03C; + case 0xE111: + return 0xFE827; + case 0xE112: + return 0xFE510; + case 0xE113: + return 0xFE4F5; + case 0xE114: + return 0xFEB85; + case 0xE115: + return 0xFE7D9; + case 0xE116: + return 0xFE4CA; + case 0xE117: + return 0xFE515; + case 0xE118: + return 0xFE03F; + case 0xE119: + return 0xFE042; + case 0xE11A: + return 0xFE1B2; + case 0xE11B: + return 0xFE1AE; + case 0xE11C: + return 0xFE1B3; + case 0xE11D: + return 0xFE4F6; + case 0xE11E: + return 0xFE53B; + case 0xE11F: + return 0xFE537; + case 0xE120: + return 0xFE960; + case 0xE121: + return 0xFE4BC; + case 0xE122: + return 0xFE7FB; + case 0xE123: + return 0xFE7FA; + case 0xE124: + return 0xFE7FD; + case 0xE125: + return 0xFE807; + case 0xE126: + return 0xFE81D; + case 0xE127: + return 0xFE81E; + case 0xE128: + return 0xFE81F; + case 0xE129: + return 0xFE820; + case 0xE12A: + return 0xFE81C; + case 0xE12B: + return 0xFE1B1; + case 0xE12C: + return 0xFE81B; + case 0xE12D: + return 0xFE80B; + case 0xE12E: + return 0xFEB32; + case 0xE12F: + return 0xFE4DD; + case 0xE130: + return 0xFE80C; + case 0xE131: + return 0xFE7DB; + case 0xE132: + return 0xFE7D7; + case 0xE133: + return 0xFE80D; + case 0xE134: + return 0xFE7DC; + case 0xE135: + return 0xFE7EE; + case 0xE136: + return 0xFE7EB; + case 0xE137: + return 0xFE7F8; + case 0xE138: + return 0xFEB33; + case 0xE139: + return 0xFEB34; + case 0xE13A: + return 0xFEB35; + case 0xE13B: + return 0xFE509; + case 0xE13C: + return 0xFEB59; + case 0xE13D: + return 0xFE004; + case 0xE13E: + return 0xFE4D6; + case 0xE13F: + return 0xFE505; + case 0xE140: + return 0xFE507; + case 0xE141: + return 0xFE821; + case 0xE142: + return 0xFE52F; + case 0xE143: + return 0xFE514; + case 0xE144: + return 0xFEB86; + case 0xE145: + return 0xFEB87; + case 0xE146: + return 0xFE00B; + case 0xE147: + return 0xFE965; + case 0xE148: + return 0xFE546; + case 0xE149: + return 0xFE4DE; + case 0xE14A: + return 0xFE4DF; + case 0xE14B: + return 0xFE531; + case 0xE14C: + return 0xFEB5E; + case 0xE14D: + return 0xFE4B5; + case 0xE14E: + return 0xFE7F7; + case 0xE14F: + return 0xFE7F6; + case 0xE150: + return 0xFE7E7; + case 0xE151: + return 0xFE506; + case 0xE152: + return 0xFE1A1; + case 0xE153: + return 0xFE4B3; + case 0xE154: + return 0xFE4B6; + case 0xE155: + return 0xFE4B4; + case 0xE156: + return 0xFE4B9; + case 0xE157: + return 0xFE4BA; + case 0xE158: + return 0xFE4B7; + case 0xE159: + return 0xFE7E6; + case 0xE15A: + return 0xFE7EF; + case 0xE201: + return 0xFE7F0; + case 0xE202: + return 0xFE7E8; + case 0xE203: + return 0xFEB24; + case 0xE204: + return 0xFEB19; + case 0xE205: + return 0xFEB61; + case 0xE206: + return 0xFEB62; + case 0xE207: + return 0xFEB25; + case 0xE208: + return 0xFEB1F; + case 0xE209: + return 0xFE044; + case 0xE20A: + return 0xFEB20; + case 0xE20B: + return 0xFE838; + case 0xE20C: + return 0xFEB1A; + case 0xE20D: + return 0xFEB1C; + case 0xE20E: + return 0xFEB1B; + case 0xE20F: + return 0xFEB1D; + case 0xE210: + return 0xFE82C; + case 0xE211: + return 0xFE82B; + case 0xE212: + return 0xFEB36; + case 0xE213: + return 0xFEB37; + case 0xE214: + return 0xFEB38; + case 0xE215: + return 0xFEB39; + case 0xE216: + return 0xFEB3A; + case 0xE217: + return 0xFEB3B; + case 0xE218: + return 0xFEB3C; + case 0xE219: + return 0xFEB63; + case 0xE21A: + return 0xFEB64; + case 0xE21B: + return 0xFEB67; + case 0xE21C: + return 0xFE82E; + case 0xE21D: + return 0xFE82F; + case 0xE21E: + return 0xFE830; + case 0xE21F: + return 0xFE831; + case 0xE220: + return 0xFE832; + case 0xE221: + return 0xFE833; + case 0xE222: + return 0xFE834; + case 0xE223: + return 0xFE835; + case 0xE224: + return 0xFE836; + case 0xE225: + return 0xFE837; + case 0xE226: + return 0xFEB3D; + case 0xE227: + return 0xFEB3E; + case 0xE228: + return 0xFEB3F; + case 0xE229: + return 0xFEB81; + case 0xE22A: + return 0xFEB31; + case 0xE22B: + return 0xFEB2F; + case 0xE22C: + return 0xFEB40; + case 0xE22D: + return 0xFEB41; + case 0xE22E: + return 0xFEB99; + case 0xE22F: + return 0xFEB9A; + case 0xE230: + return 0xFEB9B; + case 0xE231: + return 0xFEB9C; + case 0xE232: + return 0xFEAF8; + case 0xE233: + return 0xFEAF9; + case 0xE234: + return 0xFEAFA; + case 0xE235: + return 0xFEAFB; + case 0xE236: + return 0xFEAF0; + case 0xE237: + return 0xFEAF2; + case 0xE238: + return 0xFEAF1; + case 0xE239: + return 0xFEAF3; + case 0xE23A: + return 0xFEAFC; + case 0xE23B: + return 0xFEAFD; + case 0xE23C: + return 0xFEAFE; + case 0xE23D: + return 0xFEAFF; + case 0xE23E: + return 0xFE4F8; + case 0xE23F: + return 0xFE02B; + case 0xE240: + return 0xFE02C; + case 0xE241: + return 0xFE02D; + case 0xE242: + return 0xFE02E; + case 0xE243: + return 0xFE02F; + case 0xE244: + return 0xFE030; + case 0xE245: + return 0xFE031; + case 0xE246: + return 0xFE032; + case 0xE247: + return 0xFE033; + case 0xE248: + return 0xFE034; + case 0xE249: + return 0xFE035; + case 0xE24A: + return 0xFE036; + case 0xE24B: + return 0xFE037; + case 0xE24C: + return 0xFEB42; + case 0xE24D: + return 0xFEB27; + case 0xE24E: + return 0xFEB29; + case 0xE24F: + return 0xFEB2D; + case 0xE250: + return 0xFE839; + case 0xE251: + return 0xFE83A; + case 0xE252: + return 0xFEB23; + case 0xE253: + return 0xFE1B4; + case 0xE254: + return 0xFEE77; + case 0xE255: + return 0xFEE78; + case 0xE256: + return 0xFEE79; + case 0xE257: + return 0xFEE7A; + case 0xE258: + return 0xFEE7B; + case 0xE259: + return 0xFEE7C; + case 0xE25A: + return 0xFEE7D; + case 0xE301: + return 0xFE527; + case 0xE302: + return 0xFE4D3; + case 0xE303: + return 0xFE045; + case 0xE304: + return 0xFE03D; + case 0xE305: + return 0xFE046; + case 0xE306: + return 0xFE828; + case 0xE307: + return 0xFE047; + case 0xE308: + return 0xFE048; + case 0xE309: + return 0xFE508; + case 0xE30A: + return 0xFE803; + case 0xE30B: + return 0xFE985; + case 0xE30C: + return 0xFE987; + case 0xE30D: + return 0xFEB43; + case 0xE30E: + return 0xFEB1E; + case 0xE30F: + return 0xFE50A; + case 0xE310: + return 0xFE516; + case 0xE311: + return 0xFEB58; + case 0xE312: + return 0xFE517; + case 0xE313: + return 0xFE53E; + case 0xE314: + return 0xFE50F; + case 0xE315: + return 0xFEB2B; + case 0xE316: + return 0xFE53C; + case 0xE317: + return 0xFE530; + case 0xE318: + return 0xFE4D4; + case 0xE319: + return 0xFE4D5; + case 0xE31A: + return 0xFE4D7; + case 0xE31B: + return 0xFE4D8; + case 0xE31C: + return 0xFE195; + case 0xE31D: + return 0xFE196; + case 0xE31E: + return 0xFE197; + case 0xE31F: + return 0xFE198; + case 0xE320: + return 0xFE199; + case 0xE321: + return 0xFE4D9; + case 0xE322: + return 0xFE4DA; + case 0xE323: + return 0xFE4F0; + case 0xE324: + return 0xFE808; + case 0xE325: + return 0xFE4F2; + case 0xE326: + return 0xFE814; + case 0xE327: + return 0xFEB0D; + case 0xE328: + return 0xFEB11; + case 0xE329: + return 0xFEB12; + case 0xE32A: + return 0xFEB13; + case 0xE32B: + return 0xFEB14; + case 0xE32C: + return 0xFEB15; + case 0xE32D: + return 0xFEB16; + case 0xE32E: + return 0xFEB60; + case 0xE32F: + return 0xFEB68; + case 0xE330: + return 0xFEB5D; + case 0xE331: + return 0xFEB5B; + case 0xE332: + return 0xFEB44; + case 0xE333: + return 0xFEB45; + case 0xE334: + return 0xFEB57; + case 0xE335: + return 0xFEB69; + case 0xE336: + return 0xFEB0A; + case 0xE337: + return 0xFEB0B; + case 0xE338: + return 0xFE984; + case 0xE339: + return 0xFE964; + case 0xE33A: + return 0xFE966; + case 0xE33B: + return 0xFE967; + case 0xE33C: + return 0xFE968; + case 0xE33D: + return 0xFE969; + case 0xE33E: + return 0xFE96A; + case 0xE33F: + return 0xFE96B; + case 0xE340: + return 0xFE963; + case 0xE341: + return 0xFE96C; + case 0xE342: + return 0xFE961; + case 0xE343: + return 0xFE96D; + case 0xE344: + return 0xFE96E; + case 0xE345: + return 0xFE051; + case 0xE346: + return 0xFE052; + case 0xE347: + return 0xFE053; + case 0xE348: + return 0xFE054; + case 0xE349: + return 0xFE055; + case 0xE34A: + return 0xFE056; + case 0xE34B: + return 0xFE511; + case 0xE34C: + return 0xFE96F; + case 0xE34D: + return 0xFE970; + case 0xE401: + return 0xFE345; + case 0xE402: + return 0xFE343; + case 0xE403: + return 0xFE340; + case 0xE404: + return 0xFE333; + case 0xE405: + return 0xFE347; + case 0xE406: + return 0xFE33C; + case 0xE407: + return 0xFE33F; + case 0xE408: + return 0xFE342; + case 0xE409: + return 0xFE32A; + case 0xE40A: + return 0xFE33E; + case 0xE40B: + return 0xFE33B; + case 0xE40C: + return 0xFE32E; + case 0xE40D: + return 0xFE32F; + case 0xE40E: + return 0xFE326; + case 0xE40F: + return 0xFE325; + case 0xE410: + return 0xFE322; + case 0xE411: + return 0xFE33A; + case 0xE412: + return 0xFE334; + case 0xE413: + return 0xFE339; + case 0xE414: + return 0xFE336; + case 0xE415: + return 0xFE338; + case 0xE416: + return 0xFE33D; + case 0xE417: + return 0xFE32D; + case 0xE418: + return 0xFE32C; + case 0xE419: + return 0xFE190; + case 0xE41A: + return 0xFE192; + case 0xE41B: + return 0xFE191; + case 0xE41C: + return 0xFE193; + case 0xE41D: + return 0xFE35B; + case 0xE41E: + return 0xFEB9D; + case 0xE41F: + return 0xFEB9E; + case 0xE420: + return 0xFEB9F; + case 0xE421: + return 0xFEBA0; + case 0xE422: + return 0xFEBA1; + case 0xE423: + return 0xFE351; + case 0xE424: + return 0xFE352; + case 0xE425: + return 0xFE829; + case 0xE426: + return 0xFE353; + case 0xE427: + return 0xFE358; + case 0xE428: + return 0xFE1A0; + case 0xE429: + return 0xFE1A2; + case 0xE42A: + return 0xFE7D6; + case 0xE42B: + return 0xFE7DD; + case 0xE42C: + return 0xFE80E; + case 0xE42D: + return 0xFE7DE; + case 0xE42E: + return 0xFE7E5; + case 0xE42F: + return 0xFE7F1; + case 0xE430: + return 0xFE7F2; + case 0xE431: + return 0xFE7F3; + case 0xE432: + return 0xFE7F4; + case 0xE433: + return 0xFE7FE; + case 0xE434: + return 0xFE7E0; + case 0xE435: + return 0xFE7E2; + case 0xE436: + return 0xFE518; + case 0xE437: + return 0xFEB17; + case 0xE438: + return 0xFE519; + case 0xE439: + return 0xFE51A; + case 0xE43A: + return 0xFE51B; + case 0xE43B: + return 0xFE51C; + case 0xE43C: + return 0xFE007; + case 0xE43D: + return 0xFE82A; + case 0xE43E: + return 0xFE038; + case 0xE43F: + return 0xFE971; + case 0xE440: + return 0xFE51D; + case 0xE441: + return 0xFE1C6; + case 0xE442: + return 0xFE51E; + case 0xE443: + return 0xFE005; + case 0xE444: + return 0xFE049; + case 0xE445: + return 0xFE51F; + case 0xE446: + return 0xFE017; + case 0xE447: + return 0xFE043; + case 0xE448: + return 0xFE513; + case 0xE449: + return 0xFE00A; + case 0xE44A: + return 0xFE00C; + case 0xE44B: + return 0xFE008; + case 0xE44C: + return 0xFE00D; + case 0xE501: + return 0xFE4B8; + case 0xE502: + return 0xFE804; + case 0xE503: + return 0xFE805; + case 0xE504: + return 0xFE4BD; + case 0xE505: + return 0xFE4BE; + case 0xE506: + return 0xFE4BF; + case 0xE507: + return 0xFE802; + case 0xE508: + return 0xFE4C0; + case 0xE509: + return 0xFE4C4; + case 0xE50A: + return 0xFE4C5; + case 0xE50B: + return 0xFE4E5; + case 0xE50C: + return 0xFE4E6; + case 0xE50D: + return 0xFE4E7; + case 0xE50E: + return 0xFE4E8; + case 0xE50F: + return 0xFE4E9; + case 0xE510: + return 0xFE4EA; + case 0xE511: + return 0xFE4EB; + case 0xE512: + return 0xFE4EC; + case 0xE513: + return 0xFE4ED; + case 0xE514: + return 0xFE4EE; + case 0xE515: + return 0xFE1A4; + case 0xE516: + return 0xFE1A5; + case 0xE517: + return 0xFE1A6; + case 0xE518: + return 0xFE1A7; + case 0xE519: + return 0xFE1A8; + case 0xE51A: + return 0xFE1A9; + case 0xE51B: + return 0xFE1AA; + case 0xE51C: + return 0xFE1AB; + case 0xE51D: + return 0xFE4C6; + case 0xE51E: + return 0xFE1B5; + case 0xE51F: + return 0xFE1B6; + case 0xE520: + return 0xFE1C7; + case 0xE521: + return 0xFE1C8; + case 0xE522: + return 0xFE1C9; + case 0xE523: + return 0xFE1BA; + case 0xE524: + return 0xFE1CA; + case 0xE525: + return 0xFE1CB; + case 0xE526: + return 0xFE1CC; + case 0xE527: + return 0xFE1CD; + case 0xE528: + return 0xFE1CE; + case 0xE529: + return 0xFE1CF; + case 0xE52A: + return 0xFE1D0; + case 0xE52B: + return 0xFE1D1; + case 0xE52C: + return 0xFE1D2; + case 0xE52D: + return 0xFE1D3; + case 0xE52E: + return 0xFE1D4; + case 0xE52F: + return 0xFE1D5; + case 0xE530: + return 0xFE1D6; + case 0xE531: + return 0xFE1D7; + case 0xE532: + return 0xFE50B; + case 0xE533: + return 0xFE50C; + case 0xE534: + return 0xFE50D; + case 0xE535: + return 0xFE50E; + case 0xE536: + return 0xFE553; + case 0xE537: + return 0xFEB2A; + case 0xE538: + return 0xFEE70; + case 0xE539: + return 0xFEE71; + case 0xE53A: + return 0xFEE72; + case 0xE53B: + return 0xFEE73; + case 0xE53C: + return 0xFEE74; + case 0xE53D: + return 0xFEE75; + case 0xE53E: + return 0xFEE76; + default: + return codePoint; + } + } + +} diff --git a/src/com/fsck/k9/mail/internet/DecoderUtil.java b/src/com/fsck/k9/mail/internet/DecoderUtil.java index 3ac74947d..3e439ab99 100644 --- a/src/com/fsck/k9/mail/internet/DecoderUtil.java +++ b/src/com/fsck/k9/mail/internet/DecoderUtil.java @@ -35,7 +35,7 @@ class DecoderUtil { Base64InputStream is = new Base64InputStream(new ByteArrayInputStream(bytes)); try { - return MimeUtility.readToString(is, charset); + return CharsetSupport.readToString(is, charset); } catch (IOException e) { return null; } @@ -68,7 +68,7 @@ class DecoderUtil { QuotedPrintableInputStream is = new QuotedPrintableInputStream(new ByteArrayInputStream(bytes)); try { - return MimeUtility.readToString(is, charset); + return CharsetSupport.readToString(is, charset); } catch (IOException e) { return null; } @@ -162,7 +162,7 @@ class DecoderUtil { String charset; try { - charset = MimeUtility.fixupCharset(mimeCharset, message); + charset = CharsetSupport.fixupCharset(mimeCharset, message); } catch (MessagingException e) { return null; } diff --git a/src/com/fsck/k9/mail/internet/EncoderUtil.java b/src/com/fsck/k9/mail/internet/EncoderUtil.java index 5e7672715..2c71ecc2d 100644 --- a/src/com/fsck/k9/mail/internet/EncoderUtil.java +++ b/src/com/fsck/k9/mail/internet/EncoderUtil.java @@ -68,7 +68,7 @@ class EncoderUtil { if (charset == null) charset = determineCharset(text); - String mimeCharset = MimeUtility.getExternalCharset(charset.name()); + String mimeCharset = CharsetSupport.getExternalCharset(charset.name()); byte[] bytes = encode(text, charset); diff --git a/src/com/fsck/k9/mail/internet/JisSupport.java b/src/com/fsck/k9/mail/internet/JisSupport.java new file mode 100644 index 000000000..a6a30329a --- /dev/null +++ b/src/com/fsck/k9/mail/internet/JisSupport.java @@ -0,0 +1,103 @@ +package com.fsck.k9.mail.internet; + +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; + +class JisSupport { + public static final String SHIFT_JIS = "shift_jis"; + + public static String getJisVariantFromMessage(Message message) throws MessagingException { + if (message == null) + return null; + + // If a receiver is known to use a JIS variant, the sender transfers the message after converting the + // charset as a convention. + String variant = getJisVariantFromReceivedHeaders(message); + if (variant != null) + return variant; + + // If a receiver is not known to use any JIS variants, the sender transfers the message without converting + // the charset. + variant = getJisVariantFromFromHeaders(message); + if (variant != null) + return variant; + + return getJisVariantFromMailerHeaders(message); + } + + public static boolean isShiftJis(String charset) { + return charset.length() > 17 && charset.startsWith("x-") + && charset.endsWith("-shift_jis-2007"); + } + + public static String getJisVariantFromAddress(String address) { + if (address == null) + return null; + if (isInDomain(address, "docomo.ne.jp") || isInDomain(address, "dwmail.jp") || + isInDomain(address, "pdx.ne.jp") || isInDomain(address, "willcom.com") || + isInDomain(address, "emnet.ne.jp") || isInDomain(address, "emobile.ne.jp")) + return "docomo"; + else if (isInDomain(address, "softbank.ne.jp") || isInDomain(address, "vodafone.ne.jp") || + isInDomain(address, "disney.ne.jp") || isInDomain(address, "vertuclub.ne.jp")) + return "softbank"; + else if (isInDomain(address, "ezweb.ne.jp") || isInDomain(address, "ido.ne.jp")) + return "kddi"; + return null; + } + + + private static String getJisVariantFromMailerHeaders(Message message) throws MessagingException { + String mailerHeaders[] = message.getHeader("X-Mailer"); + if (mailerHeaders == null || mailerHeaders.length == 0) + return null; + + if (mailerHeaders[0].startsWith("iPhone Mail ") || mailerHeaders[0].startsWith("iPad Mail ")) + return "iphone"; + + return null; + } + + + private static String getJisVariantFromReceivedHeaders(Part message) throws MessagingException { + String receivedHeaders[] = message.getHeader("Received"); + if (receivedHeaders == null) + return null; + + for (String receivedHeader : receivedHeaders) { + String address = getAddressFromReceivedHeader(receivedHeader); + if (address == null) + continue; + String variant = getJisVariantFromAddress(address); + if (variant != null) + return variant; + } + return null; + } + + private static String getAddressFromReceivedHeader(String receivedHeader) { + // Not implemented yet! Extract an address from the FOR clause of the given Received header. + return null; + } + + private static String getJisVariantFromFromHeaders(Message message) throws MessagingException { + Address addresses[] = message.getFrom(); + if (addresses == null || addresses.length == 0) + return null; + + return getJisVariantFromAddress(addresses[0].getAddress()); + } + + private static boolean isInDomain(String address, String domain) { + int index = address.length() - domain.length() - 1; + if (index < 0) + return false; + + char c = address.charAt(index); + if (c != '@' && c != '.') + return false; + + return address.endsWith(domain); + } +} diff --git a/src/com/fsck/k9/mail/internet/MessageExtractor.java b/src/com/fsck/k9/mail/internet/MessageExtractor.java new file mode 100644 index 000000000..5fedd3d28 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/MessageExtractor.java @@ -0,0 +1,410 @@ +package com.fsck.k9.mail.internet; + +import android.util.Log; + +import com.fsck.k9.K9; +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 java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.fsck.k9.mail.internet.CharsetSupport.fixupCharset; +import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter; +import static com.fsck.k9.mail.internet.Viewable.Alternative; +import static com.fsck.k9.mail.internet.Viewable.Textual; + +public class MessageExtractor { + public static String getTextFromPart(Part part) { + try { + if ((part != null) && (part.getBody() != null)) { + final Body body = part.getBody(); + if (body instanceof TextBody) { + return ((TextBody)body).getText(); + } + + final String mimeType = part.getMimeType(); + if ((mimeType != null) && MimeUtility.mimeTypeMatches(mimeType, "text/*")) { + /* + * We've got a text part, so let's see if it needs to be processed further. + */ + String charset = getHeaderParameter(part.getContentType(), "charset"); + /* + * determine the charset from HTML message. + */ + if (mimeType.equalsIgnoreCase("text/html") && charset == null) { + InputStream in = part.getBody().getInputStream(); + try { + byte[] buf = new byte[256]; + in.read(buf, 0, buf.length); + String str = new String(buf, "US-ASCII"); + + if (str.isEmpty()) { + return ""; + } + Pattern p = Pattern.compile("", Pattern.CASE_INSENSITIVE); + Matcher m = p.matcher(str); + if (m.find()) { + charset = m.group(1); + } + } finally { + try { + if (in instanceof BinaryTempFileBody.BinaryTempFileBodyInputStream) { + /* + * If this is a BinaryTempFileBodyInputStream, calling close() + * will delete the file. But we can't let that happen because + * the file needs to be opened again by the code a few lines + * down. + */ + ((BinaryTempFileBody.BinaryTempFileBodyInputStream) in).closeWithoutDeleting(); + } else { + in.close(); + } + } catch (Exception e) { /* ignore */ } + } + } + charset = fixupCharset(charset, getMessageFromPart(part)); + + /* + * Now we read the part into a buffer for further processing. Because + * the stream is now wrapped we'll remove any transfer encoding at this point. + */ + InputStream in = part.getBody().getInputStream(); + try { + String text = CharsetSupport.readToString(in, charset); + + // Replace the body with a TextBody that already contains the decoded text + part.setBody(new TextBody(text)); + + return text; + } finally { + try { + /* + * This time we don't care if it's a BinaryTempFileBodyInputStream. We + * replaced the body with a TextBody instance and hence don't need the + * file anymore. + */ + in.close(); + } catch (IOException e) { /* Ignore */ } + } + } + } + + } catch (OutOfMemoryError oom) { + /* + * If we are not able to process the body there's nothing we can do about it. Return + * null and let the upper layers handle the missing content. + */ + Log.e(K9.LOG_TAG, "Unable to getTextFromPart " + oom.toString()); + } catch (Exception e) { + /* + * If we are not able to process the body there's nothing we can do about it. Return + * null and let the upper layers handle the missing content. + */ + Log.e(K9.LOG_TAG, "Unable to getTextFromPart", e); + } + return null; + } + + + /** + * Traverse the MIME tree of a message an extract viewable parts. + * + * @param part + * The message part to start from. + * @param attachments + * A list that will receive the parts that are considered attachments. + * + * @return A list of {@link Viewable}s. + * + * @throws MessagingException + * In case of an error. + */ + public static List getViewables(Part part, List attachments) throws MessagingException { + List viewables = new ArrayList(); + + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart multipart = (Multipart) body; + if (part.getMimeType().equalsIgnoreCase("multipart/alternative")) { + /* + * For multipart/alternative parts we try to find a text/plain and a text/html + * child. Everything else we find is put into 'attachments'. + */ + List text = findTextPart(multipart, true); + + Set knownTextParts = getParts(text); + List html = findHtmlPart(multipart, knownTextParts, attachments, true); + + if (!text.isEmpty() || !html.isEmpty()) { + Alternative alternative = new Alternative(text, html); + viewables.add(alternative); + } + } else { + // For all other multipart parts we recurse to grab all viewable children. + for (Part bodyPart : multipart.getBodyParts()) { + viewables.addAll(getViewables(bodyPart, attachments)); + } + } + } else if (body instanceof Message && + !("attachment".equalsIgnoreCase(part.getContentDisposition()))) { + /* + * We only care about message/rfc822 parts whose Content-Disposition header has a value + * other than "attachment". + */ + Message message = (Message) body; + + // We add the Message object so we can extract the filename later. + viewables.add(new Viewable.MessageHeader(part, message)); + + // Recurse to grab all viewable parts and attachments from that message. + viewables.addAll(getViewables(message, attachments)); + } else if (isPartTextualBody(part)) { + /* + * Save text/plain and text/html + */ + String mimeType = part.getMimeType(); + if (mimeType.equalsIgnoreCase("text/plain")) { + Viewable.Text text = new Viewable.Text(part); + viewables.add(text); + } else { + Viewable.Html html = new Viewable.Html(part); + viewables.add(html); + } + } else { + // Everything else is treated as attachment. + attachments.add(part); + } + + return viewables; + } + + public static Set getTextParts(Part part) throws MessagingException { + List attachments = new ArrayList(); + return getParts(getViewables(part, attachments)); + } + + private static Message getMessageFromPart(Part part) { + while (part != null) { + if (part instanceof Message) + return (Message)part; + + if (!(part instanceof BodyPart)) + return null; + + Multipart multipart = ((BodyPart)part).getParent(); + if (multipart == null) + return null; + + part = multipart.getParent(); + } + return null; + } + + /** + * Search the children of a {@link Multipart} for {@code text/plain} parts. + * + * @param multipart The {@code Multipart} to search through. + * @param directChild If {@code true}, this method will return after the first {@code text/plain} was + * found. + * + * @return A list of {@link Viewable.Text} viewables. + * + * @throws MessagingException + * In case of an error. + */ + private static List findTextPart(Multipart multipart, boolean directChild) + throws MessagingException { + List viewables = new ArrayList(); + + for (Part part : multipart.getBodyParts()) { + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart innerMultipart = (Multipart) body; + + /* + * Recurse to find text parts. Since this is a multipart that is a child of a + * multipart/alternative we don't want to stop after the first text/plain part + * we find. This will allow to get all text parts for constructions like this: + * + * 1. multipart/alternative + * 1.1. multipart/mixed + * 1.1.1. text/plain + * 1.1.2. text/plain + * 1.2. text/html + */ + List textViewables = findTextPart(innerMultipart, false); + + if (!textViewables.isEmpty()) { + viewables.addAll(textViewables); + if (directChild) { + break; + } + } + } else if (isPartTextualBody(part) && part.getMimeType().equalsIgnoreCase("text/plain")) { + Viewable.Text text = new Viewable.Text(part); + viewables.add(text); + if (directChild) { + break; + } + } + } + return viewables; + } + + /** + * Search the children of a {@link Multipart} for {@code text/html} parts. + * Every part that is not a {@code text/html} we want to display, we add to 'attachments'. + * + * @param multipart The {@code Multipart} to search through. + * @param knownTextParts A set of {@code text/plain} parts that shouldn't be added to 'attachments'. + * @param attachments A list that will receive the parts that are considered attachments. + * @param directChild If {@code true}, this method will add all {@code text/html} parts except the first + * found to 'attachments'. + * + * @return A list of {@link Viewable.Text} viewables. + * + * @throws MessagingException In case of an error. + */ + private static List findHtmlPart(Multipart multipart, Set knownTextParts, + List attachments, boolean directChild) throws MessagingException { + List viewables = new ArrayList(); + + boolean partFound = false; + for (Part part : multipart.getBodyParts()) { + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart innerMultipart = (Multipart) body; + + if (directChild && partFound) { + // We already found our text/html part. Now we're only looking for attachments. + findAttachments(innerMultipart, knownTextParts, attachments); + } else { + /* + * Recurse to find HTML parts. Since this is a multipart that is a child of a + * multipart/alternative we don't want to stop after the first text/html part + * we find. This will allow to get all text parts for constructions like this: + * + * 1. multipart/alternative + * 1.1. text/plain + * 1.2. multipart/mixed + * 1.2.1. text/html + * 1.2.2. text/html + * 1.3. image/jpeg + */ + List htmlViewables = findHtmlPart(innerMultipart, knownTextParts, + attachments, false); + + if (!htmlViewables.isEmpty()) { + partFound = true; + viewables.addAll(htmlViewables); + } + } + } else if (!(directChild && partFound) && isPartTextualBody(part) && + part.getMimeType().equalsIgnoreCase("text/html")) { + Viewable.Html html = new Viewable.Html(part); + viewables.add(html); + partFound = true; + } else if (!knownTextParts.contains(part)) { + // Only add this part as attachment if it's not a viewable text/plain part found + // earlier. + attachments.add(part); + } + } + + return viewables; + } + + /** + * Traverse the MIME tree and add everything that's not a known text part to 'attachments'. + * + * @param multipart + * The {@link Multipart} to start from. + * @param knownTextParts + * A set of known text parts we don't want to end up in 'attachments'. + * @param attachments + * A list that will receive the parts that are considered attachments. + */ + private static void findAttachments(Multipart multipart, Set knownTextParts, + List attachments) { + for (Part part : multipart.getBodyParts()) { + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart innerMultipart = (Multipart) body; + findAttachments(innerMultipart, knownTextParts, attachments); + } else if (!knownTextParts.contains(part)) { + attachments.add(part); + } + } + } + + /** + * Build a set of message parts for fast lookups. + * + * @param viewables + * A list of {@link Viewable}s containing references to the message parts to include in + * the set. + * + * @return The set of viewable {@code Part}s. + * + * @see MimeUtility#findHtmlPart(Multipart, Set, List, boolean) + * @see MimeUtility#findAttachments(Multipart, Set, List) + */ + private static Set getParts(List viewables) { + Set parts = new HashSet(); + + for (Viewable viewable : viewables) { + if (viewable instanceof Textual) { + parts.add(((Textual) viewable).getPart()); + } else if (viewable instanceof Alternative) { + Alternative alternative = (Alternative) viewable; + parts.addAll(getParts(alternative.getText())); + parts.addAll(getParts(alternative.getHtml())); + } + } + + return parts; + } + + private static Boolean isPartTextualBody(Part part) throws MessagingException { + String disposition = part.getDisposition(); + String dispositionType = null; + String dispositionFilename = null; + if (disposition != null) { + dispositionType = MimeUtility.getHeaderParameter(disposition, null); + dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename"); + } + + /* + * A best guess that this part is intended to be an attachment and not inline. + */ + boolean attachment = ("attachment".equalsIgnoreCase(dispositionType) || (dispositionFilename != null)); + + if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/html"))) { + return true; + } + /* + * If the part is plain text and it got this far it's part of a + * mixed (et al) and should be rendered inline. + */ + else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/plain"))) { + return true; + } + /* + * Finally, if it's nothing else we will include it as an attachment. + */ + else { + return false; + } + } +} diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 02c028edf..c58a46080 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -473,7 +473,7 @@ public class MimeMessage extends Message { if (mBody instanceof Multipart) { ((Multipart)mBody).setCharset(charset); } else if (mBody instanceof TextBody) { - MimeUtility.setCharset(charset, this); + CharsetSupport.setCharset(charset, this); ((TextBody)mBody).setCharset(charset); } } diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index 6aaa93256..e56476878 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -1,13 +1,12 @@ package com.fsck.k9.mail.internet; -import android.content.Context; -import android.util.Log; -import com.fsck.k9.K9; -import com.fsck.k9.R; -import com.fsck.k9.mail.*; -import com.fsck.k9.mail.Message.RecipientType; -import com.fsck.k9.mail.internet.BinaryTempFileBody.BinaryTempFileBodyInputStream; +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 org.apache.commons.io.IOUtils; import org.apache.james.mime4j.codec.Base64InputStream; @@ -17,32 +16,20 @@ import org.apache.james.mime4j.util.MimeUtil; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.List; import java.util.Locale; -import java.util.Set; -import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.nio.charset.Charset; -import java.nio.charset.IllegalCharsetNameException; public class MimeUtility { public static final String DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"; - public static final String K9_SETTINGS_MIME_TYPE = "application/x-k9settings"; - private static final String TEXT_DIVIDER = - "------------------------------------------------------------------------"; - /* * http://www.w3schools.com/media/media_mimeref.asp * + * http://www.stdicon.com/mimetypes */ - public static final String[][] MIME_TYPE_BY_EXTENSION_MAP = new String[][] { + private static final String[][] MIME_TYPE_BY_EXTENSION_MAP = new String[][] { //* Do not delete the next two lines { "", DEFAULT_ATTACHMENT_MIME_TYPE }, { "k9s", K9_SETTINGS_MIME_TYPE}, @@ -895,29 +882,6 @@ public class MimeUtility { { "zmm", "application/vnd.handheld-entertainment+xml"} }; - /** - * Table for MIME type replacements. - * - * Table format: wrong type, correct type - */ - private static final String[][] MIME_TYPE_REPLACEMENT_MAP = new String[][] { - {"image/jpg", "image/jpeg"}, - {"image/pjpeg", "image/jpeg"}, // see issue 1712 - {"application/x-zip-compressed", "application/zip"} // see issue 3791 - }; - - /** - * Table for character set fall-back. - * - * Table format: unsupported charset (regular expression), fall-back charset - */ - private static final String[][] CHARSET_FALLBACK_MAP = new String[][] { - // Some Android versions don't support KOI8-U - {"koi8-u", "koi8-r"}, - {"iso-2022-jp-[\\d]+", "iso-2022-jp"}, - // Default fall-back is US-ASCII - {".*", "US-ASCII"} - }; public static String unfold(String s) { if (s == null) { @@ -929,9 +893,9 @@ public class MimeUtility { private static String decode(String s, Message message) { if (s == null) { return null; + } else { + return DecoderUtil.decodeEncodedWords(s, message); } - - return DecoderUtil.decodeEncodedWords(s, message); } public static String unfoldAndDecode(String s) { @@ -951,24 +915,24 @@ public class MimeUtility { * Returns the named parameter of a header field. If name is null the first * parameter is returned, or if there are no additional parameters in the * field the entire field is returned. Otherwise the named parameter is - * searched for in a case insensitive fashion and returned. If the parameter - * cannot be found the method returns null. + * searched for in a case insensitive fashion and returned. * - * @param header - * @param name - * @return + * @param headerValue the header value + * @param parameterName the parameter name + * @return the value. if the parameter cannot be found the method returns null. */ - public static String getHeaderParameter(String header, String name) { - if (header == null) { + public static String getHeaderParameter(String headerValue, String parameterName) { + if (headerValue == null) { return null; } - header = header.replaceAll("\r|\n", ""); - String[] parts = header.split(";"); - if (name == null && parts.length > 0) { + headerValue = headerValue.replaceAll("\r|\n", ""); + String[] parts = headerValue.split(";"); + if (parameterName == null && parts.length > 0) { return parts[0].trim(); } for (String part : parts) { - if (part.trim().toLowerCase(Locale.US).startsWith(name.toLowerCase(Locale.US))) { + if (parameterName != null && + part.trim().toLowerCase(Locale.US).startsWith(parameterName.toLowerCase(Locale.US))) { String[] partParts = part.split("=", 2); if (partParts.length == 2) { String parameter = partParts[1].trim(); @@ -984,12 +948,11 @@ public class MimeUtility { return null; } - public static Part findFirstPartByMimeType(Part part, String mimeType) - throws MessagingException { + public static Part findFirstPartByMimeType(Part part, String mimeType) throws MessagingException { if (part.getBody() instanceof Multipart) { Multipart multipart = (Multipart)part.getBody(); for (BodyPart bodyPart : multipart.getBodyParts()) { - Part ret = findFirstPartByMimeType(bodyPart, mimeType); + Part ret = bodyPart.findFirstPartByMimeType(mimeType); if (ret != null) { return ret; } @@ -1000,104 +963,6 @@ public class MimeUtility { return null; } - /** - * Reads the Part's body and returns a String based on any charset conversion that needed - * to be done. Note, this does not return a text representation of HTML. - * @param part The part containing a body - * @return a String containing the converted text in the body, or null if there was no text - * or an error during conversion. - */ - public static String getTextFromPart(Part part) { - try { - if ((part != null) && (part.getBody() != null)) { - final Body body = part.getBody(); - if (body instanceof TextBody) { - return ((TextBody)body).getText(); - } - - final String mimeType = part.getMimeType(); - if ((mimeType != null) && MimeUtility.mimeTypeMatches(mimeType, "text/*")) { - /* - * We've got a text part, so let's see if it needs to be processed further. - */ - String charset = getHeaderParameter(part.getContentType(), "charset"); - /* - * determine the charset from HTML message. - */ - if (mimeType.equalsIgnoreCase("text/html") && charset == null) { - InputStream in = part.getBody().getInputStream(); - try { - byte[] buf = new byte[256]; - in.read(buf, 0, buf.length); - String str = new String(buf, "US-ASCII"); - - if (str.isEmpty()) { - return ""; - } - Pattern p = Pattern.compile("", Pattern.CASE_INSENSITIVE); - Matcher m = p.matcher(str); - if (m.find()) { - charset = m.group(1); - } - } finally { - try { - if (in instanceof BinaryTempFileBodyInputStream) { - /* - * If this is a BinaryTempFileBodyInputStream, calling close() - * will delete the file. But we can't let that happen because - * the file needs to be opened again by the code a few lines - * down. - */ - ((BinaryTempFileBodyInputStream) in).closeWithoutDeleting(); - } else { - in.close(); - } - } catch (Exception e) { /* ignore */ } - } - } - charset = fixupCharset(charset, getMessageFromPart(part)); - - /* - * Now we read the part into a buffer for further processing. Because - * the stream is now wrapped we'll remove any transfer encoding at this point. - */ - InputStream in = part.getBody().getInputStream(); - try { - String text = readToString(in, charset); - - // Replace the body with a TextBody that already contains the decoded text - part.setBody(new TextBody(text)); - - return text; - } finally { - try { - /* - * This time we don't care if it's a BinaryTempFileBodyInputStream. We - * replaced the body with a TextBody instance and hence don't need the - * file anymore. - */ - in.close(); - } catch (IOException e) { /* Ignore */ } - } - } - } - - } catch (OutOfMemoryError oom) { - /* - * If we are not able to process the body there's nothing we can do about it. Return - * null and let the upper layers handle the missing content. - */ - Log.e(K9.LOG_TAG, "Unable to getTextFromPart " + oom.toString()); - } catch (Exception e) { - /* - * If we are not able to process the body there's nothing we can do about it. Return - * null and let the upper layers handle the missing content. - */ - Log.e(K9.LOG_TAG, "Unable to getTextFromPart", e); - } - return null; - } - /** * Returns true if the given mimeType matches the matchAgainst specification. * @param mimeType A MIME type to check. @@ -1150,900 +1015,6 @@ public class MimeUtility { return tempBody; } - - /** - * Empty base class for the class hierarchy used by - * {@link MimeUtility#extractTextAndAttachments(Context, Message)}. - * - * @see Text - * @see Html - * @see MessageHeader - * @see Alternative - */ - static abstract class Viewable { /* empty */ } - - /** - * Class representing textual parts of a message that aren't marked as attachments. - * - * @see MimeUtility#isPartTextualBody(Part) - */ - static abstract class Textual extends Viewable { - private Part mPart; - - public Textual(Part part) { - mPart = part; - } - - public Part getPart() { - return mPart; - } - } - - /** - * Class representing a {@code text/plain} part of a message. - */ - static class Text extends Textual { - public Text(Part part) { - super(part); - } - } - - /** - * Class representing a {@code text/html} part of a message. - */ - static class Html extends Textual { - public Html(Part part) { - super(part); - } - } - - /** - * Class representing a {@code message/rfc822} part of a message. - * - *

- * This is used to extract basic header information when the message contents are displayed - * inline. - *

- */ - static class MessageHeader extends Viewable { - private Part mContainerPart; - private Message mMessage; - - public MessageHeader(Part containerPart, Message message) { - mContainerPart = containerPart; - mMessage = message; - } - - public Part getContainerPart() { - return mContainerPart; - } - - public Message getMessage() { - return mMessage; - } - } - - /** - * Class representing a {@code multipart/alternative} part of a message. - * - *

- * Only relevant {@code text/plain} and {@code text/html} children are stored in this container - * class. - *

- */ - static class Alternative extends Viewable { - private List mText; - private List mHtml; - - public Alternative(List text, List html) { - mText = text; - mHtml = html; - } - - public List getText() { - return mText; - } - - public List getHtml() { - return mHtml; - } - } - - /** - * Store viewable text of a message as plain text and HTML, and the parts considered - * attachments. - * - * @see MimeUtility#extractTextAndAttachments(Context, Message) - */ - public static class ViewableContainer { - /** - * The viewable text of the message in plain text. - */ - public final String text; - - /** - * The viewable text of the message in HTML. - */ - public final String html; - - /** - * The parts of the message considered attachments (everything not viewable). - */ - public final List attachments; - - ViewableContainer(String text, String html, List attachments) { - this.text = text; - this.html = html; - this.attachments = attachments; - } - } - - /** - * Collect attachment parts of a message. - * - * @param message - * The message to collect the attachment parts from. - * - * @return A list of parts regarded as attachments. - * - * @throws MessagingException - * In case of an error. - */ - public static List collectAttachments(Message message) - throws MessagingException { - try { - List attachments = new ArrayList(); - getViewables(message, attachments); - - return attachments; - } catch (Exception e) { - throw new MessagingException("Couldn't collect attachment parts", e); - } - } - - /** - * Collect the viewable textual parts of a message. - * - * @param message - * The message to extract the viewable parts from. - * - * @return A set of viewable parts of the message. - * - * @throws MessagingException - * In case of an error. - */ - public static Set collectTextParts(Message message) - throws MessagingException { - try { - List attachments = new ArrayList(); - - // Collect all viewable parts - List viewables = getViewables(message, attachments); - - // Extract the Part references - return getParts(viewables); - } catch (Exception e) { - throw new MessagingException("Couldn't extract viewable parts", e); - } - } - - /** - * Extract the viewable textual parts of a message and return the rest as attachments. - * - * @param context - * A {@link Context} instance that will be used to get localized strings. - * @param message - * The message to extract the text and attachments from. - * - * @return A {@link ViewableContainer} instance containing the textual parts of the message as - * plain text and HTML, and a list of message parts considered attachments. - * - * @throws MessagingException - * In case of an error. - */ - public static ViewableContainer extractTextAndAttachments(Context context, Message message) - throws MessagingException { - try { - List attachments = new ArrayList(); - - // Collect all viewable parts - List viewables = getViewables(message, attachments); - - /* - * Convert the tree of viewable parts into text and HTML - */ - - // Used to suppress the divider for the first viewable part - boolean hideDivider = true; - - StringBuilder text = new StringBuilder(); - StringBuilder html = new StringBuilder(); - - for (Viewable viewable : viewables) { - if (viewable instanceof Textual) { - // This is either a text/plain or text/html part. Fill the variables 'text' and - // 'html', converting between plain text and HTML as necessary. - text.append(buildText(viewable, !hideDivider)); - html.append(buildHtml(viewable, !hideDivider)); - hideDivider = false; - } else if (viewable instanceof MessageHeader) { - MessageHeader header = (MessageHeader) viewable; - Part containerPart = header.getContainerPart(); - Message innerMessage = header.getMessage(); - - addTextDivider(text, containerPart, !hideDivider); - addMessageHeaderText(context, text, innerMessage); - - addHtmlDivider(html, containerPart, !hideDivider); - addMessageHeaderHtml(context, html, innerMessage); - - hideDivider = true; - } else if (viewable instanceof Alternative) { - // Handle multipart/alternative contents - Alternative alternative = (Alternative) viewable; - - /* - * We made sure at least one of text/plain or text/html is present when - * creating the Alternative object. If one part is not present we convert the - * other one to make sure 'text' and 'html' always contain the same text. - */ - List textAlternative = alternative.getText().isEmpty() ? - alternative.getHtml() : alternative.getText(); - List htmlAlternative = alternative.getHtml().isEmpty() ? - alternative.getText() : alternative.getHtml(); - - // Fill the 'text' variable - boolean divider = !hideDivider; - for (Viewable textViewable : textAlternative) { - text.append(buildText(textViewable, divider)); - divider = true; - } - - // Fill the 'html' variable - divider = !hideDivider; - for (Viewable htmlViewable : htmlAlternative) { - html.append(buildHtml(htmlViewable, divider)); - divider = true; - } - hideDivider = false; - } - } - - return new ViewableContainer(text.toString(), html.toString(), attachments); - } catch (Exception e) { - throw new MessagingException("Couldn't extract viewable parts", e); - } - } - - /** - * Traverse the MIME tree of a message an extract viewable parts. - * - * @param part - * The message part to start from. - * @param attachments - * A list that will receive the parts that are considered attachments. - * - * @return A list of {@link Viewable}s. - * - * @throws MessagingException - * In case of an error. - */ - private static List getViewables(Part part, List attachments) throws MessagingException { - List viewables = new ArrayList(); - - Body body = part.getBody(); - if (body instanceof Multipart) { - Multipart multipart = (Multipart) body; - if (part.getMimeType().equalsIgnoreCase("multipart/alternative")) { - /* - * For multipart/alternative parts we try to find a text/plain and a text/html - * child. Everything else we find is put into 'attachments'. - */ - List text = findTextPart(multipart, true); - - Set knownTextParts = getParts(text); - List html = findHtmlPart(multipart, knownTextParts, attachments, true); - - if (!text.isEmpty() || !html.isEmpty()) { - Alternative alternative = new Alternative(text, html); - viewables.add(alternative); - } - } else { - // For all other multipart parts we recurse to grab all viewable children. - for (Part bodyPart : multipart.getBodyParts()) { - viewables.addAll(getViewables(bodyPart, attachments)); - } - } - } else if (body instanceof Message && - !("attachment".equalsIgnoreCase(getContentDisposition(part)))) { - /* - * We only care about message/rfc822 parts whose Content-Disposition header has a value - * other than "attachment". - */ - Message message = (Message) body; - - // We add the Message object so we can extract the filename later. - viewables.add(new MessageHeader(part, message)); - - // Recurse to grab all viewable parts and attachments from that message. - viewables.addAll(getViewables(message, attachments)); - } else if (isPartTextualBody(part)) { - /* - * Save text/plain and text/html - */ - String mimeType = part.getMimeType(); - if (mimeType.equalsIgnoreCase("text/plain")) { - Text text = new Text(part); - viewables.add(text); - } else { - Html html = new Html(part); - viewables.add(html); - } - } else { - // Everything else is treated as attachment. - attachments.add(part); - } - - return viewables; - } - - /** - * Search the children of a {@link Multipart} for {@code text/plain} parts. - * - * @param multipart - * The {@code Multipart} to search through. - * @param directChild - * If {@code true}, this method will return after the first {@code text/plain} was - * found. - * - * @return A list of {@link Text} viewables. - * - * @throws MessagingException - * In case of an error. - */ - private static List findTextPart(Multipart multipart, boolean directChild) - throws MessagingException { - List viewables = new ArrayList(); - - for (Part part : multipart.getBodyParts()) { - Body body = part.getBody(); - if (body instanceof Multipart) { - Multipart innerMultipart = (Multipart) body; - - /* - * Recurse to find text parts. Since this is a multipart that is a child of a - * multipart/alternative we don't want to stop after the first text/plain part - * we find. This will allow to get all text parts for constructions like this: - * - * 1. multipart/alternative - * 1.1. multipart/mixed - * 1.1.1. text/plain - * 1.1.2. text/plain - * 1.2. text/html - */ - List textViewables = findTextPart(innerMultipart, false); - - if (!textViewables.isEmpty()) { - viewables.addAll(textViewables); - if (directChild) { - break; - } - } - } else if (isPartTextualBody(part) && part.getMimeType().equalsIgnoreCase("text/plain")) { - Text text = new Text(part); - viewables.add(text); - if (directChild) { - break; - } - } - } - - return viewables; - } - - /** - * Search the children of a {@link Multipart} for {@code text/html} parts. - * - *

- * Every part that is not a {@code text/html} we want to display, we add to 'attachments'. - *

- * - * @param multipart - * The {@code Multipart} to search through. - * @param knownTextParts - * A set of {@code text/plain} parts that shouldn't be added to 'attachments'. - * @param attachments - * A list that will receive the parts that are considered attachments. - * @param directChild - * If {@code true}, this method will add all {@code text/html} parts except the first - * found to 'attachments'. - * - * @return A list of {@link Text} viewables. - * - * @throws MessagingException - * In case of an error. - */ - private static List findHtmlPart(Multipart multipart, Set knownTextParts, - List attachments, boolean directChild) throws MessagingException { - List viewables = new ArrayList(); - - boolean partFound = false; - for (Part part : multipart.getBodyParts()) { - Body body = part.getBody(); - if (body instanceof Multipart) { - Multipart innerMultipart = (Multipart) body; - - if (directChild && partFound) { - // We already found our text/html part. Now we're only looking for attachments. - findAttachments(innerMultipart, knownTextParts, attachments); - } else { - /* - * Recurse to find HTML parts. Since this is a multipart that is a child of a - * multipart/alternative we don't want to stop after the first text/html part - * we find. This will allow to get all text parts for constructions like this: - * - * 1. multipart/alternative - * 1.1. text/plain - * 1.2. multipart/mixed - * 1.2.1. text/html - * 1.2.2. text/html - * 1.3. image/jpeg - */ - List htmlViewables = findHtmlPart(innerMultipart, knownTextParts, - attachments, false); - - if (!htmlViewables.isEmpty()) { - partFound = true; - viewables.addAll(htmlViewables); - } - } - } else if (!(directChild && partFound) && isPartTextualBody(part) && - part.getMimeType().equalsIgnoreCase("text/html")) { - Html html = new Html(part); - viewables.add(html); - partFound = true; - } else if (!knownTextParts.contains(part)) { - // Only add this part as attachment if it's not a viewable text/plain part found - // earlier. - attachments.add(part); - } - } - - return viewables; - } - - /** - * Build a set of message parts for fast lookups. - * - * @param viewables - * A list of {@link Viewable}s containing references to the message parts to include in - * the set. - * - * @return The set of viewable {@code Part}s. - * - * @see MimeUtility#findHtmlPart(Multipart, Set, List, boolean) - * @see MimeUtility#findAttachments(Multipart, Set, List) - */ - private static Set getParts(List viewables) { - Set parts = new HashSet(); - - for (Viewable viewable : viewables) { - if (viewable instanceof Textual) { - parts.add(((Textual) viewable).getPart()); - } else if (viewable instanceof Alternative) { - Alternative alternative = (Alternative) viewable; - parts.addAll(getParts(alternative.getText())); - parts.addAll(getParts(alternative.getHtml())); - } - } - - return parts; - } - - /** - * Traverse the MIME tree and add everything that's not a known text part to 'attachments'. - * - * @param multipart - * The {@link Multipart} to start from. - * @param knownTextParts - * A set of known text parts we don't want to end up in 'attachments'. - * @param attachments - * A list that will receive the parts that are considered attachments. - */ - private static void findAttachments(Multipart multipart, Set knownTextParts, - List attachments) { - for (Part part : multipart.getBodyParts()) { - Body body = part.getBody(); - if (body instanceof Multipart) { - Multipart innerMultipart = (Multipart) body; - findAttachments(innerMultipart, knownTextParts, attachments); - } else if (!knownTextParts.contains(part)) { - attachments.add(part); - } - } - } - - /** - * Extract important header values from a message to display inline (plain text version). - * - * @param context - * A {@link Context} instance that will be used to get localized strings. - * @param text - * The {@link StringBuilder} that will receive the (plain text) output. - * @param message - * The message to extract the header values from. - * - * @throws MessagingException - * In case of an error. - */ - private static void addMessageHeaderText(Context context, StringBuilder text, Message message) - throws MessagingException { - // From: - Address[] from = message.getFrom(); - if (from != null && from.length > 0) { - text.append(context.getString(R.string.message_compose_quote_header_from)); - text.append(' '); - text.append(Address.toString(from)); - text.append("\r\n"); - } - - // To: - Address[] to = message.getRecipients(RecipientType.TO); - if (to != null && to.length > 0) { - text.append(context.getString(R.string.message_compose_quote_header_to)); - text.append(' '); - text.append(Address.toString(to)); - text.append("\r\n"); - } - - // Cc: - Address[] cc = message.getRecipients(RecipientType.CC); - if (cc != null && cc.length > 0) { - text.append(context.getString(R.string.message_compose_quote_header_cc)); - text.append(' '); - text.append(Address.toString(cc)); - text.append("\r\n"); - } - - // Date: - Date date = message.getSentDate(); - if (date != null) { - text.append(context.getString(R.string.message_compose_quote_header_send_date)); - text.append(' '); - text.append(date.toString()); - text.append("\r\n"); - } - - // Subject: - String subject = message.getSubject(); - text.append(context.getString(R.string.message_compose_quote_header_subject)); - text.append(' '); - if (subject == null) { - text.append(context.getString(R.string.general_no_subject)); - } else { - text.append(subject); - } - text.append("\r\n\r\n"); - } - - /** - * Extract important header values from a message to display inline (HTML version). - * - * @param context - * A {@link Context} instance that will be used to get localized strings. - * @param html - * The {@link StringBuilder} that will receive the (HTML) output. - * @param message - * The message to extract the header values from. - * - * @throws MessagingException - * In case of an error. - */ - private static void addMessageHeaderHtml(Context context, StringBuilder html, Message message) - throws MessagingException { - - html.append(""); - - // From: - Address[] from = message.getFrom(); - if (from != null && from.length > 0) { - addTableRow(html, context.getString(R.string.message_compose_quote_header_from), - Address.toString(from)); - } - - // To: - Address[] to = message.getRecipients(RecipientType.TO); - if (to != null && to.length > 0) { - addTableRow(html, context.getString(R.string.message_compose_quote_header_to), - Address.toString(to)); - } - - // Cc: - Address[] cc = message.getRecipients(RecipientType.CC); - if (cc != null && cc.length > 0) { - addTableRow(html, context.getString(R.string.message_compose_quote_header_cc), - Address.toString(cc)); - } - - // Date: - Date date = message.getSentDate(); - if (date != null) { - addTableRow(html, context.getString(R.string.message_compose_quote_header_send_date), - date.toString()); - } - - // Subject: - String subject = message.getSubject(); - addTableRow(html, context.getString(R.string.message_compose_quote_header_subject), - (subject == null) ? context.getString(R.string.general_no_subject) : subject); - - html.append("
"); - } - - /** - * Output an HTML table two column row with some hardcoded style. - * - * @param html - * The {@link StringBuilder} that will receive the output. - * @param header - * The string to be put in the {@code TH} element. - * @param value - * The string to be put in the {@code TD} element. - */ - private static void addTableRow(StringBuilder html, String header, String value) { - html.append(""); - html.append(header); - html.append(""); - html.append(""); - html.append(value); - html.append(""); - } - - /** - * Use the contents of a {@link Viewable} to create the plain text to be displayed. - * - *

- * This will use {@link HtmlConverter#htmlToText(String)} to convert HTML parts to plain text - * if necessary. - *

- * - * @param viewable - * The viewable part to build the text from. - * @param prependDivider - * {@code true}, if the text divider should be inserted as first element. - * {@code false}, otherwise. - * - * @return The contents of the supplied viewable instance as plain text. - */ - private static StringBuilder buildText(Viewable viewable, boolean prependDivider) - { - StringBuilder text = new StringBuilder(); - if (viewable instanceof Textual) { - Part part = ((Textual)viewable).getPart(); - addTextDivider(text, part, prependDivider); - - String t = getTextFromPart(part); - if (t == null) { - t = ""; - } else if (viewable instanceof Html) { - t = HtmlConverter.htmlToText(t); - } - text.append(t); - } else if (viewable instanceof Alternative) { - // That's odd - an Alternative as child of an Alternative; go ahead and try to use the - // text/plain child; fall-back to the text/html part. - Alternative alternative = (Alternative) viewable; - - List textAlternative = alternative.getText().isEmpty() ? - alternative.getHtml() : alternative.getText(); - - boolean divider = prependDivider; - for (Viewable textViewable : textAlternative) { - text.append(buildText(textViewable, divider)); - divider = true; - } - } - - return text; - } - - /* - * Some constants that are used by addTextDivider() below. - */ - private static final int TEXT_DIVIDER_LENGTH = TEXT_DIVIDER.length(); - private static final String FILENAME_PREFIX = "----- "; - private static final int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX.length(); - private static final String FILENAME_SUFFIX = " "; - private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length(); - - /** - * Add a plain text divider between two plain text message parts. - * - * @param text - * The {@link StringBuilder} to append the divider to. - * @param part - * The message part that will follow after the divider. This is used to extract the - * part's name. - * @param prependDivider - * {@code true}, if the divider should be appended. {@code false}, otherwise. - */ - private static void addTextDivider(StringBuilder text, Part part, boolean prependDivider) { - if (prependDivider) { - String filename = getPartName(part); - - text.append("\r\n\r\n"); - int len = filename.length(); - if (len > 0) { - if (len > TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - FILENAME_SUFFIX_LENGTH) { - filename = filename.substring(0, TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - - FILENAME_SUFFIX_LENGTH - 3) + "..."; - } - text.append(FILENAME_PREFIX); - text.append(filename); - text.append(FILENAME_SUFFIX); - text.append(TEXT_DIVIDER.substring(0, TEXT_DIVIDER_LENGTH - - FILENAME_PREFIX_LENGTH - filename.length() - FILENAME_SUFFIX_LENGTH)); - } else { - text.append(TEXT_DIVIDER); - } - text.append("\r\n\r\n"); - } - } - - /** - * Use the contents of a {@link Viewable} to create the HTML to be displayed. - * - *

- * This will use {@link HtmlConverter#textToHtml(String)} to convert plain text parts - * to HTML if necessary. - *

- * - * @param viewable - * The viewable part to build the HTML from. - * @param prependDivider - * {@code true}, if the HTML divider should be inserted as first element. - * {@code false}, otherwise. - * - * @return The contents of the supplied viewable instance as HTML. - */ - private static StringBuilder buildHtml(Viewable viewable, boolean prependDivider) - { - StringBuilder html = new StringBuilder(); - if (viewable instanceof Textual) { - Part part = ((Textual)viewable).getPart(); - addHtmlDivider(html, part, prependDivider); - - String t = getTextFromPart(part); - if (t == null) { - t = ""; - } else if (viewable instanceof Text) { - t = HtmlConverter.textToHtml(t); - } - html.append(t); - } else if (viewable instanceof Alternative) { - // That's odd - an Alternative as child of an Alternative; go ahead and try to use the - // text/html child; fall-back to the text/plain part. - Alternative alternative = (Alternative) viewable; - - List htmlAlternative = alternative.getHtml().isEmpty() ? - alternative.getText() : alternative.getHtml(); - - boolean divider = prependDivider; - for (Viewable htmlViewable : htmlAlternative) { - html.append(buildHtml(htmlViewable, divider)); - divider = true; - } - } - - return html; - } - - /** - * Add an HTML divider between two HTML message parts. - * - * @param html - * The {@link StringBuilder} to append the divider to. - * @param part - * The message part that will follow after the divider. This is used to extract the - * part's name. - * @param prependDivider - * {@code true}, if the divider should be appended. {@code false}, otherwise. - */ - private static void addHtmlDivider(StringBuilder html, Part part, boolean prependDivider) { - if (prependDivider) { - String filename = getPartName(part); - - html.append("

"); - html.append(filename); - html.append("

"); - } - } - - /** - * Get the name of the message part. - * - * @param part - * The part to get the name for. - * - * @return The (file)name of the part if available. An empty string, otherwise. - */ - private static String getPartName(Part part) { - try { - String disposition = part.getDisposition(); - if (disposition != null) { - String name = MimeUtility.getHeaderParameter(disposition, "filename"); - return (name == null) ? "" : name; - } - } - catch (MessagingException e) { /* ignore */ } - - return ""; - } - - /** - * Get the value of the {@code Content-Disposition} header. - * - * @param part - * The message part to read the header from. - * - * @return The value of the {@code Content-Disposition} header if available. {@code null}, - * otherwise. - */ - private static String getContentDisposition(Part part) { - try { - String disposition = part.getDisposition(); - if (disposition != null) { - return MimeUtility.getHeaderParameter(disposition, null); - } - } - catch (MessagingException e) { /* ignore */ } - - return null; - } - - private static Boolean isPartTextualBody(Part part) throws MessagingException { - String disposition = part.getDisposition(); - String dispositionType = null; - String dispositionFilename = null; - if (disposition != null) { - dispositionType = MimeUtility.getHeaderParameter(disposition, null); - dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename"); - } - - /* - * A best guess that this part is intended to be an attachment and not inline. - */ - boolean attachment = ("attachment".equalsIgnoreCase(dispositionType) || (dispositionFilename != null)); - - if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/html"))) { - return true; - } - /* - * If the part is plain text and it got this far it's part of a - * mixed (et al) and should be rendered inline. - */ - else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/plain"))) { - return true; - } - /* - * Finally, if it's nothing else we will include it as an attachment. - */ - else { - return false; - } - } - - public static String getCharsetFromAddress(String address) { - String variant = getJisVariantFromAddress(address); - if (variant != null) { - String charset = "x-" + variant + "-shift_jis-2007"; - if (Charset.isSupported(charset)) - return charset; - } - - return "UTF-8"; - } - public static String getMimeTypeByExtension(String filename) { String returnedType = null; String extension = null; @@ -2078,50 +1049,6 @@ public class MimeUtility { return null; } - /** - * Convert some wrong MIME types encountered in the wild to canonical MIME types. - * - * @param mimeType - * The original MIME type - * - * @return If {@code mimeType} is known to be wrong the correct MIME type is returned. - * Otherwise the lower case version of {@code mimeType} is returned. - * - * @see #MIME_TYPE_REPLACEMENT_MAP - */ - private static String canonicalizeMimeType(String mimeType) { - String lowerCaseMimeType = mimeType.toLowerCase(Locale.US); - for (String[] mimeTypeMapEntry : MIME_TYPE_REPLACEMENT_MAP) { - if (mimeTypeMapEntry[0].equals(lowerCaseMimeType)) { - return mimeTypeMapEntry[1]; - } - } - return lowerCaseMimeType; - } - - /** - * When viewing the attachment we want the MIME type to be as sensible as possible. So we fix - * it up if necessary. - * - * @param mimeType - * The original MIME type of the attachment. - * @param name - * The (file)name of the attachment. - * - * @return The best MIME type we can come up with. - */ - public static String getMimeTypeForViewing(String mimeType, String name) { - if (DEFAULT_ATTACHMENT_MIME_TYPE.equalsIgnoreCase(mimeType)) { - // If the MIME type is the generic "application/octet-stream" - // we try to find a better one by looking at the file extension. - return getMimeTypeByExtension(name); - } - - // Some messages contain wrong MIME types. See if we know better. - return canonicalizeMimeType(mimeType); - } - - /** * Get a default content-transfer-encoding for use with a given content-type * when adding an unencoded attachment. It's possible that 8bit encodings @@ -2152,1240 +1079,4 @@ public class MimeUtility { return (MimeUtil.ENC_BASE64); } } - - private static Message getMessageFromPart(Part part) { - while (part != null) { - if (part instanceof Message) - return (Message)part; - - if (!(part instanceof BodyPart)) - return null; - - Multipart multipart = ((BodyPart)part).getParent(); - if (multipart == null) - return null; - - part = multipart.getParent(); - } - return null; - } - - public static String fixupCharset(String charset, Message message) throws MessagingException { - if (charset == null || "0".equals(charset)) - charset = "US-ASCII"; // No encoding, so use us-ascii, which is the standard. - - charset = charset.toLowerCase(Locale.US); - if (charset.equals("cp932")) - charset = "shift_jis"; - - if (charset.equals("shift_jis") || charset.equals("iso-2022-jp")) { - String variant = getJisVariantFromMessage(message); - if (variant != null) - charset = "x-" + variant + "-" + charset + "-2007"; - } - return charset; - } - - private static String getJisVariantFromMessage(Message message) throws MessagingException { - if (message == null) - return null; - - // If a receiver is known to use a JIS variant, the sender transfers the message after converting the - // charset as a convention. - String variant = getJisVariantFromReceivedHeaders(message); - if (variant != null) - return variant; - - // If a receiver is not known to use any JIS variants, the sender transfers the message without converting - // the charset. - variant = getJisVariantFromFromHeaders(message); - if (variant != null) - return variant; - - return getJisVariantFromMailerHeaders(message); - } - - private static String getJisVariantFromReceivedHeaders(Message message) throws MessagingException { - String receivedHeaders[] = message.getHeader("Received"); - if (receivedHeaders == null) - return null; - - for (String receivedHeader : receivedHeaders) { - String address = getAddressFromReceivedHeader(receivedHeader); - if (address == null) - continue; - String variant = getJisVariantFromAddress(address); - if (variant != null) - return variant; - } - return null; - } - - private static String getAddressFromReceivedHeader(String receivedHeader) { - // Not implemented yet! Extract an address from the FOR clause of the given Received header. - return null; - } - - private static String getJisVariantFromFromHeaders(Message message) throws MessagingException { - Address addresses[] = message.getFrom(); - if (addresses == null || addresses.length == 0) - return null; - - return getJisVariantFromAddress(addresses[0].getAddress()); - } - - private static String getJisVariantFromAddress(String address) { - if (address == null) - return null; - if (isInDomain(address, "docomo.ne.jp") || isInDomain(address, "dwmail.jp") || - isInDomain(address, "pdx.ne.jp") || isInDomain(address, "willcom.com") || - isInDomain(address, "emnet.ne.jp") || isInDomain(address, "emobile.ne.jp")) - return "docomo"; - else if (isInDomain(address, "softbank.ne.jp") || isInDomain(address, "vodafone.ne.jp") || - isInDomain(address, "disney.ne.jp") || isInDomain(address, "vertuclub.ne.jp")) - return "softbank"; - else if (isInDomain(address, "ezweb.ne.jp") || isInDomain(address, "ido.ne.jp")) - return "kddi"; - return null; - } - - private static boolean isInDomain(String address, String domain) { - int index = address.length() - domain.length() - 1; - if (index < 0) - return false; - - char c = address.charAt(index); - if (c != '@' && c != '.') - return false; - - return address.endsWith(domain); - } - - private static String getJisVariantFromMailerHeaders(Message message) throws MessagingException { - String mailerHeaders[] = message.getHeader("X-Mailer"); - if (mailerHeaders == null || mailerHeaders.length == 0) - return null; - - if (mailerHeaders[0].startsWith("iPhone Mail ") || mailerHeaders[0].startsWith("iPad Mail ")) - return "iphone"; - - return null; - } - - public static String readToString(InputStream in, String charset) throws IOException { - boolean isIphoneString = false; - - // iso-2022-jp variants are supported by no versions as of Dec 2010. - if (charset.length() > 19 && charset.startsWith("x-") && - charset.endsWith("-iso-2022-jp-2007") && !Charset.isSupported(charset)) { - in = new Iso2022JpToShiftJisInputStream(in); - charset = "x-" + charset.substring(2, charset.length() - 17) + "-shift_jis-2007"; - } - - // shift_jis variants are supported by Eclair and later. - if (charset.length() > 17 && charset.startsWith("x-") && - charset.endsWith("-shift_jis-2007") && !Charset.isSupported(charset)) { - // If the JIS variant is iPhone, map the Unicode private use area in iPhone to the one in Android after - // converting the character set from the standard Shift JIS to Unicode. - if (charset.substring(2, charset.length() - 15).equals("iphone")) - isIphoneString = true; - - charset = "shift_jis"; - } - - /* - * See if there is conversion from the MIME charset to the Java one. - * this function may also throw an exception if the charset name is not known - */ - boolean supported; - try { - supported = Charset.isSupported(charset); - } catch (IllegalCharsetNameException e) { - supported = false; - } - - for (String[] rule: CHARSET_FALLBACK_MAP) { - if (supported) { - break; - } - - if (charset.matches(rule[0])) { - Log.e(K9.LOG_TAG, "I don't know how to deal with the charset " + charset + - ". Falling back to " + rule[1]); - charset = rule[1]; - try { - supported = Charset.isSupported(charset); - } catch (IllegalCharsetNameException e) { - supported = false; - } - } - } - - /* - * Convert and return as new String - */ - String str = IOUtils.toString(in, charset); - - if (isIphoneString) - str = importStringFromIphone(str); - return str; - } - - static private String importStringFromIphone(String str) { - StringBuilder buff = new StringBuilder(str.length()); - for (int i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) { - int codePoint = str.codePointAt(i); - buff.appendCodePoint(importCodePointFromIphone(codePoint)); - } - return buff.toString(); - } - - static private int importCodePointFromIphone(int codePoint) { - switch (codePoint) { - case 0xE001: - return 0xFE19B; - case 0xE002: - return 0xFE19C; - case 0xE003: - return 0xFE823; - case 0xE004: - return 0xFE19D; - case 0xE005: - return 0xFE19E; - case 0xE006: - return 0xFE4CF; - case 0xE007: - return 0xFE4CD; - case 0xE008: - return 0xFE4EF; - case 0xE009: - return 0xFE523; - case 0xE00A: - return 0xFE525; - case 0xE00B: - return 0xFE528; - case 0xE00C: - return 0xFE538; - case 0xE00D: - return 0xFEB96; - case 0xE00E: - return 0xFEB97; - case 0xE00F: - return 0xFEB98; - case 0xE010: - return 0xFEB93; - case 0xE011: - return 0xFEB94; - case 0xE012: - return 0xFEB95; - case 0xE013: - return 0xFE7D5; - case 0xE014: - return 0xFE7D2; - case 0xE015: - return 0xFE7D3; - case 0xE016: - return 0xFE7D1; - case 0xE017: - return 0xFE7DA; - case 0xE018: - return 0xFE7D4; - case 0xE019: - return 0xFE1BD; - case 0xE01A: - return 0xFE1BE; - case 0xE01B: - return 0xFE7E4; - case 0xE01C: - return 0xFE7EA; - case 0xE01D: - return 0xFE7E9; - case 0xE01E: - return 0xFE7DF; - case 0xE01F: - return 0xFE7E3; - case 0xE020: - return 0xFEB09; - case 0xE021: - return 0xFEB04; - case 0xE022: - return 0xFEB0C; - case 0xE023: - return 0xFEB0E; - case 0xE024: - return 0xFE01E; - case 0xE025: - return 0xFE01F; - case 0xE026: - return 0xFE020; - case 0xE027: - return 0xFE021; - case 0xE028: - return 0xFE022; - case 0xE029: - return 0xFE023; - case 0xE02A: - return 0xFE024; - case 0xE02B: - return 0xFE025; - case 0xE02C: - return 0xFE026; - case 0xE02D: - return 0xFE027; - case 0xE02E: - return 0xFE028; - case 0xE02F: - return 0xFE029; - case 0xE030: - return 0xFE040; - case 0xE031: - return 0xFE4D2; - case 0xE032: - return 0xFE041; - case 0xE033: - return 0xFE512; - case 0xE034: - return 0xFE825; - case 0xE035: - return 0xFE826; - case 0xE036: - return 0xFE4B0; - case 0xE037: - return 0xFE4BB; - case 0xE038: - return 0xFE4B2; - case 0xE039: - return 0xFE7EC; - case 0xE03A: - return 0xFE7F5; - case 0xE03B: - return 0xFE4C3; - case 0xE03C: - return 0xFE800; - case 0xE03D: - return 0xFE801; - case 0xE03E: - return 0xFE813; - case 0xE03F: - return 0xFEB82; - case 0xE040: - return 0xFE815; - case 0xE041: - return 0xFE816; - case 0xE042: - return 0xFE818; - case 0xE043: - return 0xFE980; - case 0xE044: - return 0xFE982; - case 0xE045: - return 0xFE981; - case 0xE046: - return 0xFE962; - case 0xE047: - return 0xFE983; - case 0xE048: - return 0xFE003; - case 0xE049: - return 0xFE001; - case 0xE04A: - return 0xFE000; - case 0xE04B: - return 0xFE002; - case 0xE04C: - return 0xFE014; - case 0xE04D: - return 0xFE009; - case 0xE04E: - return 0xFE1AF; - case 0xE04F: - return 0xFE1B8; - case 0xE050: - return 0xFE1C0; - case 0xE051: - return 0xFE1C1; - case 0xE052: - return 0xFE1B7; - case 0xE053: - return 0xFE1C2; - case 0xE054: - return 0xFE1C3; - case 0xE055: - return 0xFE1BC; - case 0xE056: - return 0xFE335; - case 0xE057: - return 0xFE330; - case 0xE058: - return 0xFE323; - case 0xE059: - return 0xFE320; - case 0xE05A: - return 0xFE4F4; - case 0xE101: - return 0xFE52D; - case 0xE102: - return 0xFE52E; - case 0xE103: - return 0xFE52B; - case 0xE104: - return 0xFE526; - case 0xE105: - return 0xFE329; - case 0xE106: - return 0xFE327; - case 0xE107: - return 0xFE341; - case 0xE108: - return 0xFE344; - case 0xE109: - return 0xFE1C4; - case 0xE10A: - return 0xFE1C5; - case 0xE10B: - return 0xFE1BF; - case 0xE10C: - return 0xFE1B0; - case 0xE10D: - return 0xFE7ED; - case 0xE10E: - return 0xFE4D1; - case 0xE10F: - return 0xFEB56; - case 0xE110: - return 0xFE03C; - case 0xE111: - return 0xFE827; - case 0xE112: - return 0xFE510; - case 0xE113: - return 0xFE4F5; - case 0xE114: - return 0xFEB85; - case 0xE115: - return 0xFE7D9; - case 0xE116: - return 0xFE4CA; - case 0xE117: - return 0xFE515; - case 0xE118: - return 0xFE03F; - case 0xE119: - return 0xFE042; - case 0xE11A: - return 0xFE1B2; - case 0xE11B: - return 0xFE1AE; - case 0xE11C: - return 0xFE1B3; - case 0xE11D: - return 0xFE4F6; - case 0xE11E: - return 0xFE53B; - case 0xE11F: - return 0xFE537; - case 0xE120: - return 0xFE960; - case 0xE121: - return 0xFE4BC; - case 0xE122: - return 0xFE7FB; - case 0xE123: - return 0xFE7FA; - case 0xE124: - return 0xFE7FD; - case 0xE125: - return 0xFE807; - case 0xE126: - return 0xFE81D; - case 0xE127: - return 0xFE81E; - case 0xE128: - return 0xFE81F; - case 0xE129: - return 0xFE820; - case 0xE12A: - return 0xFE81C; - case 0xE12B: - return 0xFE1B1; - case 0xE12C: - return 0xFE81B; - case 0xE12D: - return 0xFE80B; - case 0xE12E: - return 0xFEB32; - case 0xE12F: - return 0xFE4DD; - case 0xE130: - return 0xFE80C; - case 0xE131: - return 0xFE7DB; - case 0xE132: - return 0xFE7D7; - case 0xE133: - return 0xFE80D; - case 0xE134: - return 0xFE7DC; - case 0xE135: - return 0xFE7EE; - case 0xE136: - return 0xFE7EB; - case 0xE137: - return 0xFE7F8; - case 0xE138: - return 0xFEB33; - case 0xE139: - return 0xFEB34; - case 0xE13A: - return 0xFEB35; - case 0xE13B: - return 0xFE509; - case 0xE13C: - return 0xFEB59; - case 0xE13D: - return 0xFE004; - case 0xE13E: - return 0xFE4D6; - case 0xE13F: - return 0xFE505; - case 0xE140: - return 0xFE507; - case 0xE141: - return 0xFE821; - case 0xE142: - return 0xFE52F; - case 0xE143: - return 0xFE514; - case 0xE144: - return 0xFEB86; - case 0xE145: - return 0xFEB87; - case 0xE146: - return 0xFE00B; - case 0xE147: - return 0xFE965; - case 0xE148: - return 0xFE546; - case 0xE149: - return 0xFE4DE; - case 0xE14A: - return 0xFE4DF; - case 0xE14B: - return 0xFE531; - case 0xE14C: - return 0xFEB5E; - case 0xE14D: - return 0xFE4B5; - case 0xE14E: - return 0xFE7F7; - case 0xE14F: - return 0xFE7F6; - case 0xE150: - return 0xFE7E7; - case 0xE151: - return 0xFE506; - case 0xE152: - return 0xFE1A1; - case 0xE153: - return 0xFE4B3; - case 0xE154: - return 0xFE4B6; - case 0xE155: - return 0xFE4B4; - case 0xE156: - return 0xFE4B9; - case 0xE157: - return 0xFE4BA; - case 0xE158: - return 0xFE4B7; - case 0xE159: - return 0xFE7E6; - case 0xE15A: - return 0xFE7EF; - case 0xE201: - return 0xFE7F0; - case 0xE202: - return 0xFE7E8; - case 0xE203: - return 0xFEB24; - case 0xE204: - return 0xFEB19; - case 0xE205: - return 0xFEB61; - case 0xE206: - return 0xFEB62; - case 0xE207: - return 0xFEB25; - case 0xE208: - return 0xFEB1F; - case 0xE209: - return 0xFE044; - case 0xE20A: - return 0xFEB20; - case 0xE20B: - return 0xFE838; - case 0xE20C: - return 0xFEB1A; - case 0xE20D: - return 0xFEB1C; - case 0xE20E: - return 0xFEB1B; - case 0xE20F: - return 0xFEB1D; - case 0xE210: - return 0xFE82C; - case 0xE211: - return 0xFE82B; - case 0xE212: - return 0xFEB36; - case 0xE213: - return 0xFEB37; - case 0xE214: - return 0xFEB38; - case 0xE215: - return 0xFEB39; - case 0xE216: - return 0xFEB3A; - case 0xE217: - return 0xFEB3B; - case 0xE218: - return 0xFEB3C; - case 0xE219: - return 0xFEB63; - case 0xE21A: - return 0xFEB64; - case 0xE21B: - return 0xFEB67; - case 0xE21C: - return 0xFE82E; - case 0xE21D: - return 0xFE82F; - case 0xE21E: - return 0xFE830; - case 0xE21F: - return 0xFE831; - case 0xE220: - return 0xFE832; - case 0xE221: - return 0xFE833; - case 0xE222: - return 0xFE834; - case 0xE223: - return 0xFE835; - case 0xE224: - return 0xFE836; - case 0xE225: - return 0xFE837; - case 0xE226: - return 0xFEB3D; - case 0xE227: - return 0xFEB3E; - case 0xE228: - return 0xFEB3F; - case 0xE229: - return 0xFEB81; - case 0xE22A: - return 0xFEB31; - case 0xE22B: - return 0xFEB2F; - case 0xE22C: - return 0xFEB40; - case 0xE22D: - return 0xFEB41; - case 0xE22E: - return 0xFEB99; - case 0xE22F: - return 0xFEB9A; - case 0xE230: - return 0xFEB9B; - case 0xE231: - return 0xFEB9C; - case 0xE232: - return 0xFEAF8; - case 0xE233: - return 0xFEAF9; - case 0xE234: - return 0xFEAFA; - case 0xE235: - return 0xFEAFB; - case 0xE236: - return 0xFEAF0; - case 0xE237: - return 0xFEAF2; - case 0xE238: - return 0xFEAF1; - case 0xE239: - return 0xFEAF3; - case 0xE23A: - return 0xFEAFC; - case 0xE23B: - return 0xFEAFD; - case 0xE23C: - return 0xFEAFE; - case 0xE23D: - return 0xFEAFF; - case 0xE23E: - return 0xFE4F8; - case 0xE23F: - return 0xFE02B; - case 0xE240: - return 0xFE02C; - case 0xE241: - return 0xFE02D; - case 0xE242: - return 0xFE02E; - case 0xE243: - return 0xFE02F; - case 0xE244: - return 0xFE030; - case 0xE245: - return 0xFE031; - case 0xE246: - return 0xFE032; - case 0xE247: - return 0xFE033; - case 0xE248: - return 0xFE034; - case 0xE249: - return 0xFE035; - case 0xE24A: - return 0xFE036; - case 0xE24B: - return 0xFE037; - case 0xE24C: - return 0xFEB42; - case 0xE24D: - return 0xFEB27; - case 0xE24E: - return 0xFEB29; - case 0xE24F: - return 0xFEB2D; - case 0xE250: - return 0xFE839; - case 0xE251: - return 0xFE83A; - case 0xE252: - return 0xFEB23; - case 0xE253: - return 0xFE1B4; - case 0xE254: - return 0xFEE77; - case 0xE255: - return 0xFEE78; - case 0xE256: - return 0xFEE79; - case 0xE257: - return 0xFEE7A; - case 0xE258: - return 0xFEE7B; - case 0xE259: - return 0xFEE7C; - case 0xE25A: - return 0xFEE7D; - case 0xE301: - return 0xFE527; - case 0xE302: - return 0xFE4D3; - case 0xE303: - return 0xFE045; - case 0xE304: - return 0xFE03D; - case 0xE305: - return 0xFE046; - case 0xE306: - return 0xFE828; - case 0xE307: - return 0xFE047; - case 0xE308: - return 0xFE048; - case 0xE309: - return 0xFE508; - case 0xE30A: - return 0xFE803; - case 0xE30B: - return 0xFE985; - case 0xE30C: - return 0xFE987; - case 0xE30D: - return 0xFEB43; - case 0xE30E: - return 0xFEB1E; - case 0xE30F: - return 0xFE50A; - case 0xE310: - return 0xFE516; - case 0xE311: - return 0xFEB58; - case 0xE312: - return 0xFE517; - case 0xE313: - return 0xFE53E; - case 0xE314: - return 0xFE50F; - case 0xE315: - return 0xFEB2B; - case 0xE316: - return 0xFE53C; - case 0xE317: - return 0xFE530; - case 0xE318: - return 0xFE4D4; - case 0xE319: - return 0xFE4D5; - case 0xE31A: - return 0xFE4D7; - case 0xE31B: - return 0xFE4D8; - case 0xE31C: - return 0xFE195; - case 0xE31D: - return 0xFE196; - case 0xE31E: - return 0xFE197; - case 0xE31F: - return 0xFE198; - case 0xE320: - return 0xFE199; - case 0xE321: - return 0xFE4D9; - case 0xE322: - return 0xFE4DA; - case 0xE323: - return 0xFE4F0; - case 0xE324: - return 0xFE808; - case 0xE325: - return 0xFE4F2; - case 0xE326: - return 0xFE814; - case 0xE327: - return 0xFEB0D; - case 0xE328: - return 0xFEB11; - case 0xE329: - return 0xFEB12; - case 0xE32A: - return 0xFEB13; - case 0xE32B: - return 0xFEB14; - case 0xE32C: - return 0xFEB15; - case 0xE32D: - return 0xFEB16; - case 0xE32E: - return 0xFEB60; - case 0xE32F: - return 0xFEB68; - case 0xE330: - return 0xFEB5D; - case 0xE331: - return 0xFEB5B; - case 0xE332: - return 0xFEB44; - case 0xE333: - return 0xFEB45; - case 0xE334: - return 0xFEB57; - case 0xE335: - return 0xFEB69; - case 0xE336: - return 0xFEB0A; - case 0xE337: - return 0xFEB0B; - case 0xE338: - return 0xFE984; - case 0xE339: - return 0xFE964; - case 0xE33A: - return 0xFE966; - case 0xE33B: - return 0xFE967; - case 0xE33C: - return 0xFE968; - case 0xE33D: - return 0xFE969; - case 0xE33E: - return 0xFE96A; - case 0xE33F: - return 0xFE96B; - case 0xE340: - return 0xFE963; - case 0xE341: - return 0xFE96C; - case 0xE342: - return 0xFE961; - case 0xE343: - return 0xFE96D; - case 0xE344: - return 0xFE96E; - case 0xE345: - return 0xFE051; - case 0xE346: - return 0xFE052; - case 0xE347: - return 0xFE053; - case 0xE348: - return 0xFE054; - case 0xE349: - return 0xFE055; - case 0xE34A: - return 0xFE056; - case 0xE34B: - return 0xFE511; - case 0xE34C: - return 0xFE96F; - case 0xE34D: - return 0xFE970; - case 0xE401: - return 0xFE345; - case 0xE402: - return 0xFE343; - case 0xE403: - return 0xFE340; - case 0xE404: - return 0xFE333; - case 0xE405: - return 0xFE347; - case 0xE406: - return 0xFE33C; - case 0xE407: - return 0xFE33F; - case 0xE408: - return 0xFE342; - case 0xE409: - return 0xFE32A; - case 0xE40A: - return 0xFE33E; - case 0xE40B: - return 0xFE33B; - case 0xE40C: - return 0xFE32E; - case 0xE40D: - return 0xFE32F; - case 0xE40E: - return 0xFE326; - case 0xE40F: - return 0xFE325; - case 0xE410: - return 0xFE322; - case 0xE411: - return 0xFE33A; - case 0xE412: - return 0xFE334; - case 0xE413: - return 0xFE339; - case 0xE414: - return 0xFE336; - case 0xE415: - return 0xFE338; - case 0xE416: - return 0xFE33D; - case 0xE417: - return 0xFE32D; - case 0xE418: - return 0xFE32C; - case 0xE419: - return 0xFE190; - case 0xE41A: - return 0xFE192; - case 0xE41B: - return 0xFE191; - case 0xE41C: - return 0xFE193; - case 0xE41D: - return 0xFE35B; - case 0xE41E: - return 0xFEB9D; - case 0xE41F: - return 0xFEB9E; - case 0xE420: - return 0xFEB9F; - case 0xE421: - return 0xFEBA0; - case 0xE422: - return 0xFEBA1; - case 0xE423: - return 0xFE351; - case 0xE424: - return 0xFE352; - case 0xE425: - return 0xFE829; - case 0xE426: - return 0xFE353; - case 0xE427: - return 0xFE358; - case 0xE428: - return 0xFE1A0; - case 0xE429: - return 0xFE1A2; - case 0xE42A: - return 0xFE7D6; - case 0xE42B: - return 0xFE7DD; - case 0xE42C: - return 0xFE80E; - case 0xE42D: - return 0xFE7DE; - case 0xE42E: - return 0xFE7E5; - case 0xE42F: - return 0xFE7F1; - case 0xE430: - return 0xFE7F2; - case 0xE431: - return 0xFE7F3; - case 0xE432: - return 0xFE7F4; - case 0xE433: - return 0xFE7FE; - case 0xE434: - return 0xFE7E0; - case 0xE435: - return 0xFE7E2; - case 0xE436: - return 0xFE518; - case 0xE437: - return 0xFEB17; - case 0xE438: - return 0xFE519; - case 0xE439: - return 0xFE51A; - case 0xE43A: - return 0xFE51B; - case 0xE43B: - return 0xFE51C; - case 0xE43C: - return 0xFE007; - case 0xE43D: - return 0xFE82A; - case 0xE43E: - return 0xFE038; - case 0xE43F: - return 0xFE971; - case 0xE440: - return 0xFE51D; - case 0xE441: - return 0xFE1C6; - case 0xE442: - return 0xFE51E; - case 0xE443: - return 0xFE005; - case 0xE444: - return 0xFE049; - case 0xE445: - return 0xFE51F; - case 0xE446: - return 0xFE017; - case 0xE447: - return 0xFE043; - case 0xE448: - return 0xFE513; - case 0xE449: - return 0xFE00A; - case 0xE44A: - return 0xFE00C; - case 0xE44B: - return 0xFE008; - case 0xE44C: - return 0xFE00D; - case 0xE501: - return 0xFE4B8; - case 0xE502: - return 0xFE804; - case 0xE503: - return 0xFE805; - case 0xE504: - return 0xFE4BD; - case 0xE505: - return 0xFE4BE; - case 0xE506: - return 0xFE4BF; - case 0xE507: - return 0xFE802; - case 0xE508: - return 0xFE4C0; - case 0xE509: - return 0xFE4C4; - case 0xE50A: - return 0xFE4C5; - case 0xE50B: - return 0xFE4E5; - case 0xE50C: - return 0xFE4E6; - case 0xE50D: - return 0xFE4E7; - case 0xE50E: - return 0xFE4E8; - case 0xE50F: - return 0xFE4E9; - case 0xE510: - return 0xFE4EA; - case 0xE511: - return 0xFE4EB; - case 0xE512: - return 0xFE4EC; - case 0xE513: - return 0xFE4ED; - case 0xE514: - return 0xFE4EE; - case 0xE515: - return 0xFE1A4; - case 0xE516: - return 0xFE1A5; - case 0xE517: - return 0xFE1A6; - case 0xE518: - return 0xFE1A7; - case 0xE519: - return 0xFE1A8; - case 0xE51A: - return 0xFE1A9; - case 0xE51B: - return 0xFE1AA; - case 0xE51C: - return 0xFE1AB; - case 0xE51D: - return 0xFE4C6; - case 0xE51E: - return 0xFE1B5; - case 0xE51F: - return 0xFE1B6; - case 0xE520: - return 0xFE1C7; - case 0xE521: - return 0xFE1C8; - case 0xE522: - return 0xFE1C9; - case 0xE523: - return 0xFE1BA; - case 0xE524: - return 0xFE1CA; - case 0xE525: - return 0xFE1CB; - case 0xE526: - return 0xFE1CC; - case 0xE527: - return 0xFE1CD; - case 0xE528: - return 0xFE1CE; - case 0xE529: - return 0xFE1CF; - case 0xE52A: - return 0xFE1D0; - case 0xE52B: - return 0xFE1D1; - case 0xE52C: - return 0xFE1D2; - case 0xE52D: - return 0xFE1D3; - case 0xE52E: - return 0xFE1D4; - case 0xE52F: - return 0xFE1D5; - case 0xE530: - return 0xFE1D6; - case 0xE531: - return 0xFE1D7; - case 0xE532: - return 0xFE50B; - case 0xE533: - return 0xFE50C; - case 0xE534: - return 0xFE50D; - case 0xE535: - return 0xFE50E; - case 0xE536: - return 0xFE553; - case 0xE537: - return 0xFEB2A; - case 0xE538: - return 0xFEE70; - case 0xE539: - return 0xFEE71; - case 0xE53A: - return 0xFEE72; - case 0xE53B: - return 0xFEE73; - case 0xE53C: - return 0xFEE74; - case 0xE53D: - return 0xFEE75; - case 0xE53E: - return 0xFEE76; - default: - return codePoint; - } - } - - public static void setCharset(String charset, Part part) throws MessagingException { - part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, - part.getMimeType() + ";\r\n charset=" + getExternalCharset(charset)); - } - - public static String getExternalCharset(String charset) { - if (charset.length() > 17 && charset.startsWith("x-") && - charset.endsWith("-shift_jis-2007")) - return "shift_jis"; - - return charset; - } - - public static ViewableContainer extractPartsFromDraft(Message message) - throws MessagingException { - - Body body = message.getBody(); - if (message.isMimeType("multipart/mixed") && body instanceof MimeMultipart) { - MimeMultipart multipart = (MimeMultipart) body; - - ViewableContainer container; - int count = multipart.getCount(); - if (count >= 1) { - // The first part is either a text/plain or a multipart/alternative - BodyPart firstPart = multipart.getBodyPart(0); - container = extractTextual(firstPart); - - // The rest should be attachments - for (int i = 1; i < count; i++) { - BodyPart bodyPart = multipart.getBodyPart(i); - container.attachments.add(bodyPart); - } - } else { - container = new ViewableContainer("", "", new ArrayList()); - } - - return container; - } - - return extractTextual(message); - } - - private static ViewableContainer extractTextual(Part part) throws MessagingException { - String text = ""; - String html = ""; - List attachments = new ArrayList(); - - Body firstBody = part.getBody(); - if (part.isMimeType("text/plain")) { - String bodyText = getTextFromPart(part); - if (bodyText != null) { - text = bodyText; - html = HtmlConverter.textToHtml(text); - } - } else if (part.isMimeType("multipart/alternative") && - firstBody instanceof MimeMultipart) { - MimeMultipart multipart = (MimeMultipart) firstBody; - for (BodyPart bodyPart : multipart.getBodyParts()) { - String bodyText = getTextFromPart(bodyPart); - if (bodyText != null) { - if (text.isEmpty() && bodyPart.isMimeType("text/plain")) { - text = bodyText; - } else if (html.isEmpty() && bodyPart.isMimeType("text/html")) { - html = bodyText; - } - } - } - } - - return new ViewableContainer(text, html, attachments); - } } diff --git a/src/com/fsck/k9/mail/internet/TextBody.java b/src/com/fsck/k9/mail/internet/TextBody.java index ad6e474ac..d6355662f 100644 --- a/src/com/fsck/k9/mail/internet/TextBody.java +++ b/src/com/fsck/k9/mail/internet/TextBody.java @@ -4,7 +4,11 @@ package com.fsck.k9.mail.internet; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; import org.apache.james.mime4j.util.MimeUtil; diff --git a/src/com/fsck/k9/mail/internet/Viewable.java b/src/com/fsck/k9/mail/internet/Viewable.java new file mode 100644 index 000000000..6ec32b926 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/Viewable.java @@ -0,0 +1,105 @@ +package com.fsck.k9.mail.internet; + +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Part; + +import java.util.List; + +/** + * Empty marker class interface the class hierarchy used by + * {@link com.fsck.k9.mailstore.LocalMessageExtractor#extractTextAndAttachments(android.content.Context, com.fsck.k9.mail.Message)}. + * + * @see Viewable.Text + * @see Viewable.Html + * @see Viewable.MessageHeader + * @see Viewable.Alternative + */ +public interface Viewable { + /** + * Class representing textual parts of a message that aren't marked as attachments. + * + * @see com.fsck.k9.mail.internet.MessageExtractor#isPartTextualBody(com.fsck.k9.mail.Part) + */ + static abstract class Textual implements Viewable { + private Part mPart; + + public Textual(Part part) { + mPart = part; + } + + public Part getPart() { + return mPart; + } + } + + /** + * Class representing a {@code text/plain} part of a message. + */ + static class Text extends Textual { + public Text(Part part) { + super(part); + } + } + + /** + * Class representing a {@code text/html} part of a message. + */ + static class Html extends Textual { + public Html(Part part) { + super(part); + } + } + + /** + * Class representing a {@code message/rfc822} part of a message. + * + *

+ * This is used to extract basic header information when the message contents are displayed + * inline. + *

+ */ + static class MessageHeader implements Viewable { + private Part mContainerPart; + private Message mMessage; + + public MessageHeader(Part containerPart, Message message) { + mContainerPart = containerPart; + mMessage = message; + } + + public Part getContainerPart() { + return mContainerPart; + } + + public Message getMessage() { + return mMessage; + } + } + + /** + * Class representing a {@code multipart/alternative} part of a message. + * + *

+ * Only relevant {@code text/plain} and {@code text/html} children are stored in this container + * class. + *

+ */ + static class Alternative implements Viewable { + private List mText; + private List mHtml; + + public Alternative(List text, List html) { + mText = text; + mHtml = html; + } + + public List getText() { + return mText; + } + + public List getHtml() { + return mHtml; + } + } + +} diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java index 568f30b5e..29fae5f1c 100644 --- a/src/com/fsck/k9/mail/transport/SmtpTransport.java +++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java @@ -12,7 +12,7 @@ import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.LineWrapOutputStream; import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.filter.SmtpDataStuffing; -import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.internet.CharsetSupport; import com.fsck.k9.mail.store.StoreConfig; import com.fsck.k9.mail.ssl.TrustedSocketFactory; @@ -463,7 +463,7 @@ public class SmtpTransport extends Transport { new HashMap>(); for (Address address : addresses) { String addressString = address.getAddress(); - String charset = MimeUtility.getCharsetFromAddress(addressString); + String charset = CharsetSupport.getCharsetFromAddress(addressString); List addressesOfCharset = charsetAddressesMap.get(charset); if (addressesOfCharset == null) { addressesOfCharset = new ArrayList(); diff --git a/src/com/fsck/k9/mailstore/LocalFolder.java b/src/com/fsck/k9/mailstore/LocalFolder.java index edf81d931..63e958ec9 100644 --- a/src/com/fsck/k9/mailstore/LocalFolder.java +++ b/src/com/fsck/k9/mailstore/LocalFolder.java @@ -33,7 +33,7 @@ import com.fsck.k9.K9; import com.fsck.k9.Account.MessageFormat; import com.fsck.k9.activity.Search; import com.fsck.k9.mail.MessageRetrievalListener; -import com.fsck.k9.mail.internet.HtmlConverter; +import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; @@ -51,11 +51,11 @@ import com.fsck.k9.mail.internet.MimeMessage; 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.mail.internet.MimeUtility.ViewableContainer; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; import com.fsck.k9.provider.AttachmentProvider; + public class LocalFolder extends Folder implements Serializable { private static final long serialVersionUID = -1973296520918624767L; @@ -1299,14 +1299,14 @@ public class LocalFolder extends Folder implements Serializable { // draft messages because this will cause the values stored in // the identity header to be wrong. ViewableContainer container = - MimeUtility.extractPartsFromDraft(message); + LocalMessageExtractor.extractPartsFromDraft(message); text = container.text; html = container.html; attachments = container.attachments; } else { ViewableContainer container = - MimeUtility.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message); + LocalMessageExtractor.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message); attachments = container.attachments; text = container.text; @@ -1412,7 +1412,7 @@ public class LocalFolder extends Folder implements Serializable { message.buildMimeRepresentation(); ViewableContainer container = - MimeUtility.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message); + LocalMessageExtractor.extractTextAndAttachments(LocalFolder.this.localStore.mApplication, message); List attachments = container.attachments; String text = container.text; diff --git a/src/com/fsck/k9/mailstore/LocalMessage.java b/src/com/fsck/k9/mailstore/LocalMessage.java index 464a6b1e9..05dfacec1 100644 --- a/src/com/fsck/k9/mailstore/LocalMessage.java +++ b/src/com/fsck/k9/mailstore/LocalMessage.java @@ -120,16 +120,16 @@ public class LocalMessage extends MimeMessage { */ public String getTextForDisplay() throws MessagingException { String text = null; // First try and fetch an HTML part. - Part part = MimeUtility.findFirstPartByMimeType(this, "text/html"); + Part part = findFirstPartByMimeType("text/html"); if (part == null) { // If that fails, try and get a text part. - part = MimeUtility.findFirstPartByMimeType(this, "text/plain"); + part = findFirstPartByMimeType("text/plain"); if (part != null && part.getBody() instanceof LocalTextBody) { text = ((LocalTextBody) part.getBody()).getBodyForDisplay(); } } else { // We successfully found an HTML part; do the necessary character set decoding. - text = MimeUtility.getTextFromPart(part); + text = part.getText(); } return text; } diff --git a/src/com/fsck/k9/mailstore/LocalMessageExtractor.java b/src/com/fsck/k9/mailstore/LocalMessageExtractor.java new file mode 100644 index 000000000..546c1f62d --- /dev/null +++ b/src/com/fsck/k9/mailstore/LocalMessageExtractor.java @@ -0,0 +1,464 @@ +package com.fsck.k9.mailstore; + +import android.content.Context; + +import com.fsck.k9.R; +import com.fsck.k9.mail.Address; +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.Part; +import com.fsck.k9.helper.HtmlConverter; +import com.fsck.k9.mail.internet.MessageExtractor; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.Viewable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter; + +class LocalMessageExtractor { + private static final String TEXT_DIVIDER = + "------------------------------------------------------------------------"; + private static final int TEXT_DIVIDER_LENGTH = TEXT_DIVIDER.length(); + private static final String FILENAME_PREFIX = "----- "; + private static final int FILENAME_PREFIX_LENGTH = FILENAME_PREFIX.length(); + private static final String FILENAME_SUFFIX = " "; + private static final int FILENAME_SUFFIX_LENGTH = FILENAME_SUFFIX.length(); + + /** + * Extract the viewable textual parts of a message and return the rest as attachments. + * + * @param context A {@link android.content.Context} instance that will be used to get localized strings. + * @return A {@link ViewableContainer} instance containing the textual parts of the message as + * plain text and HTML, and a list of message parts considered attachments. + * + * @throws com.fsck.k9.mail.MessagingException + * In case of an error. + */ + public static ViewableContainer extractTextAndAttachments(Context context, Message message) throws MessagingException { + try { + List attachments = new ArrayList(); + + // Collect all viewable parts + List viewables = MessageExtractor.getViewables(message, attachments); + + /* + * Convert the tree of viewable parts into text and HTML + */ + + // Used to suppress the divider for the first viewable part + boolean hideDivider = true; + + StringBuilder text = new StringBuilder(); + StringBuilder html = new StringBuilder(); + + for (Viewable viewable : viewables) { + if (viewable instanceof Viewable.Textual) { + // This is either a text/plain or text/html part. Fill the variables 'text' and + // 'html', converting between plain text and HTML as necessary. + text.append(buildText(viewable, !hideDivider)); + html.append(buildHtml(viewable, !hideDivider)); + hideDivider = false; + } else if (viewable instanceof Viewable.MessageHeader) { + Viewable.MessageHeader header = (Viewable.MessageHeader) viewable; + Part containerPart = header.getContainerPart(); + Message innerMessage = header.getMessage(); + + addTextDivider(text, containerPart, !hideDivider); + addMessageHeaderText(context, text, innerMessage); + + addHtmlDivider(html, containerPart, !hideDivider); + addMessageHeaderHtml(context, html, innerMessage); + + hideDivider = true; + } else if (viewable instanceof Viewable.Alternative) { + // Handle multipart/alternative contents + Viewable.Alternative alternative = (Viewable.Alternative) viewable; + + /* + * We made sure at least one of text/plain or text/html is present when + * creating the Alternative object. If one part is not present we convert the + * other one to make sure 'text' and 'html' always contain the same text. + */ + List textAlternative = alternative.getText().isEmpty() ? + alternative.getHtml() : alternative.getText(); + List htmlAlternative = alternative.getHtml().isEmpty() ? + alternative.getText() : alternative.getHtml(); + + // Fill the 'text' variable + boolean divider = !hideDivider; + for (Viewable textViewable : textAlternative) { + text.append(buildText(textViewable, divider)); + divider = true; + } + + // Fill the 'html' variable + divider = !hideDivider; + for (Viewable htmlViewable : htmlAlternative) { + html.append(buildHtml(htmlViewable, divider)); + divider = true; + } + hideDivider = false; + } + } + + return new ViewableContainer(text.toString(), html.toString(), attachments); + } catch (Exception e) { + throw new MessagingException("Couldn't extract viewable parts", e); + } + } + + public static ViewableContainer extractPartsFromDraft(Message message) + throws MessagingException { + + Body body = message.getBody(); + if (message.isMimeType("multipart/mixed") && body instanceof MimeMultipart) { + MimeMultipart multipart = (MimeMultipart) body; + + ViewableContainer container; + int count = multipart.getCount(); + if (count >= 1) { + // The first part is either a text/plain or a multipart/alternative + BodyPart firstPart = multipart.getBodyPart(0); + container = extractTextual(firstPart); + + // The rest should be attachments + for (int i = 1; i < count; i++) { + BodyPart bodyPart = multipart.getBodyPart(i); + container.attachments.add(bodyPart); + } + } else { + container = new ViewableContainer("", "", new ArrayList()); + } + + return container; + } + + return extractTextual(message); + } + + /** + * Use the contents of a {@link com.fsck.k9.mail.internet.Viewable} to create the HTML to be displayed. + * + *

+ * This will use {@link com.fsck.k9.helper.HtmlConverter#textToHtml(String)} to convert plain text parts + * to HTML if necessary. + *

+ * + * @param viewable + * The viewable part to build the HTML from. + * @param prependDivider + * {@code true}, if the HTML divider should be inserted as first element. + * {@code false}, otherwise. + * + * @return The contents of the supplied viewable instance as HTML. + */ + private static StringBuilder buildHtml(Viewable viewable, boolean prependDivider) + { + StringBuilder html = new StringBuilder(); + if (viewable instanceof Viewable.Textual) { + Part part = ((Viewable.Textual)viewable).getPart(); + addHtmlDivider(html, part, prependDivider); + + String t = part.getText(); + if (t == null) { + t = ""; + } else if (viewable instanceof Viewable.Text) { + t = HtmlConverter.textToHtml(t); + } + html.append(t); + } else if (viewable instanceof Viewable.Alternative) { + // That's odd - an Alternative as child of an Alternative; go ahead and try to use the + // text/html child; fall-back to the text/plain part. + Viewable.Alternative alternative = (Viewable.Alternative) viewable; + + List htmlAlternative = alternative.getHtml().isEmpty() ? + alternative.getText() : alternative.getHtml(); + + boolean divider = prependDivider; + for (Viewable htmlViewable : htmlAlternative) { + html.append(buildHtml(htmlViewable, divider)); + divider = true; + } + } + + return html; + } + + private static StringBuilder buildText(Viewable viewable, boolean prependDivider) + { + StringBuilder text = new StringBuilder(); + if (viewable instanceof Viewable.Textual) { + Part part = ((Viewable.Textual)viewable).getPart(); + addTextDivider(text, part, prependDivider); + + String t = part.getText(); + if (t == null) { + t = ""; + } else if (viewable instanceof Viewable.Html) { + t = HtmlConverter.htmlToText(t); + } + text.append(t); + } else if (viewable instanceof Viewable.Alternative) { + // That's odd - an Alternative as child of an Alternative; go ahead and try to use the + // text/plain child; fall-back to the text/html part. + Viewable.Alternative alternative = (Viewable.Alternative) viewable; + + List textAlternative = alternative.getText().isEmpty() ? + alternative.getHtml() : alternative.getText(); + + boolean divider = prependDivider; + for (Viewable textViewable : textAlternative) { + text.append(buildText(textViewable, divider)); + divider = true; + } + } + + return text; + } + + /** + * Add an HTML divider between two HTML message parts. + * + * @param html + * The {@link StringBuilder} to append the divider to. + * @param part + * The message part that will follow after the divider. This is used to extract the + * part's name. + * @param prependDivider + * {@code true}, if the divider should be appended. {@code false}, otherwise. + */ + private static void addHtmlDivider(StringBuilder html, Part part, boolean prependDivider) { + if (prependDivider) { + String filename = getPartName(part); + + html.append("

"); + html.append(filename); + html.append("

"); + } + } + + /** + * Get the name of the message part. + * + * @param part + * The part to get the name for. + * + * @return The (file)name of the part if available. An empty string, otherwise. + */ + private static String getPartName(Part part) { + try { + String disposition = part.getDisposition(); + if (disposition != null) { + String name = getHeaderParameter(disposition, "filename"); + return (name == null) ? "" : name; + } + } + catch (MessagingException e) { /* ignore */ } + + return ""; + } + + /** + * Add a plain text divider between two plain text message parts. + * + * @param text + * The {@link StringBuilder} to append the divider to. + * @param part + * The message part that will follow after the divider. This is used to extract the + * part's name. + * @param prependDivider + * {@code true}, if the divider should be appended. {@code false}, otherwise. + */ + private static void addTextDivider(StringBuilder text, Part part, boolean prependDivider) { + if (prependDivider) { + String filename = getPartName(part); + + text.append("\r\n\r\n"); + int len = filename.length(); + if (len > 0) { + if (len > TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - FILENAME_SUFFIX_LENGTH) { + filename = filename.substring(0, TEXT_DIVIDER_LENGTH - FILENAME_PREFIX_LENGTH - + FILENAME_SUFFIX_LENGTH - 3) + "..."; + } + text.append(FILENAME_PREFIX); + text.append(filename); + text.append(FILENAME_SUFFIX); + text.append(TEXT_DIVIDER.substring(0, TEXT_DIVIDER_LENGTH - + FILENAME_PREFIX_LENGTH - filename.length() - FILENAME_SUFFIX_LENGTH)); + } else { + text.append(TEXT_DIVIDER); + } + text.append("\r\n\r\n"); + } + } + + /** + * Extract important header values from a message to display inline (plain text version). + * + * @param context + * A {@link android.content.Context} instance that will be used to get localized strings. + * @param text + * The {@link StringBuilder} that will receive the (plain text) output. + * @param message + * The message to extract the header values from. + * + * @throws com.fsck.k9.mail.MessagingException + * In case of an error. + */ + private static void addMessageHeaderText(Context context, StringBuilder text, Message message) + throws MessagingException { + // From: + Address[] from = message.getFrom(); + if (from != null && from.length > 0) { + text.append(context.getString(R.string.message_compose_quote_header_from)); + text.append(' '); + text.append(Address.toString(from)); + text.append("\r\n"); + } + + // To: + Address[] to = message.getRecipients(Message.RecipientType.TO); + if (to != null && to.length > 0) { + text.append(context.getString(R.string.message_compose_quote_header_to)); + text.append(' '); + text.append(Address.toString(to)); + text.append("\r\n"); + } + + // Cc: + Address[] cc = message.getRecipients(Message.RecipientType.CC); + if (cc != null && cc.length > 0) { + text.append(context.getString(R.string.message_compose_quote_header_cc)); + text.append(' '); + text.append(Address.toString(cc)); + text.append("\r\n"); + } + + // Date: + Date date = message.getSentDate(); + if (date != null) { + text.append(context.getString(R.string.message_compose_quote_header_send_date)); + text.append(' '); + text.append(date.toString()); + text.append("\r\n"); + } + + // Subject: + String subject = message.getSubject(); + text.append(context.getString(R.string.message_compose_quote_header_subject)); + text.append(' '); + if (subject == null) { + text.append(context.getString(R.string.general_no_subject)); + } else { + text.append(subject); + } + text.append("\r\n\r\n"); + } + + /** + * Extract important header values from a message to display inline (HTML version). + * + * @param context + * A {@link android.content.Context} instance that will be used to get localized strings. + * @param html + * The {@link StringBuilder} that will receive the (HTML) output. + * @param message + * The message to extract the header values from. + * + * @throws com.fsck.k9.mail.MessagingException + * In case of an error. + */ + private static void addMessageHeaderHtml(Context context, StringBuilder html, Message message) + throws MessagingException { + + html.append(""); + + // From: + Address[] from = message.getFrom(); + if (from != null && from.length > 0) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_from), + Address.toString(from)); + } + + // To: + Address[] to = message.getRecipients(Message.RecipientType.TO); + if (to != null && to.length > 0) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_to), + Address.toString(to)); + } + + // Cc: + Address[] cc = message.getRecipients(Message.RecipientType.CC); + if (cc != null && cc.length > 0) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_cc), + Address.toString(cc)); + } + + // Date: + Date date = message.getSentDate(); + if (date != null) { + addTableRow(html, context.getString(R.string.message_compose_quote_header_send_date), + date.toString()); + } + + // Subject: + String subject = message.getSubject(); + addTableRow(html, context.getString(R.string.message_compose_quote_header_subject), + (subject == null) ? context.getString(R.string.general_no_subject) : subject); + + html.append("
"); + } + + /** + * Output an HTML table two column row with some hardcoded style. + * + * @param html + * The {@link StringBuilder} that will receive the output. + * @param header + * The string to be put in the {@code TH} element. + * @param value + * The string to be put in the {@code TD} element. + */ + private static void addTableRow(StringBuilder html, String header, String value) { + html.append(""); + html.append(header); + html.append(""); + html.append(""); + html.append(value); + html.append(""); + } + + private static ViewableContainer extractTextual(Part part) throws MessagingException { + String text = ""; + String html = ""; + List attachments = new ArrayList(); + + Body firstBody = part.getBody(); + if (part.isMimeType("text/plain")) { + String bodyText = part.getText(); + if (bodyText != null) { + text = bodyText; + html = HtmlConverter.textToHtml(text); + } + } else if (part.isMimeType("multipart/alternative") && + firstBody instanceof MimeMultipart) { + MimeMultipart multipart = (MimeMultipart) firstBody; + for (BodyPart bodyPart : multipart.getBodyParts()) { + String bodyText = bodyPart.getText(); + if (bodyText != null) { + if (text.isEmpty() && bodyPart.isMimeType("text/plain")) { + text = bodyText; + } else if (html.isEmpty() && bodyPart.isMimeType("text/html")) { + html = bodyText; + } + } + } + } + return new ViewableContainer(text, html, attachments); + } +} diff --git a/src/com/fsck/k9/mailstore/ViewableContainer.java b/src/com/fsck/k9/mailstore/ViewableContainer.java new file mode 100644 index 000000000..2e538cd22 --- /dev/null +++ b/src/com/fsck/k9/mailstore/ViewableContainer.java @@ -0,0 +1,34 @@ +package com.fsck.k9.mailstore; + +import com.fsck.k9.mail.Part; + +import java.util.List; + +/** + * Store viewable text of a message as plain text and HTML, and the parts considered + * attachments. + * + * @see LocalMessageExtractor#extractTextAndAttachments(android.content.Context, com.fsck.k9.mail.Message) + */ +class ViewableContainer { + /** + * The viewable text of the message in plain text. + */ + public final String text; + + /** + * The viewable text of the message in HTML. + */ + public final String html; + + /** + * The parts of the message considered attachments (everything not viewable). + */ + public final List attachments; + + public ViewableContainer(String text, String html, List attachments) { + this.text = text; + this.html = html; + this.attachments = attachments; + } +} diff --git a/src/com/fsck/k9/view/MessageOpenPgpView.java b/src/com/fsck/k9/view/MessageOpenPgpView.java index 1b224112f..65bff60c8 100644 --- a/src/com/fsck/k9/view/MessageOpenPgpView.java +++ b/src/com/fsck/k9/view/MessageOpenPgpView.java @@ -215,8 +215,7 @@ public class MessageOpenPgpView extends LinearLayout { } else { try { // check for PGP/MIME encryption - Part pgp = MimeUtility - .findFirstPartByMimeType(message, "application/pgp-encrypted"); + Part pgp = message.findFirstPartByMimeType("application/pgp-encrypted"); if (pgp != null) { Toast.makeText(mContext, R.string.pgp_mime_unsupported, Toast.LENGTH_LONG) .show(); @@ -241,12 +240,12 @@ public class MessageOpenPgpView extends LinearLayout { public void run() { try { // get data String - Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + Part part = message.findFirstPartByMimeType("text/plain"); if (part == null) { - part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + part = message.findFirstPartByMimeType("text/html"); } if (part != null) { - mData = MimeUtility.getTextFromPart(part); + mData = part.getText(); } // wait for service to be bound diff --git a/src/com/fsck/k9/view/MessageWebView.java b/src/com/fsck/k9/view/MessageWebView.java index 07faaf3e1..3198d4723 100644 --- a/src/com/fsck/k9/view/MessageWebView.java +++ b/src/com/fsck/k9/view/MessageWebView.java @@ -10,7 +10,7 @@ import android.widget.Toast; import com.fsck.k9.K9; import com.fsck.k9.R; -import com.fsck.k9.mail.internet.HtmlConverter; +import com.fsck.k9.helper.HtmlConverter; public class MessageWebView extends RigidWebView { diff --git a/src/com/fsck/k9/view/SingleMessageView.java b/src/com/fsck/k9/view/SingleMessageView.java index 3ebdd89a3..25852ee17 100644 --- a/src/com/fsck/k9/view/SingleMessageView.java +++ b/src/com/fsck/k9/view/SingleMessageView.java @@ -46,7 +46,7 @@ import com.fsck.k9.fragment.MessageViewFragment; import com.fsck.k9.helper.ClipboardManager; import com.fsck.k9.helper.Contacts; import com.fsck.k9.helper.FileHelper; -import com.fsck.k9.mail.internet.HtmlConverter; +import com.fsck.k9.helper.HtmlConverter; import com.fsck.k9.helper.UrlEncodingHelper; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.Address; diff --git a/tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java b/tests-on-jvm/src/com/fsck/k9/activity/TextBodyBuilderTest.java similarity index 99% rename from tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java rename to tests-on-jvm/src/com/fsck/k9/activity/TextBodyBuilderTest.java index 7c3b096dd..1a924ea0c 100644 --- a/tests-on-jvm/src/com/fsck/k9/mail/internet/TextBodyBuilderTest.java +++ b/tests-on-jvm/src/com/fsck/k9/activity/TextBodyBuilderTest.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.internet; +package com.fsck.k9.activity; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -8,6 +8,7 @@ import org.junit.runner.RunWith; import com.fsck.k9.Account.QuoteStyle; import com.fsck.k9.activity.TextBodyBuilder; +import com.fsck.k9.mail.internet.TextBody; class TestingTextBodyBuilder extends TextBodyBuilder { diff --git a/tests-on-jvm/src/com/fsck/k9/mail/internet/CharsetSupportTest.java b/tests-on-jvm/src/com/fsck/k9/mail/internet/CharsetSupportTest.java new file mode 100644 index 000000000..1b314642c --- /dev/null +++ b/tests-on-jvm/src/com/fsck/k9/mail/internet/CharsetSupportTest.java @@ -0,0 +1,94 @@ +package com.fsck.k9.mail.internet; + +import junit.framework.TestCase; + +public class CharsetSupportTest extends TestCase { + + public void testFixupCharset() throws Exception { + String charsetOnMail; + String expect; + + charsetOnMail = "CP932"; + expect = "shift_jis"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, new MimeMessage())); + +// charsetOnMail = "koi8-u"; +// expect = "koi8-r"; +// assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, new MimeMessage())); + + MimeMessage message; + + message= new MimeMessage(); + message.setHeader("From", "aaa@docomo.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-docomo-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@dwmail.jp"); + charsetOnMail = "shift_jis"; + expect = "x-docomo-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@pdx.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-docomo-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@willcom.com"); + charsetOnMail = "shift_jis"; + expect = "x-docomo-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@emnet.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-docomo-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@emobile.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-docomo-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@softbank.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-softbank-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@vodafone.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-softbank-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@disney.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-softbank-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@vertuclub.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-softbank-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@ezweb.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-kddi-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + message = new MimeMessage(); + message.setHeader("From", "aaa@ido.ne.jp"); + charsetOnMail = "shift_jis"; + expect = "x-kddi-shift_jis-2007"; + assertEquals(expect, CharsetSupport.fixupCharset(charsetOnMail, message)); + + } +} \ No newline at end of file diff --git a/tests-on-jvm/src/com/fsck/k9/mail/internet/MimeUtilityTest.java b/tests-on-jvm/src/com/fsck/k9/mail/internet/MimeUtilityTest.java index e723cb562..fe07dddcb 100644 --- a/tests-on-jvm/src/com/fsck/k9/mail/internet/MimeUtilityTest.java +++ b/tests-on-jvm/src/com/fsck/k9/mail/internet/MimeUtilityTest.java @@ -8,15 +8,6 @@ import com.fsck.k9.mail.MessagingException; import junit.framework.TestCase; public class MimeUtilityTest extends TestCase { - - protected void setUp() throws Exception { - super.setUp(); - } - - protected void tearDown() throws Exception { - super.tearDown(); - } - public void testGetHeaderParameter() { String result; @@ -55,93 +46,4 @@ public class MimeUtilityTest extends TestCase { result = MimeUtility.getHeaderParameter("text/HTML ; charset=\"windows-1251\"", null); assertEquals("text/HTML", result); } - - public void testFixupCharset() throws MessagingException { - String charsetOnMail; - String expect; - - charsetOnMail = "CP932"; - expect = "shift_jis"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, new MimeMessage())); - -// charsetOnMail = "koi8-u"; -// expect = "koi8-r"; -// assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, new MimeMessage())); - - MimeMessage message; - - message= new MimeMessage(); - message.setHeader("From", "aaa@docomo.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-docomo-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@dwmail.jp"); - charsetOnMail = "shift_jis"; - expect = "x-docomo-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@pdx.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-docomo-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@willcom.com"); - charsetOnMail = "shift_jis"; - expect = "x-docomo-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@emnet.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-docomo-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@emobile.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-docomo-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@softbank.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-softbank-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@vodafone.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-softbank-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@disney.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-softbank-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@vertuclub.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-softbank-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@ezweb.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-kddi-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - message = new MimeMessage(); - message.setHeader("From", "aaa@ido.ne.jp"); - charsetOnMail = "shift_jis"; - expect = "x-kddi-shift_jis-2007"; - assertEquals(expect, MimeUtility.fixupCharset(charsetOnMail, message)); - - } - } diff --git a/tests/src/com/fsck/k9/mail/internet/HtmlConverterTest.java b/tests/src/com/fsck/k9/helper/HtmlConverterTest.java similarity index 99% rename from tests/src/com/fsck/k9/mail/internet/HtmlConverterTest.java rename to tests/src/com/fsck/k9/helper/HtmlConverterTest.java index 7faea3b18..32aa241ed 100644 --- a/tests/src/com/fsck/k9/mail/internet/HtmlConverterTest.java +++ b/tests/src/com/fsck/k9/helper/HtmlConverterTest.java @@ -1,4 +1,6 @@ -package com.fsck.k9.mail.internet; +package com.fsck.k9.helper; + +import com.fsck.k9.helper.HtmlConverter; import junit.framework.TestCase; diff --git a/tests/src/com/fsck/k9/mail/MessageTest.java b/tests/src/com/fsck/k9/mail/MessageTest.java index 3e56b228d..1fabfc534 100644 --- a/tests/src/com/fsck/k9/mail/MessageTest.java +++ b/tests/src/com/fsck/k9/mail/MessageTest.java @@ -14,11 +14,11 @@ import android.test.AndroidTestCase; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.internet.BinaryTempFileBody; import com.fsck.k9.mail.internet.BinaryTempFileMessageBody; +import com.fsck.k9.mail.internet.CharsetSupport; 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.MimeMultipart; -import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; public class MessageTest extends AndroidTestCase { @@ -356,7 +356,7 @@ public class MessageTest extends AndroidTestCase { + "End of test.\r\n"); textBody.setCharset("utf-8"); MimeBodyPart bodyPart = new MimeBodyPart(textBody, "text/plain"); - MimeUtility.setCharset("utf-8", bodyPart); + CharsetSupport.setCharset("utf-8", bodyPart); bodyPart.setEncoding(encoding); return bodyPart; } diff --git a/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java b/tests/src/com/fsck/k9/mailstore/LocalMessageExtractorTest.java similarity index 89% rename from tests/src/com/fsck/k9/mail/internet/ViewablesTest.java rename to tests/src/com/fsck/k9/mailstore/LocalMessageExtractorTest.java index cceb4bff5..2fc007caf 100644 --- a/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java +++ b/tests/src/com/fsck/k9/mailstore/LocalMessageExtractorTest.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.internet; +package com.fsck.k9.mailstore; import java.util.Date; import java.util.Locale; @@ -9,9 +9,14 @@ import com.fsck.k9.activity.K9ActivityCommon; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Message.RecipientType; -import com.fsck.k9.mail.internet.MimeUtility.ViewableContainer; +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.TextBody; -public class ViewablesTest extends AndroidTestCase { +import static com.fsck.k9.mailstore.LocalMessageExtractor.extractTextAndAttachments; + +public class LocalMessageExtractorTest extends AndroidTestCase { public void testSimplePlainTextMessage() throws MessagingException { String bodyText = "K-9 Mail rocks :>"; @@ -24,7 +29,7 @@ public class ViewablesTest extends AndroidTestCase { message.setBody(body); // Extract text - ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); + ViewableContainer container = extractTextAndAttachments(getContext(), message); String expectedText = bodyText; String expectedHtml = @@ -48,7 +53,7 @@ public class ViewablesTest extends AndroidTestCase { message.setBody(body); // Extract text - ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); + ViewableContainer container = extractTextAndAttachments(getContext(), message); String expectedText = "K-9 Mail rocks :>"; String expectedHtml = @@ -78,7 +83,7 @@ public class ViewablesTest extends AndroidTestCase { message.setBody(multipart); // Extract text - ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); + ViewableContainer container = extractTextAndAttachments(getContext(), message); String expectedText = bodyText1 + "\r\n\r\n" + @@ -134,7 +139,7 @@ public class ViewablesTest extends AndroidTestCase { message.setBody(multipart); // Extract text - ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); + ViewableContainer container = extractTextAndAttachments(getContext(), message); String expectedText = bodyText +