PageRenderTime 530ms CodeModel.GetById 42ms RepoModel.GetById 4ms app.codeStats 0ms

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

https://github.com/Savaged-Zen/platform_packages_apps_email
Java | 508 lines | 298 code | 39 blank | 171 comment | 104 complexity | 414cee9d386becf4f72e235e0b727690 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.email.mail;
  17. import com.android.email.Utility;
  18. import org.apache.james.mime4j.codec.EncoderUtil;
  19. import org.apache.james.mime4j.decoder.DecoderUtil;
  20. import android.text.TextUtils;
  21. import android.text.util.Rfc822Token;
  22. import android.text.util.Rfc822Tokenizer;
  23. import java.io.UnsupportedEncodingException;
  24. import java.net.URLEncoder;
  25. import java.util.ArrayList;
  26. import java.util.regex.Pattern;
  27. /**
  28. * This class represent email address.
  29. *
  30. * RFC822 email address may have following format.
  31. * "name" <address> (comment)
  32. * "name" <address>
  33. * name <address>
  34. * address
  35. * Name and comment part should be MIME/base64 encoded in header if necessary.
  36. *
  37. */
  38. public class Address {
  39. /**
  40. * Address part, in the form local_part@domain_part. No surrounding angle brackets.
  41. */
  42. private String mAddress;
  43. /**
  44. * Name part. No surrounding double quote, and no MIME/base64 encoding.
  45. * This must be null if Address has no name part.
  46. */
  47. private String mPersonal;
  48. // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
  49. private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
  50. // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
  51. private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
  52. // Regex that matches escaped character '\\([\\"])'
  53. private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
  54. private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
  55. // delimiters are chars that do not appear in an email address, used by pack/unpack
  56. private static final char LIST_DELIMITER_EMAIL = '\1';
  57. private static final char LIST_DELIMITER_PERSONAL = '\2';
  58. public Address(String address, String personal) {
  59. setAddress(address);
  60. setPersonal(personal);
  61. }
  62. public Address(String address) {
  63. setAddress(address);
  64. }
  65. public String getAddress() {
  66. return mAddress;
  67. }
  68. public void setAddress(String address) {
  69. this.mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");;
  70. }
  71. /**
  72. * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
  73. *
  74. * @return Name part of email address. Returns null if it is omitted.
  75. */
  76. public String getPersonal() {
  77. return mPersonal;
  78. }
  79. /**
  80. * Set name part from UTF-16 string. Optional surrounding double quote will be removed.
  81. * It will be also unquoted and MIME/base64 decoded.
  82. *
  83. * @param Personal name part of email address as UTF-16 string. Null is acceptable.
  84. */
  85. public void setPersonal(String personal) {
  86. if (personal != null) {
  87. personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
  88. personal = UNQUOTE.matcher(personal).replaceAll("$1");
  89. personal = DecoderUtil.decodeEncodedWords(personal);
  90. if (personal.length() == 0) {
  91. personal = null;
  92. }
  93. }
  94. this.mPersonal = personal;
  95. }
  96. /**
  97. * This method is used to check that all the addresses that the user
  98. * entered in a list (e.g. To:) are valid, so that none is dropped.
  99. */
  100. public static boolean isAllValid(String addressList) {
  101. // This code mimics the parse() method below.
  102. // I don't know how to better avoid the code-duplication.
  103. if (addressList != null && addressList.length() > 0) {
  104. Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
  105. for (int i = 0, length = tokens.length; i < length; ++i) {
  106. Rfc822Token token = tokens[i];
  107. String address = token.getAddress();
  108. if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
  109. return false;
  110. }
  111. }
  112. }
  113. return true;
  114. }
  115. /**
  116. * Parse a comma-delimited list of addresses in RFC822 format and return an
  117. * array of Address objects.
  118. *
  119. * @param addressList Address list in comma-delimited string.
  120. * @return An array of 0 or more Addresses.
  121. */
  122. public static Address[] parse(String addressList) {
  123. if (addressList == null || addressList.length() == 0) {
  124. return EMPTY_ADDRESS_ARRAY;
  125. }
  126. Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
  127. ArrayList<Address> addresses = new ArrayList<Address>();
  128. for (int i = 0, length = tokens.length; i < length; ++i) {
  129. Rfc822Token token = tokens[i];
  130. String address = token.getAddress();
  131. if (!TextUtils.isEmpty(address)) {
  132. if (isValidAddress(address)) {
  133. String name = token.getName();
  134. if (TextUtils.isEmpty(name)) {
  135. name = null;
  136. }
  137. addresses.add(new Address(address, name));
  138. }
  139. }
  140. }
  141. return addresses.toArray(new Address[] {});
  142. }
  143. /**
  144. * Checks whether a string email address is valid.
  145. * E.g. name@domain.com is valid.
  146. */
  147. /* package */ static boolean isValidAddress(String address) {
  148. // Note: Some email provider may violate the standard, so here we only check that
  149. // address consists of two part that are separated by '@', and domain part contains
  150. // at least one '.'.
  151. int len = address.length();
  152. int firstAt = address.indexOf('@');
  153. int lastAt = address.lastIndexOf('@');
  154. int firstDot = address.indexOf('.', lastAt + 1);
  155. int lastDot = address.lastIndexOf('.');
  156. return firstAt > 0 && firstAt == lastAt && lastAt + 1 < firstDot
  157. && firstDot <= lastDot && lastDot < len - 1;
  158. }
  159. @Override
  160. public boolean equals(Object o) {
  161. if (o instanceof Address) {
  162. // It seems that the spec says that the "user" part is case-sensitive,
  163. // while the domain part in case-insesitive.
  164. // So foo@yahoo.com and Foo@yahoo.com are different.
  165. // This may seem non-intuitive from the user POV, so we
  166. // may re-consider it if it creates UI trouble.
  167. // A problem case is "replyAll" sending to both
  168. // a@b.c and to A@b.c, which turn out to be the same on the server.
  169. // Leave unchanged for now (i.e. case-sensitive).
  170. return getAddress().equals(((Address) o).getAddress());
  171. }
  172. return super.equals(o);
  173. }
  174. /**
  175. * Get human readable address string.
  176. * Do not use this for email header.
  177. *
  178. * @return Human readable address string. Not quoted and not encoded.
  179. */
  180. @Override
  181. public String toString() {
  182. if (mPersonal != null) {
  183. if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
  184. return Utility.quoteString(mPersonal) + " <" + mAddress + ">";
  185. } else {
  186. return mPersonal + " <" + mAddress + ">";
  187. }
  188. } else {
  189. return mAddress;
  190. }
  191. }
  192. /**
  193. * Get human readable comma-delimited address string.
  194. *
  195. * @param addresses Address array
  196. * @return Human readable comma-delimited address string.
  197. */
  198. public static String toString(Address[] addresses) {
  199. if (addresses == null || addresses.length == 0) {
  200. return null;
  201. }
  202. if (addresses.length == 1) {
  203. return addresses[0].toString();
  204. }
  205. StringBuffer sb = new StringBuffer(addresses[0].toString());
  206. for (int i = 1; i < addresses.length; i++) {
  207. sb.append(',');
  208. sb.append(addresses[i].toString());
  209. }
  210. return sb.toString();
  211. }
  212. /**
  213. * Get RFC822/MIME compatible address string.
  214. *
  215. * @return RFC822/MIME compatible address string.
  216. * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
  217. */
  218. public String toHeader() {
  219. if (mPersonal != null) {
  220. return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
  221. } else {
  222. return mAddress;
  223. }
  224. }
  225. /**
  226. * Get RFC822/MIME compatible comma-delimited address string.
  227. *
  228. * @param addresses Address array
  229. * @return RFC822/MIME compatible comma-delimited address string.
  230. * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
  231. */
  232. public static String toHeader(Address[] addresses) {
  233. if (addresses == null || addresses.length == 0) {
  234. return null;
  235. }
  236. if (addresses.length == 1) {
  237. return addresses[0].toHeader();
  238. }
  239. StringBuffer sb = new StringBuffer(addresses[0].toHeader());
  240. for (int i = 1; i < addresses.length; i++) {
  241. // We need space character to be able to fold line.
  242. sb.append(", ");
  243. sb.append(addresses[i].toHeader());
  244. }
  245. return sb.toString();
  246. }
  247. /**
  248. * Get Human friendly address string.
  249. *
  250. * @return the personal part of this Address, or the address part if the
  251. * personal part is not available
  252. */
  253. public String toFriendly() {
  254. if (mPersonal != null && mPersonal.length() > 0) {
  255. return mPersonal;
  256. } else {
  257. return mAddress;
  258. }
  259. }
  260. /**
  261. * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
  262. * details on the per-address conversion).
  263. *
  264. * @param addresses Array of Address[] values
  265. * @return A comma-delimited string listing all of the addresses supplied. Null if source
  266. * was null or empty.
  267. */
  268. public static String toFriendly(Address[] addresses) {
  269. if (addresses == null || addresses.length == 0) {
  270. return null;
  271. }
  272. if (addresses.length == 1) {
  273. return addresses[0].toFriendly();
  274. }
  275. StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
  276. for (int i = 1; i < addresses.length; i++) {
  277. sb.append(',');
  278. sb.append(addresses[i].toFriendly());
  279. }
  280. return sb.toString();
  281. }
  282. /**
  283. * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
  284. */
  285. public static String unpackToString(String packedList) {
  286. return toString(unpack(packedList));
  287. }
  288. /**
  289. * Returns exactly the same result as Address.pack(Address.parse(textList)).
  290. */
  291. public static String parseAndPack(String textList) {
  292. return Address.pack(Address.parse(textList));
  293. }
  294. /**
  295. * Returns null if the packedList has 0 addresses, otherwise returns the first address.
  296. * The same as Address.unpack(packedList)[0] for non-empty list.
  297. * This is an utility method that offers some performance optimization opportunities.
  298. */
  299. public static Address unpackFirst(String packedList) {
  300. Address[] array = unpack(packedList);
  301. return array.length > 0 ? array[0] : null;
  302. }
  303. /**
  304. * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
  305. * This implementation is brute-force, and could be replaced with a more efficient version
  306. * if desired.
  307. */
  308. public static String packedToHeader(String packedList) {
  309. return toHeader(unpack(packedList));
  310. }
  311. /**
  312. * Unpacks an address list previously packed with pack()
  313. * @param addressList String with packed addresses as returned by pack()
  314. * @return array of addresses resulting from unpack
  315. */
  316. public static Address[] unpack(String addressList) {
  317. if (addressList == null || addressList.length() == 0) {
  318. return EMPTY_ADDRESS_ARRAY;
  319. }
  320. ArrayList<Address> addresses = new ArrayList<Address>();
  321. int length = addressList.length();
  322. int pairStartIndex = 0;
  323. int pairEndIndex = 0;
  324. /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
  325. is used, not for every email address; i.e. not for every iteration of the while().
  326. This reduces the theoretical complexity from quadratic to linear,
  327. and provides some speed-up in practice by removing redundant scans of the string.
  328. */
  329. int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
  330. while (pairStartIndex < length) {
  331. pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
  332. if (pairEndIndex == -1) {
  333. pairEndIndex = length;
  334. }
  335. Address address;
  336. if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
  337. // in this case the DELIMITER_PERSONAL is in a future pair,
  338. // so don't use personal, and don't update addressEndIndex
  339. address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
  340. } else {
  341. address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
  342. addressList.substring(addressEndIndex + 1, pairEndIndex));
  343. // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
  344. addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
  345. }
  346. addresses.add(address);
  347. pairStartIndex = pairEndIndex + 1;
  348. }
  349. return addresses.toArray(EMPTY_ADDRESS_ARRAY);
  350. }
  351. /**
  352. * Packs an address list into a String that is very quick to read
  353. * and parse. Packed lists can be unpacked with unpack().
  354. * The format is a series of packed addresses separated by LIST_DELIMITER_EMAIL.
  355. * Each address is packed as
  356. * a pair of address and personal separated by LIST_DELIMITER_PERSONAL,
  357. * where the personal and delimiter are optional.
  358. * E.g. "foo@x.com\1joe@x.com\2Joe Doe"
  359. * @param addresses Array of addresses
  360. * @return a string containing the packed addresses.
  361. */
  362. public static String pack(Address[] addresses) {
  363. // TODO: return same value for both null & empty list
  364. if (addresses == null) {
  365. return null;
  366. }
  367. final int nAddr = addresses.length;
  368. if (nAddr == 0) {
  369. return "";
  370. }
  371. // shortcut: one email with no displayName
  372. if (nAddr == 1 && addresses[0].getPersonal() == null) {
  373. return addresses[0].getAddress();
  374. }
  375. StringBuffer sb = new StringBuffer();
  376. for (int i = 0; i < nAddr; i++) {
  377. if (i != 0) {
  378. sb.append(LIST_DELIMITER_EMAIL);
  379. }
  380. final Address address = addresses[i];
  381. sb.append(address.getAddress());
  382. final String displayName = address.getPersonal();
  383. if (displayName != null) {
  384. sb.append(LIST_DELIMITER_PERSONAL);
  385. sb.append(displayName);
  386. }
  387. }
  388. return sb.toString();
  389. }
  390. /**
  391. * Produces the same result as pack(array), but only packs one (this) address.
  392. */
  393. public String pack() {
  394. final String address = getAddress();
  395. final String personal = getPersonal();
  396. if (personal == null) {
  397. return address;
  398. } else {
  399. return address + LIST_DELIMITER_PERSONAL + personal;
  400. }
  401. }
  402. /**
  403. * Legacy unpack() used for reading the old data (migration),
  404. * as found in LocalStore (Donut; db version up to 24).
  405. * @See unpack()
  406. */
  407. public static Address[] legacyUnpack(String addressList) {
  408. if (addressList == null || addressList.length() == 0) {
  409. return new Address[] { };
  410. }
  411. ArrayList<Address> addresses = new ArrayList<Address>();
  412. int length = addressList.length();
  413. int pairStartIndex = 0;
  414. int pairEndIndex = 0;
  415. int addressEndIndex = 0;
  416. while (pairStartIndex < length) {
  417. pairEndIndex = addressList.indexOf(',', pairStartIndex);
  418. if (pairEndIndex == -1) {
  419. pairEndIndex = length;
  420. }
  421. addressEndIndex = addressList.indexOf(';', pairStartIndex);
  422. String address = null;
  423. String personal = null;
  424. if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) {
  425. address =
  426. Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex));
  427. }
  428. else {
  429. address =
  430. Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex));
  431. personal =
  432. Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex));
  433. }
  434. addresses.add(new Address(address, personal));
  435. pairStartIndex = pairEndIndex + 1;
  436. }
  437. return addresses.toArray(new Address[] { });
  438. }
  439. /**
  440. * Legacy pack() used for writing to old data (migration),
  441. * as found in LocalStore (Donut; db version up to 24).
  442. * @See unpack()
  443. */
  444. public static String legacyPack(Address[] addresses) {
  445. if (addresses == null) {
  446. return null;
  447. } else if (addresses.length == 0) {
  448. return "";
  449. }
  450. StringBuffer sb = new StringBuffer();
  451. for (int i = 0, count = addresses.length; i < count; i++) {
  452. Address address = addresses[i];
  453. try {
  454. sb.append(URLEncoder.encode(address.getAddress(), "UTF-8"));
  455. if (address.getPersonal() != null) {
  456. sb.append(';');
  457. sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8"));
  458. }
  459. if (i < count - 1) {
  460. sb.append(',');
  461. }
  462. }
  463. catch (UnsupportedEncodingException uee) {
  464. return null;
  465. }
  466. }
  467. return sb.toString();
  468. }
  469. }