From 14fe30f3bfa8c16dc8ec48e29cc4a4f23bb3c3cd Mon Sep 17 00:00:00 2001 From: Josh Guilfoyle Date: Sun, 26 Oct 2008 23:18:09 -0700 Subject: [PATCH 01/11] Perform case-insensitive matches in mimeTypeMatches to support User-Agent's which use uppercased Content-Type. (fixed formatting) --- src/com/android/email/mail/internet/MimeUtility.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/com/android/email/mail/internet/MimeUtility.java b/src/com/android/email/mail/internet/MimeUtility.java index 46e3eb2ee..66b2a7e9f 100644 --- a/src/com/android/email/mail/internet/MimeUtility.java +++ b/src/com/android/email/mail/internet/MimeUtility.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.decoder.Base64InputStream; @@ -189,7 +190,9 @@ public class MimeUtility { * @return */ public static boolean mimeTypeMatches(String mimeType, String matchAgainst) { - return mimeType.matches(matchAgainst.replaceAll("\\*", "\\.\\*")); + Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"), + Pattern.CASE_INSENSITIVE); + return p.matcher(mimeType).matches(); } /** @@ -201,7 +204,7 @@ public class MimeUtility { */ public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) { for (String matchType : matchAgainst) { - if (mimeType.matches(matchType.replaceAll("\\*", "\\.\\*"))) { + if (mimeTypeMatches(mimeType, matchType)) { return true; } } From 8aeb922462694d22f42c5890e5434e9de9dc3987 Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Thu, 19 Mar 2009 23:08:56 -0700 Subject: [PATCH 02/11] auto import from //branches/cupcake_rel/...@141571 --- res/values-ja/strings.xml | 4 +- .../email/mail/internet/MimeMessage.java | 3 +- .../email/mail/internet/MimeUtility.java | 117 +++- .../james/mime4j/codec/EncoderUtil.java | 626 ++++++++++++++++++ .../james/mime4j/decoder/DecoderUtil.java | 234 ++++--- .../apache/james/mime4j/util/CharsetUtil.java | 67 ++ .../email/mail/internet/MimeMessageTest.java | 98 ++- .../email/mail/internet/MimeUtilityTest.java | 115 +++- 8 files changed, 1130 insertions(+), 134 deletions(-) create mode 100644 src/org/apache/james/mime4j/codec/EncoderUtil.java diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index 1c7c95840..24d598073 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -16,7 +16,7 @@ "メールの添付ファイルを読み取る" - "メールの添付ファイルの表示をこのアプリケーションに許可します。" + "Eメールの添付ファイルの読取表示をこのアプリケーションに許可します。" "メール" "アカウント" "作成" @@ -167,7 +167,7 @@ "名前" "通知設定" "バイブレーション" - "メール受信: バイブレーションもON" + "メール受信: バイブレーションON" "着信音を選択" "サーバーの設定" "アカウントを削除" diff --git a/src/com/android/email/mail/internet/MimeMessage.java b/src/com/android/email/mail/internet/MimeMessage.java index 27d7aadb4..371c5b1de 100644 --- a/src/com/android/email/mail/internet/MimeMessage.java +++ b/src/com/android/email/mail/internet/MimeMessage.java @@ -247,7 +247,8 @@ public class MimeMessage extends Message { } public void setSubject(String subject) throws MessagingException { - setHeader("Subject", subject); + final int HEADER_NAME_LENGTH = 9; // "Subject: " + setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH)); } public Address[] getFrom() throws MessagingException { diff --git a/src/com/android/email/mail/internet/MimeUtility.java b/src/com/android/email/mail/internet/MimeUtility.java index 7aa740221..008b60adf 100644 --- a/src/com/android/email/mail/internet/MimeUtility.java +++ b/src/com/android/email/mail/internet/MimeUtility.java @@ -25,6 +25,7 @@ import com.android.email.mail.Multipart; import com.android.email.mail.Part; import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.codec.EncoderUtil; import org.apache.james.mime4j.decoder.Base64InputStream; import org.apache.james.mime4j.decoder.DecoderUtil; import org.apache.james.mime4j.decoder.QuotedPrintableInputStream; @@ -37,14 +38,27 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.regex.Matcher; import java.util.regex.Pattern; public class MimeUtility { + + private final static Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n"); + + /** + * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string + * object whenever possible. + */ public static String unfold(String s) { if (s == null) { return null; } - return s.replaceAll("\r|\n", ""); + Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s); + if (patternMatcher.find()) { + patternMatcher.reset(); + s = patternMatcher.replaceAll(""); + } + return s; } public static String decode(String s) { @@ -59,9 +73,99 @@ public class MimeUtility { } // TODO implement proper foldAndEncode + // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent + // duplication of encoding. public static String foldAndEncode(String s) { return s; } + + /** + * INTERIM version of foldAndEncode that will be used only by Subject: headers. + * This is safer than implementing foldAndEncode() (see above) and risking unknown damage + * to other headers. + * + * TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK. + * + * @param s original string to encode and fold + * @param usedCharacters number of characters already used up by header name + + * @return the String ready to be transmitted + */ + public static String foldAndEncode2(String s, int usedCharacters) { + // james.mime4j.codec.EncoderUtil.java + // encode: encodeIfNecessary(text, usage, numUsedInHeaderName) + // Usage.TEXT_TOKENlooks like the right thing for subjects + // use WORD_ENTITY for address/names + + String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN, + usedCharacters); + + return fold(encoded, usedCharacters); + } + + /** + * INTERIM: From newer version of org.apache.james (but we don't want to import + * the entire MimeUtil class). + * + * Splits the specified string into a multiple-line representation with + * lines no longer than 76 characters (because the line might contain + * encoded words; see RFC + * 2047 section 2). If the string contains non-whitespace sequences + * longer than 76 characters a line break is inserted at the whitespace + * character following the sequence resulting in a line longer than 76 + * characters. + * + * @param s + * string to split. + * @param usedCharacters + * number of characters already used up. Usually the number of + * characters for header field name plus colon and one space. + * @return a multiple-line representation of the given string. + */ + public static String fold(String s, int usedCharacters) { + final int maxCharacters = 76; + + final int length = s.length(); + if (usedCharacters + length <= maxCharacters) + return s; + + StringBuilder sb = new StringBuilder(); + + int lastLineBreak = -usedCharacters; + int wspIdx = indexOfWsp(s, 0); + while (true) { + if (wspIdx == length) { + sb.append(s.substring(Math.max(0, lastLineBreak))); + return sb.toString(); + } + + int nextWspIdx = indexOfWsp(s, wspIdx + 1); + + if (nextWspIdx - lastLineBreak > maxCharacters) { + sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx)); + sb.append("\r\n"); + lastLineBreak = wspIdx; + } + + wspIdx = nextWspIdx; + } + } + + /** + * INTERIM: From newer version of org.apache.james (but we don't want to import + * the entire MimeUtil class). + * + * Search for whitespace. + */ + private static int indexOfWsp(String s, int fromIndex) { + final int len = s.length(); + for (int index = fromIndex; index < len; index++) { + char c = s.charAt(index); + if (c == ' ' || c == '\t') + return index; + } + return len; + } /** * Returns the named parameter of a header field. If name is null the first @@ -69,6 +173,11 @@ public class MimeUtility { * 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. + * + * TODO: quite inefficient with the inner trimming & splitting. + * TODO: Also has a latent bug: uses "startsWith" to match the name, which can false-positive. + * TODO: The doc says that for a null name you get the first param, but you get the header. + * Should probably just fix the doc, but if other code assumes that behavior, fix the code. * * @param header * @param name @@ -78,13 +187,13 @@ public class MimeUtility { if (header == null) { return null; } - header = header.replaceAll("\r|\n", ""); - String[] parts = header.split(";"); + String[] parts = unfold(header).split(";"); if (name == null) { return parts[0]; } + String lowerCaseName = name.toLowerCase(); for (String part : parts) { - if (part.trim().toLowerCase().startsWith(name.toLowerCase())) { + if (part.trim().toLowerCase().startsWith(lowerCaseName)) { String parameter = part.split("=", 2)[1].trim(); if (parameter.startsWith("\"") && parameter.endsWith("\"")) { return parameter.substring(1, parameter.length() - 1); diff --git a/src/org/apache/james/mime4j/codec/EncoderUtil.java b/src/org/apache/james/mime4j/codec/EncoderUtil.java new file mode 100644 index 000000000..d6f3998d4 --- /dev/null +++ b/src/org/apache/james/mime4j/codec/EncoderUtil.java @@ -0,0 +1,626 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.codec; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.BitSet; +import java.util.Locale; + +import org.apache.james.mime4j.util.CharsetUtil; + +/** + * ANDROID: THIS CLASS IS COPIED FROM A NEWER VERSION OF MIME4J + */ + +/** + * Static methods for encoding header field values. This includes encoded-words + * as defined in RFC 2047 + * or display-names of an e-mail address, for example. + * + */ +public class EncoderUtil { + + // This array is a lookup table that translates 6-bit positive integer index + // values into their "Base64 Alphabet" equivalents as specified in Table 1 + // of RFC 2045. + // ANDROID: THIS TABLE IS COPIED FROM BASE64OUTPUTSTREAM + static final byte[] BASE64_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', + 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', '+', '/' }; + + // Byte used to pad output. + private static final byte BASE64_PAD = '='; + + private static final BitSet Q_REGULAR_CHARS = initChars("=_?"); + + private static final BitSet Q_RESTRICTED_CHARS = initChars("=_?\"#$%&'(),.:;<>@[\\]^`{|}~"); + + private static final int MAX_USED_CHARACTERS = 50; + + private static final String ENC_WORD_PREFIX = "=?"; + private static final String ENC_WORD_SUFFIX = "?="; + + private static final int ENCODED_WORD_MAX_LENGTH = 75; // RFC 2047 + + private static final BitSet TOKEN_CHARS = initChars("()<>@,;:\\\"/[]?="); + + private static final BitSet ATEXT_CHARS = initChars("()<>@.,;:\\\"[]"); + + private static BitSet initChars(String specials) { + BitSet bs = new BitSet(128); + for (char ch = 33; ch < 127; ch++) { + if (specials.indexOf(ch) == -1) { + bs.set(ch); + } + } + return bs; + } + + /** + * Selects one of the two encodings specified in RFC 2047. + */ + public enum Encoding { + /** The B encoding (identical to base64 defined in RFC 2045). */ + B, + /** The Q encoding (similar to quoted-printable defined in RFC 2045). */ + Q + } + + /** + * Indicates the intended usage of an encoded word. + */ + public enum Usage { + /** + * Encoded word is used to replace a 'text' token in any Subject or + * Comments header field. + */ + TEXT_TOKEN, + /** + * Encoded word is used to replace a 'word' entity within a 'phrase', + * for example, one that precedes an address in a From, To, or Cc + * header. + */ + WORD_ENTITY + } + + private EncoderUtil() { + } + + /** + * Encodes the display-name portion of an address. See RFC 5322 section 3.4 + * and RFC 2047 section + * 5.3. The specified string should not be folded. + * + * @param displayName + * display-name to encode. + * @return encoded display-name. + */ + public static String encodeAddressDisplayName(String displayName) { + // display-name = phrase + // phrase = 1*( encoded-word / word ) + // word = atom / quoted-string + // atom = [CFWS] 1*atext [CFWS] + // CFWS = comment or folding white space + + if (isAtomPhrase(displayName)) { + return displayName; + } else if (hasToBeEncoded(displayName, 0)) { + return encodeEncodedWord(displayName, Usage.WORD_ENTITY); + } else { + return quote(displayName); + } + } + + /** + * Encodes the local part of an address specification as described in RFC + * 5322 section 3.4.1. Leading and trailing CFWS should have been removed + * before calling this method. The specified string should not contain any + * illegal (control or non-ASCII) characters. + * + * @param localPart + * the local part to encode + * @return the encoded local part. + */ + public static String encodeAddressLocalPart(String localPart) { + // local-part = dot-atom / quoted-string + // dot-atom = [CFWS] dot-atom-text [CFWS] + // CFWS = comment or folding white space + + if (isDotAtomText(localPart)) { + return localPart; + } else { + return quote(localPart); + } + } + + /** + * Encodes the specified strings into a header parameter as described in RFC + * 2045 section 5.1 and RFC 2183 section 2. The specified strings should not + * contain any illegal (control or non-ASCII) characters. + * + * @param name + * parameter name. + * @param value + * parameter value. + * @return encoded result. + */ + public static String encodeHeaderParameter(String name, String value) { + name = name.toLowerCase(Locale.US); + + // value := token / quoted-string + if (isToken(value)) { + return name + "=" + value; + } else { + return name + "=" + quote(value); + } + } + + /** + * Shortcut method that encodes the specified text into an encoded-word if + * the text has to be encoded. + * + * @param text + * text to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @param usedCharacters + * number of characters already used up (0 <= usedCharacters <= 50). + * @return the specified text if encoding is not necessary or an encoded + * word or a sequence of encoded words otherwise. + */ + public static String encodeIfNecessary(String text, Usage usage, + int usedCharacters) { + if (hasToBeEncoded(text, usedCharacters)) + return encodeEncodedWord(text, usage, usedCharacters); + else + return text; + } + + /** + * Determines if the specified string has to encoded into an encoded-word. + * Returns true if the text contains characters that don't + * fall into the printable ASCII character set or if the text contains a + * 'word' (sequence of non-whitespace characters) longer than 77 characters + * (including characters already used up in the line). + * + * @param text + * text to analyze. + * @param usedCharacters + * number of characters already used up (0 <= usedCharacters <= 50). + * @return true if the specified text has to be encoded into + * an encoded-word, false otherwise. + */ + public static boolean hasToBeEncoded(String text, int usedCharacters) { + if (text == null) + throw new IllegalArgumentException(); + if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS) + throw new IllegalArgumentException(); + + int nonWhiteSpaceCount = usedCharacters; + + for (int idx = 0; idx < text.length(); idx++) { + char ch = text.charAt(idx); + if (ch == '\t' || ch == ' ') { + nonWhiteSpaceCount = 0; + } else { + nonWhiteSpaceCount++; + if (nonWhiteSpaceCount > 77) { + // Line cannot be folded into multiple lines with no more + // than 78 characters each. Encoding as encoded-words makes + // that possible. One character has to be reserved for + // folding white space; that leaves 77 characters. + return true; + } + + if (ch < 32 || ch >= 127) { + // non-printable ascii character has to be encoded + return true; + } + } + } + + return false; + } + + /** + * Encodes the specified text into an encoded word or a sequence of encoded + * words separated by space. The text is separated into a sequence of + * encoded words if it does not fit in a single one. + *

+ * The charset to encode the specified text into a byte array and the + * encoding to use for the encoded-word are detected automatically. + *

+ * This method assumes that zero characters have already been used up in the + * current line. + * + * @param text + * text to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @return the encoded word (or sequence of encoded words if the given text + * does not fit in a single encoded word). + * @see #hasToBeEncoded(String, int) + */ + public static String encodeEncodedWord(String text, Usage usage) { + return encodeEncodedWord(text, usage, 0, null, null); + } + + /** + * Encodes the specified text into an encoded word or a sequence of encoded + * words separated by space. The text is separated into a sequence of + * encoded words if it does not fit in a single one. + *

+ * The charset to encode the specified text into a byte array and the + * encoding to use for the encoded-word are detected automatically. + * + * @param text + * text to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @param usedCharacters + * number of characters already used up (0 <= usedCharacters <= 50). + * @return the encoded word (or sequence of encoded words if the given text + * does not fit in a single encoded word). + * @see #hasToBeEncoded(String, int) + */ + public static String encodeEncodedWord(String text, Usage usage, + int usedCharacters) { + return encodeEncodedWord(text, usage, usedCharacters, null, null); + } + + /** + * Encodes the specified text into an encoded word or a sequence of encoded + * words separated by space. The text is separated into a sequence of + * encoded words if it does not fit in a single one. + * + * @param text + * text to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @param usedCharacters + * number of characters already used up (0 <= usedCharacters <= 50). + * @param charset + * the Java charset that should be used to encode the specified + * string into a byte array. A suitable charset is detected + * automatically if this parameter is null. + * @param encoding + * the encoding to use for the encoded-word (either B or Q). A + * suitable encoding is automatically chosen if this parameter is + * null. + * @return the encoded word (or sequence of encoded words if the given text + * does not fit in a single encoded word). + * @see #hasToBeEncoded(String, int) + */ + public static String encodeEncodedWord(String text, Usage usage, + int usedCharacters, Charset charset, Encoding encoding) { + if (text == null) + throw new IllegalArgumentException(); + if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS) + throw new IllegalArgumentException(); + + if (charset == null) + charset = determineCharset(text); + + String mimeCharset = CharsetUtil.toMimeCharset(charset.name()); + if (mimeCharset == null) { + // cannot happen if charset was originally null + throw new IllegalArgumentException("Unsupported charset"); + } + + byte[] bytes = encode(text, charset); + + if (encoding == null) + encoding = determineEncoding(bytes, usage); + + if (encoding == Encoding.B) { + String prefix = ENC_WORD_PREFIX + mimeCharset + "?B?"; + return encodeB(prefix, text, usedCharacters, charset, bytes); + } else { + String prefix = ENC_WORD_PREFIX + mimeCharset + "?Q?"; + return encodeQ(prefix, text, usage, usedCharacters, charset, bytes); + } + } + + /** + * Encodes the specified byte array using the B encoding defined in RFC + * 2047. + * + * @param bytes + * byte array to encode. + * @return encoded string. + */ + public static String encodeB(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + + int idx = 0; + final int end = bytes.length; + for (; idx < end - 2; idx += 3) { + int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8 + | bytes[idx + 2] & 0xff; + sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]); + sb.append((char) BASE64_TABLE[data & 0x3f]); + } + + if (idx == end - 2) { + int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8; + sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]); + sb.append(BASE64_PAD); + + } else if (idx == end - 1) { + int data = (bytes[idx] & 0xff) << 16; + sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]); + sb.append(BASE64_PAD); + sb.append(BASE64_PAD); + } + + return sb.toString(); + } + + /** + * Encodes the specified byte array using the Q encoding defined in RFC + * 2047. + * + * @param bytes + * byte array to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @return encoded string. + */ + public static String encodeQ(byte[] bytes, Usage usage) { + BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS + : Q_RESTRICTED_CHARS; + + StringBuilder sb = new StringBuilder(); + + final int end = bytes.length; + for (int idx = 0; idx < end; idx++) { + int v = bytes[idx] & 0xff; + if (v == 32) { + sb.append('_'); + } else if (!qChars.get(v)) { + sb.append('='); + sb.append(hexDigit(v >>> 4)); + sb.append(hexDigit(v & 0xf)); + } else { + sb.append((char) v); + } + } + + return sb.toString(); + } + + /** + * Tests whether the specified string is a token as defined in RFC 2045 + * section 5.1. + * + * @param str + * string to test. + * @return true if the specified string is a RFC 2045 token, + * false otherwise. + */ + public static boolean isToken(String str) { + // token := 1* + // tspecials := "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / + // <"> / "/" / "[" / "]" / "?" / "=" + // CTL := 0.- 31., 127. + + final int length = str.length(); + if (length == 0) + return false; + + for (int idx = 0; idx < length; idx++) { + char ch = str.charAt(idx); + if (!TOKEN_CHARS.get(ch)) + return false; + } + + return true; + } + + private static boolean isAtomPhrase(String str) { + // atom = [CFWS] 1*atext [CFWS] + + boolean containsAText = false; + + final int length = str.length(); + for (int idx = 0; idx < length; idx++) { + char ch = str.charAt(idx); + if (ATEXT_CHARS.get(ch)) { + containsAText = true; + } else if (!CharsetUtil.isWhitespace(ch)) { + return false; + } + } + + return containsAText; + } + + // RFC 5322 section 3.2.3 + private static boolean isDotAtomText(String str) { + // dot-atom-text = 1*atext *("." 1*atext) + // atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / + // "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" + + char prev = '.'; + + final int length = str.length(); + if (length == 0) + return false; + + for (int idx = 0; idx < length; idx++) { + char ch = str.charAt(idx); + + if (ch == '.') { + if (prev == '.' || idx == length - 1) + return false; + } else { + if (!ATEXT_CHARS.get(ch)) + return false; + } + + prev = ch; + } + + return true; + } + + // RFC 5322 section 3.2.4 + private static String quote(String str) { + // quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS] + // qcontent = qtext / quoted-pair + // qtext = %d33 / %d35-91 / %d93-126 + // quoted-pair = ("\" (VCHAR / WSP)) + // VCHAR = %x21-7E + // DQUOTE = %x22 + + String escaped = str.replaceAll("[\\\\\"]", "\\\\$0"); + return "\"" + escaped + "\""; + } + + private static String encodeB(String prefix, String text, + int usedCharacters, Charset charset, byte[] bytes) { + int encodedLength = bEncodedLength(bytes); + + int totalLength = prefix.length() + encodedLength + + ENC_WORD_SUFFIX.length(); + if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) { + return prefix + encodeB(bytes) + ENC_WORD_SUFFIX; + } else { + String part1 = text.substring(0, text.length() / 2); + byte[] bytes1 = encode(part1, charset); + String word1 = encodeB(prefix, part1, usedCharacters, charset, + bytes1); + + String part2 = text.substring(text.length() / 2); + byte[] bytes2 = encode(part2, charset); + String word2 = encodeB(prefix, part2, 0, charset, bytes2); + + return word1 + " " + word2; + } + } + + private static int bEncodedLength(byte[] bytes) { + return (bytes.length + 2) / 3 * 4; + } + + private static String encodeQ(String prefix, String text, Usage usage, + int usedCharacters, Charset charset, byte[] bytes) { + int encodedLength = qEncodedLength(bytes, usage); + + int totalLength = prefix.length() + encodedLength + + ENC_WORD_SUFFIX.length(); + if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) { + return prefix + encodeQ(bytes, usage) + ENC_WORD_SUFFIX; + } else { + String part1 = text.substring(0, text.length() / 2); + byte[] bytes1 = encode(part1, charset); + String word1 = encodeQ(prefix, part1, usage, usedCharacters, + charset, bytes1); + + String part2 = text.substring(text.length() / 2); + byte[] bytes2 = encode(part2, charset); + String word2 = encodeQ(prefix, part2, usage, 0, charset, bytes2); + + return word1 + " " + word2; + } + } + + private static int qEncodedLength(byte[] bytes, Usage usage) { + BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS + : Q_RESTRICTED_CHARS; + + int count = 0; + + for (int idx = 0; idx < bytes.length; idx++) { + int v = bytes[idx] & 0xff; + if (v == 32) { + count++; + } else if (!qChars.get(v)) { + count += 3; + } else { + count++; + } + } + + return count; + } + + private static byte[] encode(String text, Charset charset) { + ByteBuffer buffer = charset.encode(text); + byte[] bytes = new byte[buffer.limit()]; + buffer.get(bytes); + return bytes; + } + + private static Charset determineCharset(String text) { + // it is an important property of iso-8859-1 that it directly maps + // unicode code points 0000 to 00ff to byte values 00 to ff. + boolean ascii = true; + final int len = text.length(); + for (int index = 0; index < len; index++) { + char ch = text.charAt(index); + if (ch > 0xff) { + return CharsetUtil.UTF_8; + } + if (ch > 0x7f) { + ascii = false; + } + } + return ascii ? CharsetUtil.US_ASCII : CharsetUtil.ISO_8859_1; + } + + private static Encoding determineEncoding(byte[] bytes, Usage usage) { + if (bytes.length == 0) + return Encoding.Q; + + BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS + : Q_RESTRICTED_CHARS; + + int qEncoded = 0; + for (int i = 0; i < bytes.length; i++) { + int v = bytes[i] & 0xff; + if (v != 32 && !qChars.get(v)) { + qEncoded++; + } + } + + int percentage = qEncoded * 100 / bytes.length; + return percentage > 30 ? Encoding.B : Encoding.Q; + } + + private static char hexDigit(int i) { + return i < 10 ? (char) (i + '0') : (char) (i - 10 + 'A'); + } +} diff --git a/src/org/apache/james/mime4j/decoder/DecoderUtil.java b/src/org/apache/james/mime4j/decoder/DecoderUtil.java index 9bd2c512d..f7b9fa8f8 100644 --- a/src/org/apache/james/mime4j/decoder/DecoderUtil.java +++ b/src/org/apache/james/mime4j/decoder/DecoderUtil.java @@ -19,15 +19,15 @@ package org.apache.james.mime4j.decoder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.util.CharsetUtil; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.james.mime4j.util.CharsetUtil; - /** * Static methods for decoding strings, byte arrays and encoded words. * @@ -146,131 +146,119 @@ public class DecoderUtil { * =?charset?enc?Encoded word?= where enc is either 'Q' or 'q' for * quoted-printable and 'B' or 'b' for Base64. * + * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J + * * @param body the string to decode. * @return the decoded string. */ public static String decodeEncodedWords(String body) { - StringBuffer sb = new StringBuffer(); - int p1 = 0; - int p2 = 0; - - try { - - /* - * Encoded words in headers have the form - * =?charset?enc?Encoded word?= where enc is either 'Q' or 'q' for - * quoted printable and 'B' and 'b' for Base64 - */ - - while (p2 < body.length()) { - /* - * Find beginning of first encoded word - */ - p1 = body.indexOf("=?", p2); - if (p1 == -1) { - /* - * None found. Emit the rest of the header and exit. - */ - sb.append(body.substring(p2)); - break; - } - - /* - * p2 points to the previously found end marker or the start - * of the entire header text. Append the text between that - * marker and the one pointed to by p1. - */ - if (p1 - p2 > 0) { - sb.append(body.substring(p2, p1)); - } - - /* - * Find the first and second '?':s after the marker pointed to - * by p1. - */ - int t1 = body.indexOf('?', p1 + 2); - int t2 = t1 != -1 ? body.indexOf('?', t1 + 1) : -1; - - /* - * Find this words end marker. - */ - p2 = t2 != -1 ? body.indexOf("?=", t2 + 1) : -1; - if (p2 == -1) { - if (t2 != -1 && (body.length() - 1 == t2 || body.charAt(t2 + 1) == '=')) { - /* - * Treat "=?charset?enc?" and "=?charset?enc?=" as - * empty strings. - */ - p2 = t2; - } else { - /* - * No end marker was found. Append the rest of the - * header and exit. - */ - sb.append(body.substring(p1)); - break; - } - } - - /* - * [p1+2, t1] -> charset - * [t1+1, t2] -> encoding - * [t2+1, p2] -> encoded word - */ - - String decodedWord = null; - if (t2 == p2) { - /* - * The text is empty - */ - decodedWord = ""; - } else { - - String mimeCharset = body.substring(p1 + 2, t1); - String enc = body.substring(t1 + 1, t2); - String encodedWord = body.substring(t2 + 1, p2); - - /* - * Convert the MIME charset to a corresponding Java one. - */ - String charset = CharsetUtil.toJavaCharset(mimeCharset); - if (charset == null) { - decodedWord = body.substring(p1, p2 + 2); - if (log.isWarnEnabled()) { - log.warn("MIME charset '" + mimeCharset - + "' in header field doesn't have a " - +"corresponding Java charset"); - } - } else if (!CharsetUtil.isDecodingSupported(charset)) { - decodedWord = body.substring(p1, p2 + 2); - if (log.isWarnEnabled()) { - log.warn("Current JDK doesn't support decoding " - + "of charset '" + charset - + "' (MIME charset '" - + mimeCharset + "')"); - } - } else { - if (enc.equalsIgnoreCase("Q")) { - decodedWord = DecoderUtil.decodeQ(encodedWord, charset); - } else if (enc.equalsIgnoreCase("B")) { - decodedWord = DecoderUtil.decodeB(encodedWord, charset); - } else { - decodedWord = encodedWord; - if (log.isWarnEnabled()) { - log.warn("Warning: Unknown encoding in " - + "header field '" + enc + "'"); - } - } - } - } - p2 += 2; - sb.append(decodedWord); - } - } catch (Throwable t) { - log.error("Decoding header field body '" + body + "'", t); + // ANDROID: Most strings will not include "=?" so a quick test can prevent unneeded + // object creation. This could also be handled via lazy creation of the StringBuilder. + if (body.indexOf("=?") == -1) { + return body; } - return sb.toString(); + int previousEnd = 0; + boolean previousWasEncoded = false; + + StringBuilder sb = new StringBuilder(); + + while (true) { + int begin = body.indexOf("=?", previousEnd); + int end = begin == -1 ? -1 : body.indexOf("?=", begin + 2); + if (end == -1) { + if (previousEnd == 0) + return body; + + sb.append(body.substring(previousEnd)); + return sb.toString(); + } + end += 2; + + String sep = body.substring(previousEnd, begin); + + String decoded = decodeEncodedWord(body, begin, end); + if (decoded == null) { + sb.append(sep); + sb.append(body.substring(begin, end)); + } else { + if (!previousWasEncoded || !CharsetUtil.isWhitespace(sep)) { + sb.append(sep); + } + sb.append(decoded); + } + + previousEnd = end; + previousWasEncoded = decoded != null; + } + } + + // return null on error + private static String decodeEncodedWord(String body, int begin, int end) { + int qm1 = body.indexOf('?', begin + 2); + if (qm1 == end - 2) + return null; + + int qm2 = body.indexOf('?', qm1 + 1); + if (qm2 == end - 2) + return null; + + String mimeCharset = body.substring(begin + 2, qm1); + String encoding = body.substring(qm1 + 1, qm2); + String encodedText = body.substring(qm2 + 1, end - 2); + + String charset = CharsetUtil.toJavaCharset(mimeCharset); + if (charset == null) { + if (log.isWarnEnabled()) { + log.warn("MIME charset '" + mimeCharset + "' in encoded word '" + + body.substring(begin, end) + "' doesn't have a " + + "corresponding Java charset"); + } + return null; + } else if (!CharsetUtil.isDecodingSupported(charset)) { + if (log.isWarnEnabled()) { + log.warn("Current JDK doesn't support decoding of charset '" + + charset + "' (MIME charset '" + mimeCharset + + "' in encoded word '" + body.substring(begin, end) + + "')"); + } + return null; + } + + if (encodedText.length() == 0) { + if (log.isWarnEnabled()) { + log.warn("Missing encoded text in encoded word: '" + + body.substring(begin, end) + "'"); + } + return null; + } + + try { + if (encoding.equalsIgnoreCase("Q")) { + return DecoderUtil.decodeQ(encodedText, charset); + } else if (encoding.equalsIgnoreCase("B")) { + return DecoderUtil.decodeB(encodedText, charset); + } else { + if (log.isWarnEnabled()) { + log.warn("Warning: Unknown encoding in encoded word '" + + body.substring(begin, end) + "'"); + } + return null; + } + } catch (UnsupportedEncodingException e) { + // should not happen because of isDecodingSupported check above + if (log.isWarnEnabled()) { + log.warn("Unsupported encoding in encoded word '" + + body.substring(begin, end) + "'", e); + } + return null; + } catch (RuntimeException e) { + if (log.isWarnEnabled()) { + log.warn("Could not decode encoded word '" + + body.substring(begin, end) + "'", e); + } + return null; + } } } diff --git a/src/org/apache/james/mime4j/util/CharsetUtil.java b/src/org/apache/james/mime4j/util/CharsetUtil.java index f5026b1f8..8bb044039 100644 --- a/src/org/apache/james/mime4j/util/CharsetUtil.java +++ b/src/org/apache/james/mime4j/util/CharsetUtil.java @@ -1056,6 +1056,73 @@ public class CharsetUtil { } } + /** + * ANDROID: THE FOLLOWING SET OF STATIC STRINGS ARE COPIED FROM A NEWER VERSION OF MIME4J + */ + + /** carriage return - line feed sequence */ + public static final String CRLF = "\r\n"; + + /** US-ASCII CR, carriage return (13) */ + public static final int CR = '\r'; + + /** US-ASCII LF, line feed (10) */ + public static final int LF = '\n'; + + /** US-ASCII SP, space (32) */ + public static final int SP = ' '; + + /** US-ASCII HT, horizontal-tab (9)*/ + public static final int HT = '\t'; + + public static final java.nio.charset.Charset US_ASCII = java.nio.charset.Charset + .forName("US-ASCII"); + + public static final java.nio.charset.Charset ISO_8859_1 = java.nio.charset.Charset + .forName("ISO-8859-1"); + + public static final java.nio.charset.Charset UTF_8 = java.nio.charset.Charset + .forName("UTF-8"); + + /** + * Returns true if the specified character is a whitespace + * character (CR, LF, SP or HT). + * + * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J + * + * @param ch + * character to test. + * @return true if the specified character is a whitespace + * character, false otherwise. + */ + public static boolean isWhitespace(char ch) { + return ch == SP || ch == HT || ch == CR || ch == LF; + } + + /** + * Returns true if the specified string consists entirely of + * whitespace characters. + * + * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J + * + * @param s + * string to test. + * @return true if the specified string consists entirely of + * whitespace characters, false otherwise. + */ + public static boolean isWhitespace(final String s) { + if (s == null) { + throw new IllegalArgumentException("String may not be null"); + } + final int len = s.length(); + for (int i = 0; i < len; i++) { + if (!isWhitespace(s.charAt(i))) { + return false; + } + } + return true; + } + /** * Determines if the VM supports encoding (chars to bytes) the * specified character set. NOTE: the given character set name may diff --git a/tests/src/com/android/email/mail/internet/MimeMessageTest.java b/tests/src/com/android/email/mail/internet/MimeMessageTest.java index d1fde4a6e..45f223e4c 100644 --- a/tests/src/com/android/email/mail/internet/MimeMessageTest.java +++ b/tests/src/com/android/email/mail/internet/MimeMessageTest.java @@ -35,6 +35,26 @@ import junit.framework.TestCase; */ @SmallTest public class MimeMessageTest extends TestCase { + + /** up arrow, down arrow, left arrow, right arrow */ + private final String SHORT_UNICODE = "\u2191\u2193\u2190\u2192"; + private final String SHORT_UNICODE_ENCODED = "=?UTF-8?B?4oaR4oaT4oaQ4oaS?="; + + /** a string without any unicode */ + private final String SHORT_PLAIN = "abcd"; + + /** longer unicode strings */ + private final String LONG_UNICODE_16 = SHORT_UNICODE + SHORT_UNICODE + + SHORT_UNICODE + SHORT_UNICODE; + private final String LONG_UNICODE_64 = LONG_UNICODE_16 + LONG_UNICODE_16 + + LONG_UNICODE_16 + LONG_UNICODE_16; + + /** longer plain strings (with fold points) */ + private final String LONG_PLAIN_16 = "abcdefgh ijklmno"; + private final String LONG_PLAIN_64 = + LONG_PLAIN_16 + LONG_PLAIN_16 + LONG_PLAIN_16 + LONG_PLAIN_16; + private final String LONG_PLAIN_256 = + LONG_PLAIN_64 + LONG_PLAIN_64 + LONG_PLAIN_64 + LONG_PLAIN_64; // TODO: more tests. @@ -98,7 +118,7 @@ public class MimeMessageTest extends TestCase { assertEquals("set and get Message-ID", testId2, message2.getMessageId()); } - /* + /** * Confirm getContentID() correctly works. */ public void testGetContentId() throws MessagingException { @@ -116,4 +136,80 @@ public class MimeMessageTest extends TestCase { message.setHeader(MimeHeader.HEADER_CONTENT_ID, "<" + cid1 + ">"); assertEquals(cid1, message.getContentId()); } + + /** + * Confirm that setSubject() works with plain strings + */ + public void testSetSubjectPlain() throws MessagingException { + MimeMessage message = new MimeMessage(); + + message.setSubject(SHORT_PLAIN); + + // test 1: readback + assertEquals("plain subjects", SHORT_PLAIN, message.getSubject()); + + // test 2: raw readback is not escaped + String rawHeader = message.getFirstHeader("Subject"); + assertEquals("plain subject not encoded", -1, rawHeader.indexOf("=?")); + + // test 3: long subject (shouldn't fold) + message.setSubject(LONG_PLAIN_64); + rawHeader = message.getFirstHeader("Subject"); + String[] split = rawHeader.split("\r\n"); + assertEquals("64 shouldn't fold", 1, split.length); + + // test 4: very long subject (should fold) + message.setSubject(LONG_PLAIN_256); + rawHeader = message.getFirstHeader("Subject"); + split = rawHeader.split("\r\n"); + assertTrue("long subject should fold", split.length > 1); + for (String s : split) { + assertTrue("split lines max length 78", s.length() <= 76); // 76+\r\n = 78 + String trimmed = s.trim(); + assertFalse("split lines are not encoded", trimmed.startsWith("=?")); + } + } + + /** + * Confirm that setSubject() works with unicode strings + */ + public void testSetSubject() throws MessagingException { + MimeMessage message = new MimeMessage(); + + message.setSubject(SHORT_UNICODE); + + // test 1: readback in unicode + assertEquals("unicode readback", SHORT_UNICODE, message.getSubject()); + + // test 2: raw readback is escaped + String rawHeader = message.getFirstHeader("Subject"); + assertEquals("raw readback", SHORT_UNICODE_ENCODED, rawHeader); + } + + /** + * Confirm folding operations on unicode subjects + */ + public void testSetLongSubject() throws MessagingException { + MimeMessage message = new MimeMessage(); + + // test 1: long unicode - readback in unicode + message.setSubject(LONG_UNICODE_16); + assertEquals("unicode readback 16", LONG_UNICODE_16, message.getSubject()); + + // test 2: longer unicode (will fold) + message.setSubject(LONG_UNICODE_64); + assertEquals("unicode readback 64", LONG_UNICODE_64, message.getSubject()); + + // test 3: check folding & encoding + String rawHeader = message.getFirstHeader("Subject"); + String[] split = rawHeader.split("\r\n"); + assertTrue("long subject should fold", split.length > 1); + for (String s : split) { + assertTrue("split lines max length 78", s.length() <= 76); // 76+\r\n = 78 + String trimmed = s.trim(); + assertTrue("split lines are encoded", + trimmed.startsWith("=?") && trimmed.endsWith("?=")); + } + } + } \ No newline at end of file diff --git a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java index e894161be..b39e4cf0d 100644 --- a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java +++ b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java @@ -35,11 +35,120 @@ import junit.framework.TestCase; @SmallTest public class MimeUtilityTest extends TestCase { - // TODO: tests for unfold(String s) + /** up arrow, down arrow, left arrow, right arrow */ + private final String SHORT_UNICODE = "\u2191\u2193\u2190\u2192"; + private final String SHORT_UNICODE_ENCODED = "=?UTF-8?B?4oaR4oaT4oaQ4oaS?="; + + /** a string without any unicode */ + private final String SHORT_PLAIN = "abcd"; + + /** a typical no-param header */ + private final String HEADER_NO_PARAMETER = + "header"; + /** a typical multi-param header */ + private final String HEADER_MULTI_PARAMETER = + "header; Param1Name=Param1Value; Param2Name=Param2Value"; + + /** + * Test that decode/unfold is efficient when it can be + */ + public void testEfficientUnfoldAndDecode() { + String result1 = MimeUtility.unfold(SHORT_PLAIN); + String result2 = MimeUtility.decode(SHORT_PLAIN); + String result3 = MimeUtility.unfoldAndDecode(SHORT_PLAIN); + + assertSame(SHORT_PLAIN, result1); + assertSame(SHORT_PLAIN, result2); + assertSame(SHORT_PLAIN, result3); + } + + // TODO: more tests for unfold(String s) + + /** + * Test that decode is working for simple strings + */ + public void testDecodeSimple() { + String result1 = MimeUtility.decode(SHORT_UNICODE_ENCODED); + assertEquals(SHORT_UNICODE, result1); + } + // TODO: tests for decode(String s) + + /** + * Test that unfoldAndDecode is working for simple strings + */ + public void testUnfoldAndDecodeSimple() { + String result1 = MimeUtility.unfoldAndDecode(SHORT_UNICODE_ENCODED); + assertEquals(SHORT_UNICODE, result1); + } + // TODO: tests for unfoldAndDecode(String s) - // TODO: tests for foldAndEncode(String s) - // TODO: tests for getHeaderParameter(String header, String name) + + /** + * Test that fold/encode is efficient when it can be + */ + public void testEfficientFoldAndEncode() { + String result1 = MimeUtility.foldAndEncode(SHORT_PLAIN); + String result2 = MimeUtility.foldAndEncode2(SHORT_PLAIN, 10); + String result3 = MimeUtility.fold(SHORT_PLAIN, 10); + + assertSame(SHORT_PLAIN, result1); + assertSame(SHORT_PLAIN, result2); + assertSame(SHORT_PLAIN, result3); + } + + // TODO: more tests for foldAndEncode(String s) + + /** + * Test that foldAndEncode2 is working for simple strings + */ + public void testFoldAndEncode2() { + String result1 = MimeUtility.foldAndEncode2(SHORT_UNICODE, 10); + assertEquals(SHORT_UNICODE_ENCODED, result1); + } + + // TODO: more tests for foldAndEncode2(String s) + // TODO: more tests for fold(String s, int usedCharacters) + + /** + * Basic tests of getHeaderParameter() + * + * Typical header value: multipart/mixed; boundary="----E5UGTXUQQJV80DR8SJ88F79BRA4S8K" + * + * Function spec says: + * if header is null: return null + * if name is null: if params, return first param. else return full field + * else: if param is found (case insensitive) return it + * else return null + */ + public void testGetHeaderParameter() { + // if header is null, return null + assertNull("null header check", MimeUtility.getHeaderParameter(null, "name")); + + // if name is null, return first param or full header + // NOTE: The docs are wrong - it returns the header (no params) in that case +// assertEquals("null name first param per docs", "Param1Value", +// MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, null)); + assertEquals("null name first param per code", "header", + MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, null)); + assertEquals("null name full header", HEADER_NO_PARAMETER, + MimeUtility.getHeaderParameter(HEADER_NO_PARAMETER, null)); + + // find name + assertEquals("get 1st param", "Param1Value", + MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "Param1Name")); + assertEquals("get 2nd param", "Param2Value", + MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "Param2Name")); + assertEquals("get missing param", null, + MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "Param3Name")); + + // case insensitivity + assertEquals("get 2nd param all LC", "Param2Value", + MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "param2name")); + assertEquals("get 2nd param all UC", "Param2Value", + MimeUtility.getHeaderParameter(HEADER_MULTI_PARAMETER, "PARAM2NAME")); + } + // TODO: tests for findFirstPartByMimeType(Part part, String mimeType) /** Tests for findPartByContentId(Part part, String contentId) */ From 4fdc52ee3d835be900e53910bd48e042692acb84 Mon Sep 17 00:00:00 2001 From: Eric Fischer <> Date: Tue, 24 Mar 2009 18:33:42 -0700 Subject: [PATCH 03/11] Automated import from //branches/cupcake/...@141866,141866 --- res/values-cs/strings.xml | 23 ++++++++++++----------- res/values-de/strings.xml | 23 ++++++++++++----------- res/values-es/strings.xml | 23 ++++++++++++----------- res/values-fr/strings.xml | 23 ++++++++++++----------- res/values-it/strings.xml | 23 ++++++++++++----------- res/values-ja/strings.xml | 31 ++++++++++++++++--------------- res/values-ko/strings.xml | 23 ++++++++++++----------- res/values-nb/strings.xml | 15 ++++++++------- res/values-nl/strings.xml | 23 ++++++++++++----------- res/values-pl/strings.xml | 23 ++++++++++++----------- res/values-ru/strings.xml | 23 ++++++++++++----------- res/values-zh-rCN/strings.xml | 23 ++++++++++++----------- res/values-zh-rTW/strings.xml | 23 ++++++++++++----------- 13 files changed, 156 insertions(+), 143 deletions(-) diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml index f38314c38..2bed10a90 100644 --- a/res/values-cs/strings.xml +++ b/res/values-cs/strings.xml @@ -50,11 +50,15 @@ "Chyba připojení" "Opakovat načítání dalších zpráv" "Nový e-mail" - - - - - + + "%d nepřečtená zpráva (%s)" + "Nepřečtené zprávy: %d (%s)" + "Nepřečtené zprávy: %d (%s)" + + + "v(e) %d účtech" + "v(e) %d účtech" + "Doručená pošta" "Vítá vás nastavení aplikace Email!"\n\n"V aplikaci Email můžete používat jakékoli e-mailové účty."\n\n"Nejznámější e-mailové účty lze nastavit ve dvou krocích!" "Verze: %s" @@ -65,8 +69,7 @@ "Kopie" "Skrytá kopie" "Předmět" - - + "Napsat e-mail" \n\n"-------- Původní zpráva --------"\n"Předmět: %s"\n"Odesílatel: %s"\n"Komu: %s"\n"Kopie: %s"\n\n \n\n"%snapsal/a:"\n\n "Text v uvozovkách" @@ -84,10 +87,8 @@ "Zpráva byla smazána." "Zpráva byla zrušena." "Zpráva byla uložena jako koncept." - - - - + "Přidat kontakt" + "Přidat „%s“ do kontaktů" "Nastavit e-mail" "Zadejte e-mailovou adresu účtu:" "E-mailová adresa" diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 28bb5d218..3bfa8c709 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -50,11 +50,15 @@ "Verbindungsfehler" "Laden weiterer Nachrichten erneut versuchen" "Neue E-Mail" - - - - - + + "%d ungelesen (%s)" + "%d ungelesen (%s)" + "%d ungelesen (%s)" + + + "in %d E-Mail-Konten" + "in %d E-Mail-Konten" + "Posteingang" "Willkommen bei der Einrichtung von E-Mails."\n\n"Nutzen Sie ein beliebiges E-Mail-Konto."\n\n"Gängige E-Mail-Konten können in nur zwei Schritten eingerichtet werden." "Version: %s" @@ -65,8 +69,7 @@ "Cc" "Bcc" "Betreff" - - + "E-Mail schreiben" \n\n"-------- Originalnachricht --------"\n"Betreff: %s"\n"Von: %s"\n"An: %s"\n"Cc: %s"\n\n \n\n"%s schrieb:"\n\n "Zitierter Text" @@ -84,10 +87,8 @@ "Nachricht gelöscht" "Nachricht gelöscht" "Nachricht als Entwurf gespeichert" - - - - + "Kontakt hinzufügen" + "\"%s\" zu den Kontakten hinzufügen" "E-Mail einrichten" "Geben Sie Ihre im Konto gespeicherte E-Mail-Adresse ein:" "E-Mail-Adresse" diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 71a7fd0d7..e253906ba 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -50,11 +50,15 @@ "Error de conexión" "Intentar cargar más mensajes" "Nuevo mensaje" - - - - - + + "%d no leídos (%s)" + "%d no leídos (%s)" + "%d no leídos (%s)" + + + "en %d cuentas" + "en %d cuentas" + "Recibidos" "Te damos la bienvenida a la configuración del correo electrónico."\n\n"Usa cualquier cuenta de correo electrónico con \"Correo electrónico\"."\n\n"Las cuentas de correo electrónico más populares se pueden configurar en 2 pasos." "Versión: %s" @@ -65,8 +69,7 @@ "Cc" "CCO" "Asunto" - - + "Redactar" \n\n"-------- Original Message --------"\n"Subject: %s"\n"From: %s"\n"To: %s"\n"CC: %s"\n\n \n\n"%s wrote:"\n\n "Texto entre comillas" @@ -84,10 +87,8 @@ "Mensaje suprimido" "Mensaje descartado" "Mensaje guardado como borrador" - - - - + "Añadir contacto" + "Añadir \"%s\" a los contactos" "Configurar correo electrónico" "Introduce la dirección de correo electrónico de tu cuenta:" "Dirección de correo electrónico" diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index f72f185c0..9a0f522ea 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -50,11 +50,15 @@ "Erreur de connexion" "Essayer de charger plus de messages" "Nouvel e-mail" - - - - - + + "%d non lu (%s)" + "%d non lus (%s)" + "%d non lus (%s)" + + + "dans %d comptes" + "dans %d comptes" + "Boîte de réception" "Bienvenue dans le programme de configuration de votre messagerie électronique !"\n\n"Utilisez le compte de votre choix."\n\n"Les comptes de messagerie les plus courants peuvent être configurés en deux étapes !" "Version : %s" @@ -65,8 +69,7 @@ "Cc" "Cci" "Objet" - - + "Nouveau message" \n\n"-------- Message original --------"\n"Objet : %s"\n"De : %s"\n"À : %s"\n"Cc : %s"\n\n \n\n"%s a écrit :"\n\n "Texte du message précédent" @@ -84,10 +87,8 @@ "Message supprimé." "Message supprimé." "Message enregistré comme brouillon." - - - - + "Ajouter un contact" + "Ajouter \"%s\" aux contacts" "Configurer la messagerie électronique" "Saisissez l\'adresse e-mail de votre compte :" "Adresse e-mail" diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 112fad49d..b95d1c6bb 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -50,11 +50,15 @@ "Errore di connessione" "Prova nuovamente a caricare altri messaggi" "Nuova email" - - - - - + + "%d da leggere (%s)" + "%d da leggere (%s)" + "%d da leggere (%s)" + + + "in %d account" + "in %d account" + "Posta in arrivo" "Benvenuto in Impostazione email."\n\n"Usa qualunque account email con Email."\n\n"Puoi impostare gli account email più popolari in due semplici passaggi." "Versione: %s" @@ -65,8 +69,7 @@ "Cc" "Ccn" "Oggetto" - - + "Scrivi email" \n\n"-------- Messaggio originale --------"\n"Oggetto: %s"\n"Da: %s"\n"A: %s"\n"CC: %s"\n\n \n\n"%s ha scritto:"\n\n "Testo tra virgolette" @@ -84,10 +87,8 @@ "Messaggio eliminato." "Messaggio eliminato." "Messaggio salvato come bozza." - - - - + "Aggiungi contatto" + "Aggiungi \"%s\" ai contatti" "Imposta email" "Digita l\'indirizzo email del tuo account:" "Indirizzo email" diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index 24d598073..b1944e756 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -31,7 +31,7 @@ "転送" "完了" "破棄" - "下書きとして保存" + "下書き保存" "更新" "アカウントを追加" "作成" @@ -50,11 +50,15 @@ "接続エラー" "追加のメールを読み込み直す" "新着メール" - - - - - + + "未読%d件(%s)" + "未読%d件(%s)" + "未読%d件(%s)" + + + "(%d個のアカウント)" + "(%d個のアカウント)" + "受信トレイ" "メールアカウントのセットアップ"\n\n"お好きなメールアカウントを登録してください。"\n\n"画面の手順に沿って簡単に追加できます。" "バージョン: %s" @@ -65,8 +69,7 @@ "Cc" "Bcc" "件名" - - + "メールを作成" \n\n"-------- 元のメッセージ --------"\n"件名: %s"\n"From: %s"\n"To: %s"\n"Cc: %s"\n\n \n\n"%s: "\n\n "元のメッセージ" @@ -83,11 +86,9 @@ "添付ファイルを取得中" "メッセージを削除しました。" "メッセージを破棄しました。" - "メッセージを下書きとして保存しました。" - - - - + "メッセージを下書き保存しました。" + "連絡先を追加" + "%s を連絡先に追加する" "メールアカウントの登録" "メールのアカウント情報を入力:" "メールアドレス" @@ -157,8 +158,8 @@ "全般設定" "既定のアカウント" "いつもこのアカウントでメールを送信する" - "メール通知" - "メールの着信をステータスバーで知らせる" + "メール着信通知" + "メール受信: ステータスバーで通知" "メールチェックの頻度" "受信設定" "送信設定" diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index 85511805b..b85d26aa4 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -50,11 +50,15 @@ "연결 오류" "추가 메일 로드 재시도" "새 이메일" - - - - - + + "읽지 않은 메시지 %d개(%s)" + "읽지 않은 메시지 %d개(%s)" + "읽지 않은 메시지 %d개(%s)" + + + "(%d개 계정)" + "(%d개 계정)" + "받은편지함" "이메일 설정에 오신 것을 환영합니다."\n\n"원하는 이메일 계정을 사용하세요."\n\n"가장 많이 사용하는 이메일 계정은 2단계 만에 설정할 수 있습니다." "버전: %s" @@ -65,8 +69,7 @@ "참조" "숨은참조" "제목" - - + "편지쓰기" \n\n"-------- 원본 메일 --------"\n"제목: %s"\n"보낸사람: %s"\n"받는사람: %s"\n"참조: %s"\n\n \n\n"%s님이 작성:"\n\n "받은메일" @@ -84,10 +87,8 @@ "메일이 삭제되었습니다." "메일이 삭제되었습니다." "메일을 임시로 저장했습니다." - - - - + "주소 추가" + "주소록에 \'%s\' 추가" "이메일 설정" "계정 이메일 주소 입력:" "이메일 주소" diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index 7e6b025c2..78dbc31f9 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -50,23 +50,24 @@ "Feil ved tilkobling" "Prøv på nytt å hente flere meldinger" "Ny e-post" - - - + + "%d ulest (%s)" + "%d uleste (%s)" + "%d uleste (%s)" + "Innboks" "Velkommen til e-post-oppsettet!"\n\n"Du kan bruke hvilken som helst e-postkonto."\n\n"De fleste vanlige e-postkontoer kan settes opp i to trinn!" "Version: %s" - "Enable extra debug logging?" - "Enable sensitive information debug logging? (May show passwords in logs.)" + "Slå på ekstra debug-logging?" + "Slå på debug-logging av sensitiv informasjon? (Kan vise passord i logger.)" "Hent flere meldinger" "Til" "Kopi" "Blindkopi" "Emne" - - + "Skriv e-post" \n\n"-------- Original Message --------"\n"Subject: %s"\n"From: %s"\n"To: %s"\n"CC: %s"\n\n \n\n"%s:"\n\n "Sitert tekst" diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index e71de77a3..b209fd167 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -50,11 +50,15 @@ "Verbindingsfout" "Probeer opnieuw meer berichten te laden" "Nieuwe e-mail" - - - - - + + "%d ongelezen (%s)" + "%d ongelezen (%s)" + "%d ongelezen (%s)" + + + "in %d accounts" + "in %d accounts" + "Postvak IN" "Welkom bij E-mail instellen."\n\n"Je kunt elke account gebruiken met E-mail."\n\n"De meest populaire e-mailaccounts kunnen in twee stappen worden ingesteld." "Versie: %s" @@ -65,8 +69,7 @@ "Cc" "Bcc" "Onderwerp" - - + "Nieuw bericht" \n\n"-------- Oorspronkelijk bericht --------"\n"Onderwerp: %s"\n"Van: %s"\n"Aan: %s"\n"Cc: %s"\n\n \n\n"%sschreef:"\n\n "Geciteerde tekst" @@ -84,10 +87,8 @@ "Bericht verwijderd." "Bericht wordt verwijderd" "Bericht opgeslagen als concept." - - - - + "Contactpersoon toevoegen" + "Voeg \"%s\" toe aan contactpersonen" "E-mail instellen" "Typ het e-mailadres van je account:" "E-mailadres" diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index ab4f036cc..3c1ca1da7 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -50,11 +50,15 @@ "Błąd połączenia" "Ponów próbę załadowania większej liczby wiadomości" "Nowa wiadomość e-mail" - - - - - + + "Nieprzeczytane: %d (%s)" + "Nieprzeczytane: %d (%s)" + "Nieprzeczytane: %d (%s)" + + + "na %d kontach" + "na %d kontach" + "Odebrane" "Zapraszamy do skonfigurowania poczty e-mail!"\n\n"W tej usłudze można korzystać z dowolnego konta e-mail."\n\n"Konfiguracja najpopularniejszych kont e-mail składa się z tylko 2 etapów." "Wersja: %s" @@ -65,8 +69,7 @@ "DW" "UDW" "Temat" - - + "Utwórz wiadomość" \n\n"-------- Wiadomość oryginalna --------"\n"Temat: %s"\n"Od: %s"\n"Do: %s"\n"DW: %s"\n\n \n\n"Użytkownik %s napisał:"\n\n "Cytowany tekst" @@ -84,10 +87,8 @@ "Wiadomość została usunięta." "Wiadomość została odrzucona." "Wiadomość została zapisana jako wersja robocza." - - - - + "Dodaj kontakt" + "Dodaj adres „%s” do kontaktów" "Skonfiguruj konto e-mail" "Podaj adres e-mail swojego konta:" "Adres e-mail" diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 805e85d07..58ee0cfa1 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -50,11 +50,15 @@ "Ошибка подключения" "Повторить попытку загрузки остальных писем" "Новое письмо" - - - - - + + "Непрочитанных: %d (%s)" + "Непрочитанных: %d (%s)" + "Непрочитанных: %d (%s)" + + + "в нескольких (%d) аккаунтах" + "в нескольких (%d) аккаунтах" + "Входящие" "Добро пожаловать на страницу настройки электронной почты."\n\n"С нашей почтовой системой можно использовать любой аккаунт электронной почты."\n\n"Самые распространенные аккаунты электронной почты можно настроить за 2 шага." "Версия: %s" @@ -65,8 +69,7 @@ "Копия" "Скрытая" "Тема" - - + "Написать письмо" \n\n"-------- Исходное сообщение --------"\n"Тема: %s"\n"От: %s"\n"Кому: %s"\n"Копия: %s"\n\n \n\n"%s написал(а):"\n\n "Цитируемый текст" @@ -84,10 +87,8 @@ "Письмо удалено." "Письмо не сохранено." "Письмо сохранено как черновик." - - - - + "Добавить контакт" + "Добавить адрес %s в контакты" "Настройка электронной почты" "Укажите почтовый адрес своего аккаунта:" "Адрес электронной почты" diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 2e0614814..2964b889c 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -50,11 +50,15 @@ "连接错误" "重新尝试载入更多邮件" "新电子邮件" - - - - - + + "有 %d 封未读邮件 (%s)" + "有 %d 封未读邮件 (%s)" + "有 %d 封未读邮件 (%s)" + + + "在 %d 个帐户中" + "在 %d 个帐户中" + "收件箱" "欢迎设置电子邮件!"\n\n"在此可使用任意电子邮件帐户。"\n\n"大多数常见的电子邮件帐户通过 2 步即可完成设置!" "版本:%s" @@ -65,8 +69,7 @@ "抄送" "密送" "主题" - - + "撰写邮件" \n\n"-------- 原始邮件 --------"\n"主题:%s"\n"发件人:%s"\n"收件人:%s"\n"抄送:%s"\n\n \n\n"%s写道:"\n\n "引用文字" @@ -84,10 +87,8 @@ "邮件已删除。" "邮件已取消。" "邮件已另存为草稿。" - - - - + "添加联系人" + "将“%s”添加到通讯录中" "设置电子邮件" "键入您的帐户电子邮件地址:" "电子邮件地址" diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index d2345e1d9..093ba286f 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -50,11 +50,15 @@ "連線錯誤" "重新載入更多郵件" "新電子郵件" - - - - - + + "%d 封未讀取郵件 (%s)" + "%d 封未讀取郵件 (%s)" + "%d 封未讀取郵件 (%s)" + + + "%d 個帳戶" + "%d 個帳戶" + "收件匣" "歡迎使用 [電子郵件] 設定!"\n\n"[電子郵件] 設定可讓您使用各種電子郵件帳戶!"\n\n"大部分常見的電子郵件只需要兩個步驟就能設定完成!" "版本:%s" @@ -65,8 +69,7 @@ "副本" "密件副本" "主旨" - - + "撰寫郵件" \n\n"--------原始郵件 --------"\n"主旨:%s"\n"寄件者:%s"\n"收件者:%s"\n"副本:%s"\n\n \n\n"%s提到:"\n\n "引用的文字" @@ -84,10 +87,8 @@ "已刪除郵件。" "已捨棄郵件。" "已儲存郵件草稿。" - - - - + "新增聯絡人" + "將「%s」新增為聯絡人" "設定電子郵件" "請輸入您帳戶的電子郵件地址:" "電子郵件地址" From f7eead207407c25b41fe622e9bd4f29a2b50ee7b Mon Sep 17 00:00:00 2001 From: Andy Stadler <> Date: Tue, 24 Mar 2009 19:00:38 -0700 Subject: [PATCH 04/11] Automated import from //branches/cupcake/...@142109,142109 --- .../james/mime4j/decoder/DecoderUtil.java | 15 +++++- .../email/mail/internet/MimeUtilityTest.java | 46 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/org/apache/james/mime4j/decoder/DecoderUtil.java b/src/org/apache/james/mime4j/decoder/DecoderUtil.java index f7b9fa8f8..7876ff700 100644 --- a/src/org/apache/james/mime4j/decoder/DecoderUtil.java +++ b/src/org/apache/james/mime4j/decoder/DecoderUtil.java @@ -166,7 +166,20 @@ public class DecoderUtil { while (true) { int begin = body.indexOf("=?", previousEnd); - int end = begin == -1 ? -1 : body.indexOf("?=", begin + 2); + + // ANDROID: The mime4j original version has an error here. It gets confused if + // the encoded string begins with an '=' (just after "?Q?"). This patch seeks forward + // to find the two '?' in the "header", before looking for the final "?=". + int endScan = begin + 2; + if (begin != -1) { + int qm1 = body.indexOf('?', endScan + 2); + int qm2 = body.indexOf('?', qm1 + 1); + if (qm2 != -1) { + endScan = qm2 + 1; + } + } + + int end = begin == -1 ? -1 : body.indexOf("?=", endScan); if (end == -1) { if (previousEnd == 0) return body; diff --git a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java index b39e4cf0d..814774b3c 100644 --- a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java +++ b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java @@ -48,6 +48,31 @@ public class MimeUtilityTest extends TestCase { /** a typical multi-param header */ private final String HEADER_MULTI_PARAMETER = "header; Param1Name=Param1Value; Param2Name=Param2Value"; + + /** + * a string generated by google calendar that contains two interesting gotchas: + * 1. Uses windows-1252 encoding, and en-dash recoded appropriately (\u2013 / =96) + * 2. Because the first encoded char requires '=XX' encoding, we create an "internal" + * "?=" that the decoder must correctly skip over. + **/ + private final String CALENDAR_SUBJECT_UNICODE = + "=?windows-1252?Q?=5BReminder=5D_test_=40_Fri_Mar_20_10=3A30am_=96_11am_=28andro?=" + + "\r\n\t" + + "=?windows-1252?Q?id=2Etr=40gmail=2Ecom=29?="; + private final String CALENDAR_SUBJECT_PLAIN = + "[Reminder] test @ Fri Mar 20 10:30am \u2013 11am (android.tr@gmail.com)"; + + /** + * Some basic degenerate strings designed to exercise error handling in the decoder + */ + private final String CALENDAR_DEGENERATE_UNICODE_1 = + "=?windows-1252?Q=5B?="; + private final String CALENDAR_DEGENERATE_UNICODE_2 = + "=?windows-1252Q?=5B?="; + private final String CALENDAR_DEGENERATE_UNICODE_3 = + "=?windows-1252?="; + private final String CALENDAR_DEGENERATE_UNICODE_4 = + "=?windows-1252"; /** * Test that decode/unfold is efficient when it can be @@ -82,7 +107,26 @@ public class MimeUtilityTest extends TestCase { assertEquals(SHORT_UNICODE, result1); } - // TODO: tests for unfoldAndDecode(String s) + /** + * test decoding complex string from google calendar that has two gotchas for the decoder. + * also tests a couple of degenerate cases that should "fail" decoding and pass through. + */ + public void testComplexDecode() { + String result1 = MimeUtility.unfoldAndDecode(CALENDAR_SUBJECT_UNICODE); + assertEquals(CALENDAR_SUBJECT_PLAIN, result1); + + // These degenerate cases should "fail" and return the same string + String degenerate1 = MimeUtility.unfoldAndDecode(CALENDAR_DEGENERATE_UNICODE_1); + assertEquals("degenerate case 1", CALENDAR_DEGENERATE_UNICODE_1, degenerate1); + String degenerate2 = MimeUtility.unfoldAndDecode(CALENDAR_DEGENERATE_UNICODE_2); + assertEquals("degenerate case 2", CALENDAR_DEGENERATE_UNICODE_2, degenerate2); + String degenerate3 = MimeUtility.unfoldAndDecode(CALENDAR_DEGENERATE_UNICODE_3); + assertEquals("degenerate case 3", CALENDAR_DEGENERATE_UNICODE_3, degenerate3); + String degenerate4 = MimeUtility.unfoldAndDecode(CALENDAR_DEGENERATE_UNICODE_4); + assertEquals("degenerate case 4", CALENDAR_DEGENERATE_UNICODE_4, degenerate4); + } + + // TODO: more tests for unfoldAndDecode(String s) /** * Test that fold/encode is efficient when it can be From 0589ff73b0b3a3c40c6f6e58bcd4f5a2e07b47bc Mon Sep 17 00:00:00 2001 From: Andy Stadler <> Date: Tue, 24 Mar 2009 19:05:40 -0700 Subject: [PATCH 05/11] Automated import from //branches/cupcake/...@142151,142151 --- res/values/strings.xml | 3 +++ src/com/android/email/Email.java | 8 ++++++++ src/com/android/email/activity/MessageCompose.java | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/res/values/strings.xml b/res/values/strings.xml index de4f706a0..57c9fa20f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -157,6 +157,9 @@ You must add at least one recipient. Some attachments cannot be forwarded because they have not downloaded. + + File too large to attach. To: diff --git a/src/com/android/email/Email.java b/src/com/android/email/Email.java index 81aae8e2b..2e49bff76 100644 --- a/src/com/android/email/Email.java +++ b/src/com/android/email/Email.java @@ -116,6 +116,14 @@ public class Email extends Application { */ public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (5 * 1024 * 1024); + /** + * The maximum size of an attachment we're willing to upload (measured as stored on disk). + * Attachments that are base64 encoded (most) will be about 1.375x their actual size + * so we should probably factor that in. A 5MB attachment will generally be around + * 6.8MB uploaded. + */ + public static final int MAX_ATTACHMENT_UPLOAD_SIZE = (5 * 1024 * 1024); + /** * Called throughout the application when the number of accounts has changed. This method * enables or disables the Compose activity, the boot receiver and the service based on diff --git a/src/com/android/email/activity/MessageCompose.java b/src/com/android/email/activity/MessageCompose.java index 424e15eff..4fca856f9 100644 --- a/src/com/android/email/activity/MessageCompose.java +++ b/src/com/android/email/activity/MessageCompose.java @@ -832,6 +832,13 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus attachment.name = uri.getLastPathSegment(); } + // Before attaching the attachment, make sure it meets any other pre-attach criteria + if (attachment.size > Email.MAX_ATTACHMENT_UPLOAD_SIZE) { + Toast.makeText(this, R.string.message_compose_attachment_size, Toast.LENGTH_LONG) + .show(); + return; + } + View view = getLayoutInflater().inflate( R.layout.message_compose_attachment, mAttachments, From d351a92869438d937b84c27b026803a92923d1db Mon Sep 17 00:00:00 2001 From: Tadashi Takaoka <> Date: Tue, 24 Mar 2009 19:44:56 -0700 Subject: [PATCH 06/11] Automated import from //branches/cupcake/...@142456,142456 --- .../james/mime4j/codec/EncoderUtil.java | 6 +++--- .../email/mail/internet/MimeUtilityTest.java | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/org/apache/james/mime4j/codec/EncoderUtil.java b/src/org/apache/james/mime4j/codec/EncoderUtil.java index d6f3998d4..c81a83c88 100644 --- a/src/org/apache/james/mime4j/codec/EncoderUtil.java +++ b/src/org/apache/james/mime4j/codec/EncoderUtil.java @@ -374,14 +374,14 @@ public class EncoderUtil { sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]); sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]); sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]); - sb.append(BASE64_PAD); + sb.append((char) BASE64_PAD); } else if (idx == end - 1) { int data = (bytes[idx] & 0xff) << 16; sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]); sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]); - sb.append(BASE64_PAD); - sb.append(BASE64_PAD); + sb.append((char) BASE64_PAD); + sb.append((char) BASE64_PAD); } return sb.toString(); diff --git a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java index 814774b3c..641fe4973 100644 --- a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java +++ b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java @@ -39,6 +39,14 @@ public class MimeUtilityTest extends TestCase { private final String SHORT_UNICODE = "\u2191\u2193\u2190\u2192"; private final String SHORT_UNICODE_ENCODED = "=?UTF-8?B?4oaR4oaT4oaQ4oaS?="; + /** dollar and euro sign */ + private final String PADDED2_UNICODE = "$\u20AC"; + private final String PADDED2_UNICODE_ENCODED = "=?UTF-8?B?JOKCrA==?="; + private final String PADDED1_UNICODE = "$$\u20AC"; + private final String PADDED1_UNICODE_ENCODED = "=?UTF-8?B?JCTigqw=?="; + private final String PADDED0_UNICODE = "$$$\u20AC"; + private final String PADDED0_UNICODE_ENCODED = "=?UTF-8?B?JCQk4oKs?="; + /** a string without any unicode */ private final String SHORT_PLAIN = "abcd"; @@ -141,6 +149,19 @@ public class MimeUtilityTest extends TestCase { assertSame(SHORT_PLAIN, result3); } + /** + * Test about base64 padding variety. + */ + public void testPaddingOfFoldAndEncode2() { + String result1 = MimeUtility.foldAndEncode2(PADDED2_UNICODE, 0); + String result2 = MimeUtility.foldAndEncode2(PADDED1_UNICODE, 0); + String result3 = MimeUtility.foldAndEncode2(PADDED0_UNICODE, 0); + + assertEquals("padding 2", PADDED2_UNICODE_ENCODED, result1); + assertEquals("padding 1", PADDED1_UNICODE_ENCODED, result2); + assertEquals("padding 0", PADDED0_UNICODE_ENCODED, result3); + } + // TODO: more tests for foldAndEncode(String s) /** From 7e7abb0e3d77eab11f28f08475a941fdc3a82c13 Mon Sep 17 00:00:00 2001 From: Tadashi Takaoka <> Date: Tue, 24 Mar 2009 19:45:36 -0700 Subject: [PATCH 07/11] Automated import from //branches/cupcake/...@142457,142457 --- src/com/android/email/Email.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/com/android/email/Email.java b/src/com/android/email/Email.java index 2e49bff76..c2fc1ec10 100644 --- a/src/com/android/email/Email.java +++ b/src/com/android/email/Email.java @@ -73,7 +73,6 @@ public class Email extends Application { * The MIME type(s) of attachments we're not willing to view. */ public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] { - "image/gif", }; /** @@ -87,7 +86,6 @@ public class Email extends Application { * The MIME type(s) of attachments we're not willing to download to SD. */ public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] { - "image/gif", }; /** From 44cf2e2882d9d96e6f515c36334cac5ae8e2c305 Mon Sep 17 00:00:00 2001 From: Tadashi Takaoka <> Date: Wed, 25 Mar 2009 01:29:24 -0700 Subject: [PATCH 08/11] Automated import from //branches/cupcake/...@142522,142522 --- .../james/mime4j/codec/EncoderUtil.java | 12 ++-- .../email/mail/internet/MimeUtilityTest.java | 70 +++++++++++++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/org/apache/james/mime4j/codec/EncoderUtil.java b/src/org/apache/james/mime4j/codec/EncoderUtil.java index c81a83c88..6841bc998 100644 --- a/src/org/apache/james/mime4j/codec/EncoderUtil.java +++ b/src/org/apache/james/mime4j/codec/EncoderUtil.java @@ -518,12 +518,14 @@ public class EncoderUtil { if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) { return prefix + encodeB(bytes) + ENC_WORD_SUFFIX; } else { - String part1 = text.substring(0, text.length() / 2); + int splitOffset = text.offsetByCodePoints(text.length() / 2, -1); + + String part1 = text.substring(0, splitOffset); byte[] bytes1 = encode(part1, charset); String word1 = encodeB(prefix, part1, usedCharacters, charset, bytes1); - String part2 = text.substring(text.length() / 2); + String part2 = text.substring(splitOffset); byte[] bytes2 = encode(part2, charset); String word2 = encodeB(prefix, part2, 0, charset, bytes2); @@ -544,12 +546,14 @@ public class EncoderUtil { if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) { return prefix + encodeQ(bytes, usage) + ENC_WORD_SUFFIX; } else { - String part1 = text.substring(0, text.length() / 2); + int splitOffset = text.offsetByCodePoints(text.length() / 2, -1); + + String part1 = text.substring(0, splitOffset); byte[] bytes1 = encode(part1, charset); String word1 = encodeQ(prefix, part1, usage, usedCharacters, charset, bytes1); - String part2 = text.substring(text.length() / 2); + String part2 = text.substring(splitOffset); byte[] bytes2 = encode(part2, charset); String word2 = encodeQ(prefix, part2, usage, 0, charset, bytes2); diff --git a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java index 641fe4973..5f53f6330 100644 --- a/tests/src/com/android/email/mail/internet/MimeUtilityTest.java +++ b/tests/src/com/android/email/mail/internet/MimeUtilityTest.java @@ -50,6 +50,38 @@ public class MimeUtilityTest extends TestCase { /** a string without any unicode */ private final String SHORT_PLAIN = "abcd"; + /** long subject which will be split into two MIME/Base64 chunks */ + private final String LONG_UNICODE_SPLIT = + "$" + + "\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC" + + "\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC\u20AC"; + private final String LONG_UNICODE_SPLIT_ENCODED = + "=?UTF-8?B?JOKCrOKCrOKCrOKCrOKCrOKCrOKCrOKCrA==?=" + "\r\n " + + "=?UTF-8?B?4oKs4oKs4oKs4oKs4oKs4oKs4oKs4oKs4oKs4oKs4oKs4oKs?="; + + /** strings that use supplemental characters and really stress encode/decode */ + // actually it's U+10400 + private final String SHORT_SUPPLEMENTAL = "\uD801\uDC00"; + private final String SHORT_SUPPLEMENTAL_ENCODED = "=?UTF-8?B?8JCQgA==?="; + private final String LONG_SUPPLEMENTAL = SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL; + private final String LONG_SUPPLEMENTAL_ENCODED = + "=?UTF-8?B?8JCQgPCQkIDwkJCA8JCQgA==?=" + "\r\n " + + "=?UTF-8?B?8JCQgPCQkIDwkJCA8JCQgPCQkIDwkJCA?="; + private final String LONG_SUPPLEMENTAL_2 = "a" + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL + SHORT_SUPPLEMENTAL; + private final String LONG_SUPPLEMENTAL_ENCODED_2 = + "=?UTF-8?B?YfCQkIDwkJCA8JCQgPCQkIA=?=" + "\r\n " + + "=?UTF-8?B?8JCQgPCQkIDwkJCA8JCQgPCQkIDwkJCA?="; + // Earth is U+1D300. + private final String LONG_SUPPLEMENTAL_QP = + "*Monogram for Earth \uD834\uDF00. Monogram for Human \u268b."; + private final String LONG_SUPPLEMENTAL_QP_ENCODED = + "=?UTF-8?Q?*Monogram_for_Earth_?=" + "\r\n " + + "=?UTF-8?Q?=F0=9D=8C=80._Monogram_for_Human_=E2=9A=8B.?="; + /** a typical no-param header */ private final String HEADER_NO_PARAMETER = "header"; @@ -172,6 +204,44 @@ public class MimeUtilityTest extends TestCase { assertEquals(SHORT_UNICODE_ENCODED, result1); } + /** + * Test that foldAndEncode2 is working for long strings which needs splitting. + */ + public void testFoldAndEncode2WithLongSplit() { + String result = MimeUtility.foldAndEncode2(LONG_UNICODE_SPLIT, "Subject: ".length()); + + assertEquals("long string", LONG_UNICODE_SPLIT_ENCODED, result); + } + + /** + * Tests of foldAndEncode2 that involve supplemental characters (UTF-32) + * + * Note that the difference between LONG_SUPPLEMENTAL and LONG_SUPPLEMENTAL_2 is the + * insertion of a single character at the head of the string. This is intended to disrupt + * the code that splits the long string into multiple encoded words, and confirm that it + * properly applies the breaks between UTF-32 code points. + */ + public void testFoldAndEncode2Supplemental() { + String result1 = MimeUtility.foldAndEncode2(SHORT_SUPPLEMENTAL, "Subject: ".length()); + String result2 = MimeUtility.foldAndEncode2(LONG_SUPPLEMENTAL, "Subject: ".length()); + String result3 = MimeUtility.foldAndEncode2(LONG_SUPPLEMENTAL_2, "Subject: ".length()); + assertEquals("short supplemental", SHORT_SUPPLEMENTAL_ENCODED, result1); + assertEquals("long supplemental", LONG_SUPPLEMENTAL_ENCODED, result2); + assertEquals("long supplemental 2", LONG_SUPPLEMENTAL_ENCODED_2, result3); + } + + /** + * Tests of foldAndEncode2 that involve supplemental characters (UTF-32) + * + * Note that the difference between LONG_SUPPLEMENTAL and LONG_SUPPLEMENTAL_QP is that + * the former will be encoded as base64 but the latter will be encoded as quoted printable. + */ + public void testFoldAndEncode2SupplementalQuotedPrintable() { + String result = MimeUtility.foldAndEncode2(LONG_SUPPLEMENTAL_QP, "Subject: ".length()); + assertEquals("long supplement quoted printable", + LONG_SUPPLEMENTAL_QP_ENCODED, result); + } + // TODO: more tests for foldAndEncode2(String s) // TODO: more tests for fold(String s, int usedCharacters) From 6a571d8c6c2ffadbc978061242d67e1ccf3265c2 Mon Sep 17 00:00:00 2001 From: Andy Stadler <> Date: Wed, 25 Mar 2009 15:07:39 -0700 Subject: [PATCH 09/11] Automated import from //branches/cupcake/...@142594,142594 --- res/values/strings.xml | 4 ++ .../android/email/activity/MessageView.java | 42 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 57c9fa20f..390501bb7 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -190,6 +190,10 @@ the email address, e.g. "Add xyz@foo.com to contacts" --> Add \"%s\" to contacts + + This attachment cannot be displayed. Set up email diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java index 22f248784..cfa9c4585 100644 --- a/src/com/android/email/activity/MessageView.java +++ b/src/com/android/email/activity/MessageView.java @@ -36,6 +36,7 @@ import com.android.email.provider.AttachmentProvider; import org.apache.commons.io.IOUtils; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -134,6 +135,7 @@ public class MessageView extends Activity private static final int MSG_SHOW_SHOW_PICTURES = 9; private static final int MSG_FETCHING_ATTACHMENT = 10; private static final int MSG_SET_SENDER_PRESENCE = 11; + private static final int MSG_VIEW_ATTACHMENT_ERROR = 12; @Override public void handleMessage(android.os.Message msg) { @@ -188,6 +190,11 @@ public class MessageView extends Activity case MSG_SET_SENDER_PRESENCE: updateSenderPresence(msg.arg1); break; + case MSG_VIEW_ATTACHMENT_ERROR: + Toast.makeText(MessageView.this, + getString(R.string.message_view_display_attachment_toast), + Toast.LENGTH_SHORT).show(); + break; default: super.handleMessage(msg); } @@ -260,6 +267,10 @@ public class MessageView extends Activity .obtain(this, MSG_SET_SENDER_PRESENCE, presenceIconId, 0) .sendToTarget(); } + + public void attachmentViewError() { + sendEmptyMessage(MSG_VIEW_ATTACHMENT_ERROR); + } } /** @@ -1037,20 +1048,26 @@ public class MessageView extends Activity out.close(); in.close(); mHandler.attachmentSaved(file.getName()); - new MediaScannerNotifier(MessageView.this, file); + new MediaScannerNotifier(MessageView.this, file, mHandler); } catch (IOException ioe) { mHandler.attachmentNotSaved(); } } else { - Uri uri = AttachmentProvider.getAttachmentUri( - mAccount, - attachment.part.getAttachmentId()); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(uri); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startActivity(intent); + try { + Uri uri = AttachmentProvider.getAttachmentUri( + mAccount, + attachment.part.getAttachmentId()); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(intent); + } catch (ActivityNotFoundException e) { + mHandler.attachmentViewError(); + // TODO: Add a proper warning message (and lots of upstream cleanup to prevent + // it from happening) in the next release. + } } } @@ -1072,10 +1089,12 @@ public class MessageView extends Activity private Context mContext; private MediaScannerConnection mConnection; private File mFile; + MessageViewHandler mHandler; - public MediaScannerNotifier(Context context, File file) { + public MediaScannerNotifier(Context context, File file, MessageViewHandler handler) { mContext = context; mFile = file; + mHandler = handler; mConnection = new MediaScannerConnection(context, this); mConnection.connect(); } @@ -1091,9 +1110,14 @@ public class MessageView extends Activity intent.setData(uri); mContext.startActivity(intent); } + } catch (ActivityNotFoundException e) { + mHandler.attachmentViewError(); + // TODO: Add a proper warning message (and lots of upstream cleanup to prevent + // it from happening) in the next release. } finally { mConnection.disconnect(); mContext = null; + mHandler = null; } } } From bcab70b68c8459ffec04555f365033f280325b2b Mon Sep 17 00:00:00 2001 From: Eric Fischer <> Date: Wed, 25 Mar 2009 15:22:44 -0700 Subject: [PATCH 10/11] Automated import from //branches/cupcake/...@142643,142643 --- res/values-ja/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index b1944e756..4629cfaed 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -75,6 +75,8 @@ "元のメッセージ" "宛先を入力してください。" "ダウンロードされていない添付ファイルは転送できません。" + + "To:" "Cc:" "開く" From 8d448bbd7cdad4e56402cc7a06c7be749d941dcd Mon Sep 17 00:00:00 2001 From: Eric Fischer <> Date: Wed, 25 Mar 2009 21:52:36 -0700 Subject: [PATCH 11/11] Automated import from //branches/cupcake/...@142860,142860 --- res/values-cs/strings.xml | 2 ++ res/values-de/strings.xml | 2 ++ res/values-es/strings.xml | 2 ++ res/values-fr/strings.xml | 2 ++ res/values-it/strings.xml | 2 ++ res/values-ja/strings.xml | 4 ++-- res/values-ko/strings.xml | 2 ++ res/values-nb/strings.xml | 4 ++++ res/values-nl/strings.xml | 2 ++ res/values-pl/strings.xml | 2 ++ res/values-ru/strings.xml | 2 ++ res/values-zh-rCN/strings.xml | 2 ++ res/values-zh-rTW/strings.xml | 2 ++ res/values/strings.xml | 6 +++--- 14 files changed, 31 insertions(+), 5 deletions(-) diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml index 2bed10a90..7e4dca24f 100644 --- a/res/values-cs/strings.xml +++ b/res/values-cs/strings.xml @@ -75,6 +75,7 @@ "Text v uvozovkách" "Je třeba přidat nejméně jednoho příjemce." "Některé přílohy nelze přeposlat, protože nebyly staženy." + "Soubor nelze připojit, protože je příliš velký." "Komu:" "Kopie:" "Otevřít" @@ -89,6 +90,7 @@ "Zpráva byla uložena jako koncept." "Přidat kontakt" "Přidat „%s“ do kontaktů" + "Tuto přílohu nelze zobrazit." "Nastavit e-mail" "Zadejte e-mailovou adresu účtu:" "E-mailová adresa" diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 3bfa8c709..ce4229b3a 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -75,6 +75,7 @@ "Zitierter Text" "Sie müssen mindestens einen Empfänger hinzufügen." "Einige Anhänge können nicht weitergeleitet werden, da Sie noch nicht heruntergeladen wurden." + "Dateianhang zu groß" "An:" "Cc:" "Öffnen" @@ -89,6 +90,7 @@ "Nachricht als Entwurf gespeichert" "Kontakt hinzufügen" "\"%s\" zu den Kontakten hinzufügen" + "Dieser Anhang kann nicht angezeigt werden." "E-Mail einrichten" "Geben Sie Ihre im Konto gespeicherte E-Mail-Adresse ein:" "E-Mail-Adresse" diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index e253906ba..30f1fd907 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -75,6 +75,7 @@ "Texto entre comillas" "Debes especificar, al menos, un destinatario." "No se pueden reenviar algunos archivos adjuntos porque no se han descargado." + "El archivo es demasiado grande para adjuntarlo." "Para:" "Cc:" "Abrir" @@ -89,6 +90,7 @@ "Mensaje guardado como borrador" "Añadir contacto" "Añadir \"%s\" a los contactos" + "No se puede mostrar este archivo adjunto." "Configurar correo electrónico" "Introduce la dirección de correo electrónico de tu cuenta:" "Dirección de correo electrónico" diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 9a0f522ea..734d1e65c 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -75,6 +75,7 @@ "Texte du message précédent" "Vous devez ajouter au moins un destinataire." "Impossible de transférer certaines pièces jointes : elles n\'ont pas été téléchargées." + "Impossible de joindre le fichier, car il est trop volumineux." "À :" "Cc :" "Ouvrir" @@ -89,6 +90,7 @@ "Message enregistré comme brouillon." "Ajouter un contact" "Ajouter \"%s\" aux contacts" + "Impossible d\'afficher cette pièce jointe." "Configurer la messagerie électronique" "Saisissez l\'adresse e-mail de votre compte :" "Adresse e-mail" diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index b95d1c6bb..76743debf 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -75,6 +75,7 @@ "Testo tra virgolette" "Devi aggiungere almeno un destinatario." "Impossibile inviare alcuni allegati poiché non sono stati scaricati." + "File troppo grande. Impossibile allegarlo." "A:" "Cc:" "Apri" @@ -89,6 +90,7 @@ "Messaggio salvato come bozza." "Aggiungi contatto" "Aggiungi \"%s\" ai contatti" + "Impossibile visualizzare l\'allegato." "Imposta email" "Digita l\'indirizzo email del tuo account:" "Indirizzo email" diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index 4629cfaed..136dfd729 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -75,8 +75,7 @@ "元のメッセージ" "宛先を入力してください。" "ダウンロードされていない添付ファイルは転送できません。" - - + "添付ファイルが大きすぎます。" "To:" "Cc:" "開く" @@ -91,6 +90,7 @@ "メッセージを下書き保存しました。" "連絡先を追加" "%s を連絡先に追加する" + "この添付ファイルは表示できません。" "メールアカウントの登録" "メールのアカウント情報を入力:" "メールアドレス" diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index b85d26aa4..a5c762c73 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -75,6 +75,7 @@ "받은메일" "받는 사람을 한 명 이상 추가해야 합니다." "일부 첨부파일이 다운로드되지 않아 전달할 수 없습니다." + "파일이 너무 커서 첨부할 수 없습니다." "받는사람:" "참조:" "열기" @@ -89,6 +90,7 @@ "메일을 임시로 저장했습니다." "주소 추가" "주소록에 \'%s\' 추가" + "이 첨부파일은 표시할 수 없습니다." "이메일 설정" "계정 이메일 주소 입력:" "이메일 주소" diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index 78dbc31f9..e9803bcb3 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -73,6 +73,9 @@ "Sitert tekst" "Du må legge til minst én mottager." "Noen vedlegg kunne ikke sendes videre fordi de ikke er blitt lastet ned." + + + "Til:" "Kopi:" "Åpne" @@ -89,6 +92,7 @@ + "Kan ikke vise vedlegget." "Sett opp e-post" "Skriv e-postadressen til kontoen din:" "E-postadresse" diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index b209fd167..0a04cd47c 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -75,6 +75,7 @@ "Geciteerde tekst" "Je moet minstens één ontvanger toevoegen." "Sommige bijlages kunnen niet worden doorgestuurd omdat ze niet zijn gedownload." + "Bestand is te groot om bij te voegen." "Aan:" "Cc:" "Openen" @@ -89,6 +90,7 @@ "Bericht opgeslagen als concept." "Contactpersoon toevoegen" "Voeg \"%s\" toe aan contactpersonen" + "Deze bijlage kan niet worden verwijderd." "E-mail instellen" "Typ het e-mailadres van je account:" "E-mailadres" diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 3c1ca1da7..24c30d3ab 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -75,6 +75,7 @@ "Cytowany tekst" "Musisz dodać co najmniej jednego adresata." "Nie można przekazać dalej niektórych załączników, ponieważ nie zostały one pobrane." + "Plik jest zbyt duży do załączenia." "Do:" "DW:" "Otwórz" @@ -89,6 +90,7 @@ "Wiadomość została zapisana jako wersja robocza." "Dodaj kontakt" "Dodaj adres „%s” do kontaktów" + "Nie można wyświetlić tego załącznika." "Skonfiguruj konto e-mail" "Podaj adres e-mail swojego konta:" "Adres e-mail" diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 58ee0cfa1..295a7d0a9 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -75,6 +75,7 @@ "Цитируемый текст" "Необходимо добавить хотя бы одного получателя." "Некоторые приложения нельзя переслать, поскольку они не загружены." + "Файл имеет слишком большой размер и не может быть вложен." "Кому:" "Копия:" "Открыть" @@ -89,6 +90,7 @@ "Письмо сохранено как черновик." "Добавить контакт" "Добавить адрес %s в контакты" + "Не удается показать это приложение." "Настройка электронной почты" "Укажите почтовый адрес своего аккаунта:" "Адрес электронной почты" diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 2964b889c..c7fb33a6b 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -75,6 +75,7 @@ "引用文字" "您必须至少添加一个收件人。" "某些附件尚未下载,因此无法转发。" + "文件太大,无法附加。" "收件人:" "抄送:" "打开" @@ -89,6 +90,7 @@ "邮件已另存为草稿。" "添加联系人" "将“%s”添加到通讯录中" + "此附件无法显示。" "设置电子邮件" "键入您的帐户电子邮件地址:" "电子邮件地址" diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index 093ba286f..3946d53fa 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -75,6 +75,7 @@ "引用的文字" "您必須新增至少一位收件者。" "尚未下載部分附件,因此無法予以轉寄。" + "檔案太大,超過附檔上限。" "收件者:" "副本:" "開啟" @@ -89,6 +90,7 @@ "已儲存郵件草稿。" "新增聯絡人" "將「%s」新增為聯絡人" + "無法顯示此附件。" "設定電子郵件" "請輸入您帳戶的電子郵件地址:" "電子郵件地址" diff --git a/res/values/strings.xml b/res/values/strings.xml index 390501bb7..509fb6d1d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -158,8 +158,8 @@ Some attachments cannot be forwarded because they have not downloaded. - File too large to attach. + borrowed from camera app (msgid="8944461117941172986") --> + File too large to attach. To: @@ -193,7 +193,7 @@ This attachment cannot be displayed. + msgid="2079093904785941494">This attachment cannot be displayed. Set up email