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