1 /* 2 * Copyright 2020 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 17 package androidx.core.net; 18 19 import android.net.Uri; 20 21 import androidx.core.util.Preconditions; 22 23 import org.jspecify.annotations.NonNull; 24 import org.jspecify.annotations.Nullable; 25 26 import java.util.HashMap; 27 import java.util.Locale; 28 import java.util.Map; 29 30 /** 31 * MailTo URI parser. Replacement for {@link android.net.MailTo}. 32 * 33 * <p>This class parses a mailto scheme URI and then can be queried for the parsed parameters. 34 * This implements RFC 6068.</p> 35 * 36 * <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result, 37 * you should always ensure that you write your URI with the scheme using lower case letters, 38 * and normalize any URIs you receive from outside of Android to ensure the scheme is lower case. 39 * </em></p> 40 */ 41 public final class MailTo { 42 public static final String MAILTO_SCHEME = "mailto:"; 43 private static final String MAILTO = "mailto"; 44 45 // Well known headers 46 private static final String TO = "to"; 47 private static final String BODY = "body"; 48 private static final String CC = "cc"; 49 private static final String BCC = "bcc"; 50 private static final String SUBJECT = "subject"; 51 52 // All the parsed content is added to the headers. 53 private HashMap<String, String> mHeaders; 54 55 /** 56 * Private constructor. The only way to build a Mailto object is through 57 * the parse() method. 58 */ MailTo()59 private MailTo() { 60 mHeaders = new HashMap<>(); 61 } 62 63 /** 64 * Test to see if the given string is a mailto URI 65 * 66 * <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result, 67 * you should always ensure that you write your URI string with the scheme using lower case 68 * letters, and normalize any URIs you receive from outside of Android to ensure the scheme is 69 * lower case.</em></p> 70 * 71 * @param uri string to be tested 72 * @return true if the string is a mailto URI 73 */ isMailTo(@ullable String uri)74 public static boolean isMailTo(@Nullable String uri) { 75 return uri != null && uri.startsWith(MAILTO_SCHEME); 76 } 77 78 /** 79 * Test to see if the given Uri is a mailto URI 80 * 81 * <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result, 82 * you should always ensure that you write your Uri with the scheme using lower case letters, 83 * and normalize any Uris you receive from outside of Android to ensure the scheme is lower 84 * case.</em></p> 85 * 86 * @param uri Uri to be tested 87 * @return true if the Uri is a mailto URI 88 */ isMailTo(@ullable Uri uri)89 public static boolean isMailTo(@Nullable Uri uri) { 90 return uri != null && MAILTO.equals(uri.getScheme()); 91 } 92 93 /** 94 * Parse and decode a mailto scheme string. This parser implements 95 * RFC 6068. The returned object can be queried for the parsed parameters. 96 * 97 * <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result, 98 * you should always ensure that you write your URI string with the scheme using lower case 99 * letters, and normalize any URIs you receive from outside of Android to ensure the scheme is 100 * lower case.</em></p> 101 * 102 * @param uri String containing a mailto URI 103 * @return MailTo object 104 * @exception ParseException if the scheme is not a mailto URI 105 */ parse(@onNull String uri)106 public static @NonNull MailTo parse(@NonNull String uri) throws ParseException { 107 Preconditions.checkNotNull(uri); 108 109 if (!isMailTo(uri)) { 110 throw new ParseException("Not a mailto scheme"); 111 } 112 113 // Drop fragment if present 114 int fragmentIndex = uri.indexOf('#'); 115 if (fragmentIndex != -1) { 116 uri = uri.substring(0, fragmentIndex); 117 } 118 119 String address; 120 String query; 121 int queryIndex = uri.indexOf('?'); 122 if (queryIndex == -1) { 123 address = Uri.decode(uri.substring(MAILTO_SCHEME.length())); 124 query = null; 125 } else { 126 address = Uri.decode(uri.substring(MAILTO_SCHEME.length(), queryIndex)); 127 query = uri.substring(queryIndex + 1); 128 } 129 130 MailTo mailTo = new MailTo(); 131 132 // Parse out the query parameters 133 if (query != null) { 134 @SuppressWarnings("StringSplitter") 135 String[] queries = query.split("&"); 136 for (String queryParameter : queries) { 137 String[] nameValueArray = queryParameter.split("=", 2); 138 if (nameValueArray.length == 0) { 139 continue; 140 } 141 142 // insert the headers with the name in lowercase so that 143 // we can easily find common headers 144 String queryParameterKey = Uri.decode(nameValueArray[0]).toLowerCase(Locale.ROOT); 145 String queryParameterValue = nameValueArray.length > 1 146 ? Uri.decode(nameValueArray[1]) : null; 147 148 mailTo.mHeaders.put(queryParameterKey, queryParameterValue); 149 } 150 } 151 152 // Address can be specified in both the headers and just after the 153 // mailto line. Join the two together. 154 String toParameter = mailTo.getTo(); 155 if (toParameter != null) { 156 address += ", " + toParameter; 157 } 158 mailTo.mHeaders.put(TO, address); 159 160 return mailTo; 161 } 162 163 /** 164 * Parse and decode a mailto scheme Uri. This parser implements 165 * RFC 6068. The returned object can be queried for the parsed parameters. 166 * 167 * <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result, 168 * you should always ensure that you write your Uri with the scheme using lower case letters, 169 * and normalize any Uris you receive from outside of Android to ensure the scheme is lower 170 * case.</em></p> 171 * 172 * @param uri Uri containing a mailto URI 173 * @return MailTo object 174 * @exception ParseException if the scheme is not a mailto URI 175 */ parse(@onNull Uri uri)176 public static @NonNull MailTo parse(@NonNull Uri uri) throws ParseException { 177 return parse(uri.toString()); 178 } 179 180 /** 181 * Retrieve the To address line from the parsed mailto URI. This could be 182 * several email address that are comma-space delimited. 183 * If no To line was specified, then null is return 184 * @return comma delimited email addresses or null 185 */ getTo()186 public @Nullable String getTo() { 187 return mHeaders.get(TO); 188 } 189 190 /** 191 * Retrieve the CC address line from the parsed mailto URI. This could be 192 * several email address that are comma-space delimited. 193 * If no CC line was specified, then null is return 194 * @return comma delimited email addresses or null 195 */ getCc()196 public @Nullable String getCc() { 197 return mHeaders.get(CC); 198 } 199 200 /** 201 * Retrieve the BCC address line from the parsed mailto URI. This could be 202 * several email address that are comma-space delimited. 203 * If no BCC line was specified, then null is return 204 * @return comma delimited email addresses or null 205 */ getBcc()206 public @Nullable String getBcc() { 207 return mHeaders.get(BCC); 208 } 209 210 /** 211 * Retrieve the subject line from the parsed mailto URI. 212 * If no subject line was specified, then null is return 213 * @return subject or null 214 */ getSubject()215 public @Nullable String getSubject() { 216 return mHeaders.get(SUBJECT); 217 } 218 219 /** 220 * Retrieve the body line from the parsed mailto URI. 221 * If no body line was specified, then null is return 222 * @return body or null 223 */ getBody()224 public @Nullable String getBody() { 225 return mHeaders.get(BODY); 226 } 227 228 /** 229 * Retrieve all the parsed email headers from the mailto URI 230 * @return map containing all parsed values 231 */ getHeaders()232 public @Nullable Map<String, String> getHeaders() { 233 return mHeaders; 234 } 235 236 @Override toString()237 public @NonNull String toString() { 238 StringBuilder sb = new StringBuilder(MAILTO_SCHEME); 239 sb.append('?'); 240 for (Map.Entry<String, String> header : mHeaders.entrySet()) { 241 sb.append(Uri.encode(header.getKey())); 242 sb.append('='); 243 sb.append(Uri.encode(header.getValue())); 244 sb.append('&'); 245 } 246 return sb.toString(); 247 } 248 } 249