From c61dc117d2438e9491511c163adee3623a62c475 Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 1 May 2022 23:39:36 +0200 Subject: [PATCH] Replace usage of `android.text.util.Rfc822Token[izer]` in `Address` At some point we need to clean up our email address parser mess. But for now we just copy Android's implementation of `Rfc822Token` and `Rfc822Tokenizer`. --- .../main/java/com/fsck/k9/mail/Address.java | 5 +- .../com/fsck/k9/mail/helper/Rfc822Token.java | 205 ++++++++++++ .../fsck/k9/mail/helper/Rfc822Tokenizer.java | 313 ++++++++++++++++++ 3 files changed, 520 insertions(+), 3 deletions(-) create mode 100644 mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Token.java create mode 100644 mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Tokenizer.java diff --git a/mail/common/src/main/java/com/fsck/k9/mail/Address.java b/mail/common/src/main/java/com/fsck/k9/mail/Address.java index fdaa1d9f7..d1382c866 100644 --- a/mail/common/src/main/java/com/fsck/k9/mail/Address.java +++ b/mail/common/src/main/java/com/fsck/k9/mail/Address.java @@ -7,6 +7,8 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; +import com.fsck.k9.mail.helper.Rfc822Token; +import com.fsck.k9.mail.helper.Rfc822Tokenizer; import com.fsck.k9.mail.helper.TextUtils; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.codec.DecodeMonitor; @@ -18,9 +20,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.VisibleForTesting; import timber.log.Timber; -import android.text.util.Rfc822Token; -import android.text.util.Rfc822Tokenizer; - public class Address implements Serializable { private static final Pattern ATOM = Pattern.compile("^(?:[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]|\\s)+$"); diff --git a/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Token.java b/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Token.java new file mode 100644 index 000000000..bf0721f91 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Token.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed 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 com.fsck.k9.mail.helper; + + +import org.jetbrains.annotations.Nullable; + + +/** + * This class stores an RFC 822-like name, address, and comment, + * and provides methods to convert them to quoted strings. + */ +public class Rfc822Token { + @Nullable + private String mName, mAddress, mComment; + + /** + * Creates a new Rfc822Token with the specified name, address, + * and comment. + */ + public Rfc822Token(@Nullable String name, @Nullable String address, @Nullable String comment) { + mName = name; + mAddress = address; + mComment = comment; + } + + /** + * Returns the name part. + */ + @Nullable + public String getName() { + return mName; + } + + /** + * Returns the address part. + */ + @Nullable + public String getAddress() { + return mAddress; + } + + /** + * Returns the comment part. + */ + @Nullable + public String getComment() { + return mComment; + } + + /** + * Changes the name to the specified name. + */ + public void setName(@Nullable String name) { + mName = name; + } + + /** + * Changes the address to the specified address. + */ + public void setAddress(@Nullable String address) { + mAddress = address; + } + + /** + * Changes the comment to the specified comment. + */ + public void setComment(@Nullable String comment) { + mComment = comment; + } + + /** + * Returns the name (with quoting added if necessary), + * the comment (in parentheses), and the address (in angle brackets). + * This should be suitable for inclusion in an RFC 822 address list. + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + + if (mName != null && mName.length() != 0) { + sb.append(quoteNameIfNecessary(mName)); + sb.append(' '); + } + + if (mComment != null && mComment.length() != 0) { + sb.append('('); + sb.append(quoteComment(mComment)); + sb.append(") "); + } + + if (mAddress != null && mAddress.length() != 0) { + sb.append('<'); + sb.append(mAddress); + sb.append('>'); + } + + return sb.toString(); + } + + /** + * Returns the name, conservatively quoting it if there are any + * characters that are likely to cause trouble outside of a + * quoted string, or returning it literally if it seems safe. + */ + public static String quoteNameIfNecessary(String name) { + int len = name.length(); + + for (int i = 0; i < len; i++) { + char c = name.charAt(i); + + if (! ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c == ' ') || + (c >= '0' && c <= '9'))) { + return '"' + quoteName(name) + '"'; + } + } + + return name; + } + + /** + * Returns the name, with internal backslashes and quotation marks + * preceded by backslashes. The outer quote marks themselves are not + * added by this method. + */ + public static String quoteName(String name) { + StringBuilder sb = new StringBuilder(); + + int len = name.length(); + for (int i = 0; i < len; i++) { + char c = name.charAt(i); + + if (c == '\\' || c == '"') { + sb.append('\\'); + } + + sb.append(c); + } + + return sb.toString(); + } + + /** + * Returns the comment, with internal backslashes and parentheses + * preceded by backslashes. The outer parentheses themselves are + * not added by this method. + */ + public static String quoteComment(String comment) { + int len = comment.length(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < len; i++) { + char c = comment.charAt(i); + + if (c == '(' || c == ')' || c == '\\') { + sb.append('\\'); + } + + sb.append(c); + } + + return sb.toString(); + } + + public int hashCode() { + int result = 17; + if (mName != null) result = 31 * result + mName.hashCode(); + if (mAddress != null) result = 31 * result + mAddress.hashCode(); + if (mComment != null) result = 31 * result + mComment.hashCode(); + return result; + } + + private static boolean stringEquals(String a, String b) { + if (a == null) { + return (b == null); + } else { + return (a.equals(b)); + } + } + + public boolean equals(@Nullable Object o) { + if (!(o instanceof Rfc822Token)) { + return false; + } + Rfc822Token other = (Rfc822Token) o; + return (stringEquals(mName, other.mName) && + stringEquals(mAddress, other.mAddress) && + stringEquals(mComment, other.mComment)); + } +} diff --git a/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Tokenizer.java b/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Tokenizer.java new file mode 100644 index 000000000..faf640ab0 --- /dev/null +++ b/mail/common/src/main/java/com/fsck/k9/mail/helper/Rfc822Tokenizer.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed 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 com.fsck.k9.mail.helper; + + +import java.util.ArrayList; +import java.util.Collection; + +/** + * This class works as a Tokenizer for MultiAutoCompleteTextView for + * address list fields, and also provides a method for converting + * a string of addresses (such as might be typed into such a field) + * into a series of Rfc822Tokens. + */ +public class Rfc822Tokenizer { + + /** + * This constructor will try to take a string like + * "Foo Bar (something) <foo\@google.com>, + * blah\@google.com (something)" + * and convert it into one or more Rfc822Tokens, output into the supplied + * collection. + * + * It does *not* decode MIME encoded-words; charset conversion + * must already have taken place if necessary. + * It will try to be tolerant of broken syntax instead of + * returning an error. + * + */ + public static void tokenize(CharSequence text, Collection out) { + StringBuilder name = new StringBuilder(); + StringBuilder address = new StringBuilder(); + StringBuilder comment = new StringBuilder(); + + int i = 0; + int cursor = text.length(); + + while (i < cursor) { + char c = text.charAt(i); + + if (c == ',' || c == ';') { + i++; + + while (i < cursor && text.charAt(i) == ' ') { + i++; + } + + crunch(name); + + if (address.length() > 0) { + out.add(new Rfc822Token(name.toString(), + address.toString(), + comment.toString())); + } else if (name.length() > 0) { + out.add(new Rfc822Token(null, + name.toString(), + comment.toString())); + } + + name.setLength(0); + address.setLength(0); + comment.setLength(0); + } else if (c == '"') { + i++; + + while (i < cursor) { + c = text.charAt(i); + + if (c == '"') { + i++; + break; + } else if (c == '\\') { + if (i + 1 < cursor) { + name.append(text.charAt(i + 1)); + } + i += 2; + } else { + name.append(c); + i++; + } + } + } else if (c == '(') { + int level = 1; + i++; + + while (i < cursor && level > 0) { + c = text.charAt(i); + + if (c == ')') { + if (level > 1) { + comment.append(c); + } + + level--; + i++; + } else if (c == '(') { + comment.append(c); + level++; + i++; + } else if (c == '\\') { + if (i + 1 < cursor) { + comment.append(text.charAt(i + 1)); + } + i += 2; + } else { + comment.append(c); + i++; + } + } + } else if (c == '<') { + i++; + + while (i < cursor) { + c = text.charAt(i); + + if (c == '>') { + i++; + break; + } else { + address.append(c); + i++; + } + } + } else if (c == ' ') { + name.append('\0'); + i++; + } else { + name.append(c); + i++; + } + } + + crunch(name); + + if (address.length() > 0) { + out.add(new Rfc822Token(name.toString(), + address.toString(), + comment.toString())); + } else if (name.length() > 0) { + out.add(new Rfc822Token(null, + name.toString(), + comment.toString())); + } + } + + /** + * This method will try to take a string like + * "Foo Bar (something) <foo\@google.com>, + * blah\@google.com (something)" + * and convert it into one or more Rfc822Tokens. + * It does *not* decode MIME encoded-words; charset conversion + * must already have taken place if necessary. + * It will try to be tolerant of broken syntax instead of + * returning an error. + */ + public static Rfc822Token[] tokenize(CharSequence text) { + ArrayList out = new ArrayList(); + tokenize(text, out); + return out.toArray(new Rfc822Token[out.size()]); + } + + private static void crunch(StringBuilder sb) { + int i = 0; + int len = sb.length(); + + while (i < len) { + char c = sb.charAt(i); + + if (c == '\0') { + if (i == 0 || i == len - 1 || + sb.charAt(i - 1) == ' ' || + sb.charAt(i - 1) == '\0' || + sb.charAt(i + 1) == ' ' || + sb.charAt(i + 1) == '\0') { + sb.deleteCharAt(i); + len--; + } else { + i++; + } + } else { + i++; + } + } + + for (i = 0; i < len; i++) { + if (sb.charAt(i) == '\0') { + sb.setCharAt(i, ' '); + } + } + } + + /** + * {@inheritDoc} + */ + public int findTokenStart(CharSequence text, int cursor) { + /* + * It's hard to search backward, so search forward until + * we reach the cursor. + */ + + int best = 0; + int i = 0; + + while (i < cursor) { + i = findTokenEnd(text, i); + + if (i < cursor) { + i++; // Skip terminating punctuation + + while (i < cursor && text.charAt(i) == ' ') { + i++; + } + + if (i < cursor) { + best = i; + } + } + } + + return best; + } + + /** + * {@inheritDoc} + */ + public int findTokenEnd(CharSequence text, int cursor) { + int len = text.length(); + int i = cursor; + + while (i < len) { + char c = text.charAt(i); + + if (c == ',' || c == ';') { + return i; + } else if (c == '"') { + i++; + + while (i < len) { + c = text.charAt(i); + + if (c == '"') { + i++; + break; + } else if (c == '\\' && i + 1 < len) { + i += 2; + } else { + i++; + } + } + } else if (c == '(') { + int level = 1; + i++; + + while (i < len && level > 0) { + c = text.charAt(i); + + if (c == ')') { + level--; + i++; + } else if (c == '(') { + level++; + i++; + } else if (c == '\\' && i + 1 < len) { + i += 2; + } else { + i++; + } + } + } else if (c == '<') { + i++; + + while (i < len) { + c = text.charAt(i); + + if (c == '>') { + i++; + break; + } else { + i++; + } + } + } else { + i++; + } + } + + return i; + } + + /** + * Terminates the specified address with a comma and space. + * This assumes that the specified text already has valid syntax. + * The Adapter subclass's convertToString() method must make that + * guarantee. + */ + public CharSequence terminateToken(CharSequence text) { + return text + ", "; + } +}