PageRenderTime 43ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/src/com/android/emailcommon/mail/Address.java

https://github.com/SOKP-1/packages_apps_UnifiedEmail
Java | 462 lines | 239 code | 41 blank | 182 comment | 74 complexity | 62ca9eecb5114b7127417767f926ecb1 MD5 | raw file
  1. /*
  2. * Copyright (C) 2008 The Android Open Source Project
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.android.emailcommon.mail;
  17. import android.text.TextUtils;
  18. import android.text.util.Rfc822Token;
  19. import android.text.util.Rfc822Tokenizer;
  20. import com.google.common.annotations.VisibleForTesting;
  21. import org.apache.james.mime4j.codec.EncoderUtil;
  22. import org.apache.james.mime4j.decoder.DecoderUtil;
  23. import java.util.ArrayList;
  24. import java.util.regex.Pattern;
  25. /**
  26. * This class represent email address.
  27. *
  28. * RFC822 email address may have following format.
  29. * "name" <address> (comment)
  30. * "name" <address>
  31. * name <address>
  32. * address
  33. * Name and comment part should be MIME/base64 encoded in header if necessary.
  34. *
  35. */
  36. public class Address {
  37. /**
  38. * Address part, in the form local_part@domain_part. No surrounding angle brackets.
  39. */
  40. private String mAddress;
  41. /**
  42. * Name part. No surrounding double quote, and no MIME/base64 encoding.
  43. * This must be null if Address has no name part.
  44. */
  45. private String mPersonal;
  46. // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
  47. private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
  48. // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
  49. private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
  50. // Regex that matches escaped character '\\([\\"])'
  51. private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
  52. // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
  53. // TODO: Fix this to better constrain comments.
  54. /** Regex for the local part of an email address. */
  55. private static final String LOCAL_PART = "[^@]+";
  56. /** Regex for each part of the domain part, i.e. the thing between the dots. */
  57. private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
  58. /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
  59. private static final String DOMAIN_PART =
  60. "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
  61. /** Pattern to check if an email address is valid. */
  62. private static final Pattern EMAIL_ADDRESS =
  63. Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
  64. private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
  65. // delimiters are chars that do not appear in an email address, used by pack/unpack
  66. private static final char LIST_DELIMITER_EMAIL = '\1';
  67. private static final char LIST_DELIMITER_PERSONAL = '\2';
  68. public Address(String address, String personal) {
  69. setAddress(address);
  70. setPersonal(personal);
  71. }
  72. public Address(String address) {
  73. setAddress(address);
  74. }
  75. public String getAddress() {
  76. return mAddress;
  77. }
  78. public void setAddress(String address) {
  79. mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
  80. }
  81. /**
  82. * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
  83. *
  84. * @return Name part of email address. Returns null if it is omitted.
  85. */
  86. public String getPersonal() {
  87. return mPersonal;
  88. }
  89. /**
  90. * Set name part from UTF-16 string. Optional surrounding double quote will be removed.
  91. * It will be also unquoted and MIME/base64 decoded.
  92. *
  93. * @param personal name part of email address as UTF-16 string. Null is acceptable.
  94. */
  95. public void setPersonal(String personal) {
  96. if (personal != null) {
  97. personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
  98. personal = UNQUOTE.matcher(personal).replaceAll("$1");
  99. personal = DecoderUtil.decodeEncodedWords(personal);
  100. if (personal.length() == 0) {
  101. personal = null;
  102. }
  103. }
  104. mPersonal = personal;
  105. }
  106. /**
  107. * This method is used to check that all the addresses that the user
  108. * entered in a list (e.g. To:) are valid, so that none is dropped.
  109. */
  110. public static boolean isAllValid(String addressList) {
  111. // This code mimics the parse() method below.
  112. // I don't know how to better avoid the code-duplication.
  113. if (addressList != null && addressList.length() > 0) {
  114. Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
  115. for (int i = 0, length = tokens.length; i < length; ++i) {
  116. Rfc822Token token = tokens[i];
  117. String address = token.getAddress();
  118. if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
  119. return false;
  120. }
  121. }
  122. }
  123. return true;
  124. }
  125. /**
  126. * Parse a comma-delimited list of addresses in RFC822 format and return an
  127. * array of Address objects.
  128. *
  129. * @param addressList Address list in comma-delimited string.
  130. * @return An array of 0 or more Addresses.
  131. */
  132. public static Address[] parse(String addressList) {
  133. if (addressList == null || addressList.length() == 0) {
  134. return EMPTY_ADDRESS_ARRAY;
  135. }
  136. Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
  137. ArrayList<Address> addresses = new ArrayList<Address>();
  138. for (int i = 0, length = tokens.length; i < length; ++i) {
  139. Rfc822Token token = tokens[i];
  140. String address = token.getAddress();
  141. if (!TextUtils.isEmpty(address)) {
  142. if (isValidAddress(address)) {
  143. String name = token.getName();
  144. if (TextUtils.isEmpty(name)) {
  145. name = null;
  146. }
  147. addresses.add(new Address(address, name));
  148. }
  149. }
  150. }
  151. return addresses.toArray(new Address[] {});
  152. }
  153. /**
  154. * Checks whether a string email address is valid.
  155. * E.g. name@domain.com is valid.
  156. */
  157. @VisibleForTesting
  158. static boolean isValidAddress(final String address) {
  159. return EMAIL_ADDRESS.matcher(address).find();
  160. }
  161. @Override
  162. public boolean equals(Object o) {
  163. if (o instanceof Address) {
  164. // It seems that the spec says that the "user" part is case-sensitive,
  165. // while the domain part in case-insesitive.
  166. // So foo@yahoo.com and Foo@yahoo.com are different.
  167. // This may seem non-intuitive from the user POV, so we
  168. // may re-consider it if it creates UI trouble.
  169. // A problem case is "replyAll" sending to both
  170. // a@b.c and to A@b.c, which turn out to be the same on the server.
  171. // Leave unchanged for now (i.e. case-sensitive).
  172. return getAddress().equals(((Address) o).getAddress());
  173. }
  174. return super.equals(o);
  175. }
  176. @Override
  177. public int hashCode() {
  178. return getAddress().hashCode();
  179. }
  180. /**
  181. * Get human readable address string.
  182. * Do not use this for email header.
  183. *
  184. * @return Human readable address string. Not quoted and not encoded.
  185. */
  186. @Override
  187. public String toString() {
  188. if (mPersonal != null && !mPersonal.equals(mAddress)) {
  189. if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
  190. return quoteString(mPersonal) + " <" + mAddress + ">";
  191. } else {
  192. return mPersonal + " <" + mAddress + ">";
  193. }
  194. } else {
  195. return mAddress;
  196. }
  197. }
  198. /**
  199. * Ensures that the given string starts and ends with the double quote character. The string is
  200. * not modified in any way except to add the double quote character to start and end if it's not
  201. * already there.
  202. *
  203. * TODO: Rename this, because "quoteString()" can mean so many different things.
  204. *
  205. * sample -> "sample"
  206. * "sample" -> "sample"
  207. * ""sample"" -> "sample"
  208. * "sample"" -> "sample"
  209. * sa"mp"le -> "sa"mp"le"
  210. * "sa"mp"le" -> "sa"mp"le"
  211. * (empty string) -> ""
  212. * " -> ""
  213. */
  214. public static String quoteString(String s) {
  215. if (s == null) {
  216. return null;
  217. }
  218. if (!s.matches("^\".*\"$")) {
  219. return "\"" + s + "\"";
  220. }
  221. else {
  222. return s;
  223. }
  224. }
  225. /**
  226. * Get human readable comma-delimited address string.
  227. *
  228. * @param addresses Address array
  229. * @return Human readable comma-delimited address string.
  230. */
  231. public static String toString(Address[] addresses) {
  232. return toString(addresses, ",");
  233. }
  234. /**
  235. * Get human readable address strings joined with the specified separator.
  236. *
  237. * @param addresses Address array
  238. * @param separator Separator
  239. * @return Human readable comma-delimited address string.
  240. */
  241. public static String toString(Address[] addresses, String separator) {
  242. if (addresses == null || addresses.length == 0) {
  243. return null;
  244. }
  245. if (addresses.length == 1) {
  246. return addresses[0].toString();
  247. }
  248. StringBuffer sb = new StringBuffer(addresses[0].toString());
  249. for (int i = 1; i < addresses.length; i++) {
  250. sb.append(separator);
  251. // TODO: investigate why this .trim() is needed.
  252. sb.append(addresses[i].toString().trim());
  253. }
  254. return sb.toString();
  255. }
  256. /**
  257. * Get RFC822/MIME compatible address string.
  258. *
  259. * @return RFC822/MIME compatible address string.
  260. * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
  261. */
  262. public String toHeader() {
  263. if (mPersonal != null) {
  264. return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
  265. } else {
  266. return mAddress;
  267. }
  268. }
  269. /**
  270. * Get RFC822/MIME compatible comma-delimited address string.
  271. *
  272. * @param addresses Address array
  273. * @return RFC822/MIME compatible comma-delimited address string.
  274. * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
  275. */
  276. public static String toHeader(Address[] addresses) {
  277. if (addresses == null || addresses.length == 0) {
  278. return null;
  279. }
  280. if (addresses.length == 1) {
  281. return addresses[0].toHeader();
  282. }
  283. StringBuffer sb = new StringBuffer(addresses[0].toHeader());
  284. for (int i = 1; i < addresses.length; i++) {
  285. // We need space character to be able to fold line.
  286. sb.append(", ");
  287. sb.append(addresses[i].toHeader());
  288. }
  289. return sb.toString();
  290. }
  291. /**
  292. * Get Human friendly address string.
  293. *
  294. * @return the personal part of this Address, or the address part if the
  295. * personal part is not available
  296. */
  297. public String toFriendly() {
  298. if (mPersonal != null && mPersonal.length() > 0) {
  299. return mPersonal;
  300. } else {
  301. return mAddress;
  302. }
  303. }
  304. /**
  305. * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
  306. * details on the per-address conversion).
  307. *
  308. * @param addresses Array of Address[] values
  309. * @return A comma-delimited string listing all of the addresses supplied. Null if source
  310. * was null or empty.
  311. */
  312. public static String toFriendly(Address[] addresses) {
  313. if (addresses == null || addresses.length == 0) {
  314. return null;
  315. }
  316. if (addresses.length == 1) {
  317. return addresses[0].toFriendly();
  318. }
  319. StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
  320. for (int i = 1; i < addresses.length; i++) {
  321. sb.append(", ");
  322. sb.append(addresses[i].toFriendly());
  323. }
  324. return sb.toString();
  325. }
  326. /**
  327. * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
  328. */
  329. public static String unpackToString(String packedList) {
  330. return toString(unpack(packedList));
  331. }
  332. /**
  333. * Returns exactly the same result as Address.pack(Address.parse(textList)).
  334. */
  335. public static String parseAndPack(String textList) {
  336. return Address.pack(Address.parse(textList));
  337. }
  338. /**
  339. * Returns null if the packedList has 0 addresses, otherwise returns the first address.
  340. * The same as Address.unpack(packedList)[0] for non-empty list.
  341. * This is an utility method that offers some performance optimization opportunities.
  342. */
  343. public static Address unpackFirst(String packedList) {
  344. Address[] array = unpack(packedList);
  345. return array.length > 0 ? array[0] : null;
  346. }
  347. /**
  348. * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
  349. * This implementation is brute-force, and could be replaced with a more efficient version
  350. * if desired.
  351. */
  352. public static String packedToHeader(String packedList) {
  353. return toHeader(unpack(packedList));
  354. }
  355. /**
  356. * Unpacks an address list that is either CSV of RFC822 addresses OR (for backward
  357. * compatibility) previously packed with pack()
  358. * @param addressList string packed with pack() or CSV of RFC822 addresses
  359. * @return array of addresses resulting from unpack
  360. */
  361. public static Address[] unpack(String addressList) {
  362. if (addressList == null || addressList.length() == 0) {
  363. return EMPTY_ADDRESS_ARRAY;
  364. }
  365. // IF we're CSV, just parse
  366. if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
  367. (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
  368. return Address.parse(addressList);
  369. }
  370. // Otherwise, do backward-compatibile unpack
  371. ArrayList<Address> addresses = new ArrayList<Address>();
  372. int length = addressList.length();
  373. int pairStartIndex = 0;
  374. int pairEndIndex = 0;
  375. /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
  376. is used, not for every email address; i.e. not for every iteration of the while().
  377. This reduces the theoretical complexity from quadratic to linear,
  378. and provides some speed-up in practice by removing redundant scans of the string.
  379. */
  380. int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
  381. while (pairStartIndex < length) {
  382. pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
  383. if (pairEndIndex == -1) {
  384. pairEndIndex = length;
  385. }
  386. Address address;
  387. if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
  388. // in this case the DELIMITER_PERSONAL is in a future pair,
  389. // so don't use personal, and don't update addressEndIndex
  390. address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
  391. } else {
  392. address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
  393. addressList.substring(addressEndIndex + 1, pairEndIndex));
  394. // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
  395. addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
  396. }
  397. addresses.add(address);
  398. pairStartIndex = pairEndIndex + 1;
  399. }
  400. return addresses.toArray(EMPTY_ADDRESS_ARRAY);
  401. }
  402. /**
  403. * Generate a String containing RFC822 addresses separated by commas
  404. * NOTE: We used to "pack" these addresses in an app-specific format, but no longer do so
  405. */
  406. public static String pack(Address[] addresses) {
  407. return Address.toHeader(addresses);
  408. }
  409. /**
  410. * Produces the same result as pack(array), but only packs one (this) address.
  411. */
  412. public String pack() {
  413. final String address = getAddress();
  414. final String personal = getPersonal();
  415. if (personal == null) {
  416. return address;
  417. } else {
  418. return address + LIST_DELIMITER_PERSONAL + personal;
  419. }
  420. }
  421. }