MailTo.java
/*
* Copyright 2020 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 androidx.core.net;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Preconditions;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* MailTo URI parser. Replacement for {@link android.net.MailTo}.
*
* <p>This class parses a mailto scheme URI and then can be queried for the parsed parameters.
* This implements RFC 6068.</p>
*
* <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result,
* you should always ensure that you write your URI with the scheme using lower case letters,
* and normalize any URIs you receive from outside of Android to ensure the scheme is lower case.
* </em></p>
*/
public final class MailTo {
public static final String MAILTO_SCHEME = "mailto:";
private static final String MAILTO = "mailto";
// Well known headers
private static final String TO = "to";
private static final String BODY = "body";
private static final String CC = "cc";
private static final String BCC = "bcc";
private static final String SUBJECT = "subject";
// All the parsed content is added to the headers.
private HashMap<String, String> mHeaders;
/**
* Private constructor. The only way to build a Mailto object is through
* the parse() method.
*/
private MailTo() {
mHeaders = new HashMap<>();
}
/**
* Test to see if the given string is a mailto URI
*
* <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result,
* you should always ensure that you write your URI string with the scheme using lower case
* letters, and normalize any URIs you receive from outside of Android to ensure the scheme is
* lower case.</em></p>
*
* @param uri string to be tested
* @return true if the string is a mailto URI
*/
public static boolean isMailTo(@Nullable String uri) {
return uri != null && uri.startsWith(MAILTO_SCHEME);
}
/**
* Test to see if the given Uri is a mailto URI
*
* <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result,
* you should always ensure that you write your Uri with the scheme using lower case letters,
* and normalize any Uris you receive from outside of Android to ensure the scheme is lower
* case.</em></p>
*
* @param uri Uri to be tested
* @return true if the Uri is a mailto URI
*/
public static boolean isMailTo(@Nullable Uri uri) {
return uri != null && MAILTO.equals(uri.getScheme());
}
/**
* Parse and decode a mailto scheme string. This parser implements
* RFC 6068. The returned object can be queried for the parsed parameters.
*
* <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result,
* you should always ensure that you write your URI string with the scheme using lower case
* letters, and normalize any URIs you receive from outside of Android to ensure the scheme is
* lower case.</em></p>
*
* @param uri String containing a mailto URI
* @return MailTo object
* @exception ParseException if the scheme is not a mailto URI
*/
@NonNull
public static MailTo parse(@NonNull String uri) throws ParseException {
Preconditions.checkNotNull(uri);
if (!isMailTo(uri)) {
throw new ParseException("Not a mailto scheme");
}
// Drop fragment if present
int fragmentIndex = uri.indexOf('#');
if (fragmentIndex != -1) {
uri = uri.substring(0, fragmentIndex);
}
String address;
String query;
int queryIndex = uri.indexOf('?');
if (queryIndex == -1) {
address = Uri.decode(uri.substring(MAILTO_SCHEME.length()));
query = null;
} else {
address = Uri.decode(uri.substring(MAILTO_SCHEME.length(), queryIndex));
query = uri.substring(queryIndex + 1);
}
MailTo mailTo = new MailTo();
// Parse out the query parameters
if (query != null) {
@SuppressWarnings("StringSplitter")
String[] queries = query.split("&");
for (String queryParameter : queries) {
String[] nameValueArray = queryParameter.split("=", 2);
if (nameValueArray.length == 0) {
continue;
}
// insert the headers with the name in lowercase so that
// we can easily find common headers
String queryParameterKey = Uri.decode(nameValueArray[0]).toLowerCase(Locale.ROOT);
String queryParameterValue = nameValueArray.length > 1
? Uri.decode(nameValueArray[1]) : null;
mailTo.mHeaders.put(queryParameterKey, queryParameterValue);
}
}
// Address can be specified in both the headers and just after the
// mailto line. Join the two together.
String toParameter = mailTo.getTo();
if (toParameter != null) {
address += ", " + toParameter;
}
mailTo.mHeaders.put(TO, address);
return mailTo;
}
/**
* Parse and decode a mailto scheme Uri. This parser implements
* RFC 6068. The returned object can be queried for the parsed parameters.
*
* <p><em>Note: scheme name matching is case-sensitive, unlike the formal RFC. As a result,
* you should always ensure that you write your Uri with the scheme using lower case letters,
* and normalize any Uris you receive from outside of Android to ensure the scheme is lower
* case.</em></p>
*
* @param uri Uri containing a mailto URI
* @return MailTo object
* @exception ParseException if the scheme is not a mailto URI
*/
@NonNull
public static MailTo parse(@NonNull Uri uri) throws ParseException {
return parse(uri.toString());
}
/**
* Retrieve the To address line from the parsed mailto URI. This could be
* several email address that are comma-space delimited.
* If no To line was specified, then null is return
* @return comma delimited email addresses or null
*/
@Nullable
public String getTo() {
return mHeaders.get(TO);
}
/**
* Retrieve the CC address line from the parsed mailto URI. This could be
* several email address that are comma-space delimited.
* If no CC line was specified, then null is return
* @return comma delimited email addresses or null
*/
@Nullable
public String getCc() {
return mHeaders.get(CC);
}
/**
* Retrieve the BCC address line from the parsed mailto URI. This could be
* several email address that are comma-space delimited.
* If no BCC line was specified, then null is return
* @return comma delimited email addresses or null
*/
@Nullable
public String getBcc() {
return mHeaders.get(BCC);
}
/**
* Retrieve the subject line from the parsed mailto URI.
* If no subject line was specified, then null is return
* @return subject or null
*/
@Nullable
public String getSubject() {
return mHeaders.get(SUBJECT);
}
/**
* Retrieve the body line from the parsed mailto URI.
* If no body line was specified, then null is return
* @return body or null
*/
@Nullable
public String getBody() {
return mHeaders.get(BODY);
}
/**
* Retrieve all the parsed email headers from the mailto URI
* @return map containing all parsed values
*/
@Nullable
public Map<String, String> getHeaders() {
return mHeaders;
}
@NonNull
@Override
public String toString() {
StringBuilder sb = new StringBuilder(MAILTO_SCHEME);
sb.append('?');
for (Map.Entry<String, String> header : mHeaders.entrySet()) {
sb.append(Uri.encode(header.getKey()));
sb.append('=');
sb.append(Uri.encode(header.getValue()));
sb.append('&');
}
return sb.toString();
}
}