mirror of
https://codeberg.org/PGPainless/wkd-java.git
synced 2024-11-22 15:22:05 +01:00
Add javadoc and move http-independent WKDFetcher logic to AbstractWKDFetchet class.
This commit is contained in:
parent
9abe217de0
commit
5613d42baf
8 changed files with 218 additions and 62 deletions
|
@ -4,6 +4,10 @@
|
||||||
|
|
||||||
package pgp.wkd.cli;
|
package pgp.wkd.cli;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception that gets thrown when an OpenPGP certificate is not carrying a User-ID binding for the email address
|
||||||
|
* that was used to look the certificate up via WKD.
|
||||||
|
*/
|
||||||
public class MissingUserIdException extends Exception {
|
public class MissingUserIdException extends Exception {
|
||||||
|
|
||||||
public MissingUserIdException() {
|
public MissingUserIdException() {
|
||||||
|
|
|
@ -8,8 +8,8 @@ import org.bouncycastle.bcpg.ArmoredOutputStream;
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||||
import org.pgpainless.PGPainless;
|
import org.pgpainless.PGPainless;
|
||||||
import org.pgpainless.key.info.KeyRingInfo;
|
import org.pgpainless.key.info.KeyRingInfo;
|
||||||
import pgp.wkd.IWKDFetcher;
|
import pgp.wkd.AbstractWKDFetcher;
|
||||||
import pgp.wkd.JavaHttpRequestWKDFetcher;
|
import pgp.wkd.HttpUrlConnectionWKDFetcher;
|
||||||
import pgp.wkd.WKDAddress;
|
import pgp.wkd.WKDAddress;
|
||||||
import pgp.wkd.WKDAddressHelper;
|
import pgp.wkd.WKDAddressHelper;
|
||||||
import pgp.wkd.cli.MissingUserIdException;
|
import pgp.wkd.cli.MissingUserIdException;
|
||||||
|
@ -40,7 +40,7 @@ public class Fetch implements Runnable {
|
||||||
)
|
)
|
||||||
boolean armor = false;
|
boolean armor = false;
|
||||||
|
|
||||||
IWKDFetcher fetcher = new JavaHttpRequestWKDFetcher();
|
AbstractWKDFetcher fetcher = new HttpUrlConnectionWKDFetcher();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
|
@ -6,4 +6,4 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
# WKD-Java
|
# WKD-Java
|
||||||
|
|
||||||
API Implementation of the Web Key Directory Specification for Java.
|
API Implementation of the Key Discovery part of the Web Key Directory Specification for Java.
|
||||||
|
|
|
@ -4,26 +4,34 @@
|
||||||
|
|
||||||
package pgp.wkd;
|
package pgp.wkd;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.ConnectException;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class JavaHttpRequestWKDFetcher implements IWKDFetcher {
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(JavaHttpRequestWKDFetcher.class);
|
/**
|
||||||
|
* Abstract class for fetching OpenPGP certificates from the WKD.
|
||||||
|
* This class can be extended to fetch files from remote servers using different HTTP clients.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractWKDFetcher {
|
||||||
|
|
||||||
@Override
|
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractWKDFetcher.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to fetch an OpenPGP certificate from the Web Key Directory.
|
||||||
|
*
|
||||||
|
* @param address WKDAddress object
|
||||||
|
* @return input stream containing the certificate in its binary representation
|
||||||
|
*
|
||||||
|
* @throws IOException in case of an error
|
||||||
|
*/
|
||||||
public InputStream fetch(WKDAddress address) throws IOException {
|
public InputStream fetch(WKDAddress address) throws IOException {
|
||||||
URI advanced = address.getAdvancedMethodURI();
|
URI advanced = address.getAdvancedMethodURI();
|
||||||
IOException advancedException;
|
IOException advancedException;
|
||||||
try {
|
try {
|
||||||
return tryFetchUri(advanced);
|
return fetchUri(advanced);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
advancedException = e;
|
advancedException = e;
|
||||||
LOGGER.debug("Could not fetch key using advanced method from " + advanced.toString(), advancedException);
|
LOGGER.debug("Could not fetch key using advanced method from " + advanced.toString(), advancedException);
|
||||||
|
@ -31,32 +39,22 @@ public class JavaHttpRequestWKDFetcher implements IWKDFetcher {
|
||||||
|
|
||||||
URI direct = address.getDirectMethodURI();
|
URI direct = address.getDirectMethodURI();
|
||||||
try {
|
try {
|
||||||
return tryFetchUri(direct);
|
return fetchUri(direct);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// we would like to use addSuppressed eventually, but Android API 10 does no support it
|
// we would like to use addSuppressed eventually, but Android API 10 does not support it
|
||||||
// e.addSuppressed(advancedException);
|
// e.addSuppressed(advancedException);
|
||||||
LOGGER.debug("Could not fetch key using direct method from " + direct.toString(), e);
|
LOGGER.debug("Could not fetch key using direct method from " + direct.toString(), e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private InputStream tryFetchUri(URI uri) throws IOException {
|
/**
|
||||||
HttpURLConnection con = getConnection(uri);
|
* Fetch the contents of the file that the {@link URI} points to from the remote server.
|
||||||
con.setRequestMethod("GET");
|
* @param uri uri
|
||||||
|
* @return file contents
|
||||||
con.setConnectTimeout(5000);
|
*
|
||||||
con.setReadTimeout(5000);
|
* @throws java.net.ConnectException in case the file or host does not exist
|
||||||
con.setInstanceFollowRedirects(false);
|
* @throws IOException in case of an IO-error
|
||||||
|
*/
|
||||||
int status = con.getResponseCode();
|
protected abstract InputStream fetchUri(URI uri) throws IOException;
|
||||||
if (status != 200) {
|
|
||||||
throw new ConnectException("Connecting to '" + uri + "' failed. Status: " + status);
|
|
||||||
}
|
|
||||||
return con.getInputStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpURLConnection getConnection(URI uri) throws IOException {
|
|
||||||
URL url = uri.toURL();
|
|
||||||
return (HttpURLConnection) url.openConnection();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package pgp.wkd;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.ConnectException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link AbstractWKDFetcher} using Java's {@link HttpURLConnection}.
|
||||||
|
*/
|
||||||
|
public class HttpUrlConnectionWKDFetcher extends AbstractWKDFetcher {
|
||||||
|
|
||||||
|
public InputStream fetchUri(URI uri) throws IOException {
|
||||||
|
URL url = uri.toURL();
|
||||||
|
HttpURLConnection con = (HttpURLConnection) url.openConnection();
|
||||||
|
con.setRequestMethod("GET");
|
||||||
|
|
||||||
|
con.setConnectTimeout(5000);
|
||||||
|
con.setReadTimeout(5000);
|
||||||
|
con.setInstanceFollowRedirects(false);
|
||||||
|
|
||||||
|
int status = con.getResponseCode();
|
||||||
|
if (status != 200) {
|
||||||
|
throw new ConnectException("Connecting to '" + uri + "' failed. Status: " + status);
|
||||||
|
}
|
||||||
|
return con.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package pgp.wkd;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public interface IWKDFetcher {
|
|
||||||
|
|
||||||
InputStream fetch(WKDAddress address) throws IOException;
|
|
||||||
}
|
|
|
@ -15,7 +15,13 @@ import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class WKDAddress {
|
/**
|
||||||
|
* Create {@link URI URIs} for discovery of certificates in the OpenPGP Web Key Directory.
|
||||||
|
*
|
||||||
|
* @see <a href="https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-13.html#name-key-discovery">
|
||||||
|
* OpenPGP Web Key Directory - §3.1. Key Discovery</a>
|
||||||
|
*/
|
||||||
|
public final class WKDAddress {
|
||||||
|
|
||||||
private static final String SCHEME = "https://";
|
private static final String SCHEME = "https://";
|
||||||
private static final String ADV_SUBDOMAIN = "openpgpkey.";
|
private static final String ADV_SUBDOMAIN = "openpgpkey.";
|
||||||
|
@ -24,12 +30,18 @@ public class WKDAddress {
|
||||||
return "/.well-known/openpgpkey/" + domain + "/hu/";
|
return "/.well-known/openpgpkey/" + domain + "/hu/";
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegEx for Email Addresses.
|
// RegExs for Email Addresses.
|
||||||
// https://www.baeldung.com/java-email-validation-regex#regular-expression-by-rfc-5322-for-email-validation
|
// https://www.baeldung.com/java-email-validation-regex#regular-expression-by-rfc-5322-for-email-validation
|
||||||
// Modified by adding capture groups '()' for local and domain part
|
// Modified by adding capture groups '()' for local and domain part
|
||||||
private static final Pattern PATTERN_EMAIL = Pattern.compile("^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+)@([a-zA-Z0-9.-]+)$");
|
private static final Pattern PATTERN_EMAIL = Pattern.compile("^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+)@([a-zA-Z0-9.-]+)$");
|
||||||
|
// Validate just the local part
|
||||||
|
private static final Pattern PATTERN_LOCAL_PART = Pattern.compile("^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+$");
|
||||||
|
// Validate just the domain part
|
||||||
|
private static final Pattern PATTERN_DOMAIN_PART = Pattern.compile("[a-zA-Z0-9.-]+$");
|
||||||
|
|
||||||
|
// Android API lvl 10 does not yet know StandardCharsets.UTF_8 :/
|
||||||
private static final Charset utf8 = Charset.forName("UTF8");
|
private static final Charset utf8 = Charset.forName("UTF8");
|
||||||
|
// Z-Base32 encoding is described in https://www.rfc-editor.org/rfc/rfc6189.html#section-5.1.6
|
||||||
private static final ZBase32 zBase32 = new ZBase32();
|
private static final ZBase32 zBase32 = new ZBase32();
|
||||||
|
|
||||||
private final String localPart;
|
private final String localPart;
|
||||||
|
@ -37,52 +49,125 @@ public class WKDAddress {
|
||||||
private final String zbase32LocalPart;
|
private final String zbase32LocalPart;
|
||||||
private final String percentEncodedLocalPart;
|
private final String percentEncodedLocalPart;
|
||||||
|
|
||||||
public WKDAddress(String localPart, String domainPart) {
|
/**
|
||||||
|
* Construct a {@link WKDAddress} from an email address' local part and domain part.
|
||||||
|
*
|
||||||
|
* @param localPart local part of the email address, case-sensitive
|
||||||
|
* @param domainPart domain part of the email address, case-insensitive
|
||||||
|
*/
|
||||||
|
private WKDAddress(String localPart, String domainPart) {
|
||||||
this.localPart = localPart;
|
this.localPart = localPart;
|
||||||
this.domainPart = domainPart.toLowerCase();
|
this.domainPart = domainPart.toLowerCase();
|
||||||
|
|
||||||
this.zbase32LocalPart = zbase32(this.localPart);
|
this.zbase32LocalPart = sha1AndZBase32Encode(this.localPart);
|
||||||
this.percentEncodedLocalPart = percentEncode(this.localPart);
|
this.percentEncodedLocalPart = percentEncode(this.localPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link WKDAddress} from an email address' local part and domain part.
|
||||||
|
*
|
||||||
|
* @param localPart local part of the email address, case-sensitive
|
||||||
|
* @param domainPart domain part of the email address, case-insensitive
|
||||||
|
*
|
||||||
|
* @return WKD address
|
||||||
|
*/
|
||||||
|
public static WKDAddress fromLocalAndDomainPart(String localPart, String domainPart) {
|
||||||
|
if (!PATTERN_LOCAL_PART.matcher(localPart).matches()) {
|
||||||
|
throw new IllegalArgumentException("Invalid local part.");
|
||||||
|
}
|
||||||
|
if (!PATTERN_DOMAIN_PART.matcher(domainPart).matches()) {
|
||||||
|
throw new IllegalArgumentException("Invalid domain part.");
|
||||||
|
}
|
||||||
|
return new WKDAddress(localPart, domainPart);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform an email address into a {@link WKDAddress} from which lookup {@link URI URIs} can be generated.
|
||||||
|
*
|
||||||
|
* @param email email address, case sensitive
|
||||||
|
* @return WKDAddress object
|
||||||
|
*/
|
||||||
public static WKDAddress fromEmail(String email) {
|
public static WKDAddress fromEmail(String email) {
|
||||||
MailAddress mailAddress = parseMailAddress(email);
|
MailAddress mailAddress = parseMailAddress(email);
|
||||||
return new WKDAddress(mailAddress.getLocalPart(), mailAddress.getDomainPart());
|
return new WKDAddress(mailAddress.getLocalPart(), mailAddress.getDomainPart());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an {@link URI} pointing to the certificate using the direct lookup method.
|
||||||
|
* The direct method requires that a WKD is available on the same domain as the users mail server.
|
||||||
|
*
|
||||||
|
* Example URI (direct format) for email "Joe.Doe@Example.ORG":
|
||||||
|
* <pre>https://example.org/.well-known/openpgpkey/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe</pre>
|
||||||
|
*
|
||||||
|
* @see <a href="https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-13.html#section-3.1-10">
|
||||||
|
* OpenPGP Web Key Directory: §3.1. Key Discovery - Direct Method</a>
|
||||||
|
*
|
||||||
|
* @return URI using the direct lookup method
|
||||||
|
*/
|
||||||
public URI getDirectMethodURI() {
|
public URI getDirectMethodURI() {
|
||||||
return URI.create(SCHEME + domainPart + DIRECT_WELL_KNOWN + zbase32LocalPart + "?l=" + percentEncodedLocalPart);
|
return URI.create(SCHEME + domainPart + DIRECT_WELL_KNOWN + zbase32LocalPart + "?l=" + percentEncodedLocalPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an {@link URI} pointing to the certificate using the advanced lookup method.
|
||||||
|
* The advanced method requires that a WKD is available on a special subdomain "openpgpkey" on the users mail server.
|
||||||
|
*
|
||||||
|
* Example URI (advanced format) for email "Joe.Doe@Example.ORG":
|
||||||
|
* <pre>https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe</pre>
|
||||||
|
*
|
||||||
|
* @see <a href="https://www.ietf.org/archive/id/draft-koch-openpgp-webkey-service-13.html#section-3.1-5">
|
||||||
|
* OpenPGP Web Key Directory: §3.1. Key Discovery - Advanced Method</a>
|
||||||
|
*
|
||||||
|
* @return URI using the advanced lookup method
|
||||||
|
*/
|
||||||
public URI getAdvancedMethodURI() {
|
public URI getAdvancedMethodURI() {
|
||||||
return URI.create(SCHEME + ADV_SUBDOMAIN + domainPart + ADV_WELL_KNOWN(domainPart) + zbase32LocalPart + "?l=" + percentEncodedLocalPart);
|
return URI.create(SCHEME + ADV_SUBDOMAIN + domainPart + ADV_WELL_KNOWN(domainPart) + zbase32LocalPart + "?l=" + percentEncodedLocalPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String zbase32(String localPart) {
|
/**
|
||||||
MessageDigest sha1;
|
* Calculate the SHA-1 hash sum of the lower-case representation of the given string and encode that using Z-Base32.
|
||||||
|
*
|
||||||
|
* @param string string
|
||||||
|
* @return zbase32 encoded sha1 sum of the string
|
||||||
|
*/
|
||||||
|
private String sha1AndZBase32Encode(String string) {
|
||||||
|
String lowerCase = string.toLowerCase();
|
||||||
|
byte[] bytes = lowerCase.getBytes(utf8);
|
||||||
|
|
||||||
|
byte[] sha1;
|
||||||
try {
|
try {
|
||||||
sha1 = MessageDigest.getInstance("SHA1");
|
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||||
|
digest.update(bytes);
|
||||||
|
sha1 = digest.digest();
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
// SHA-1 is a MUST on JVM implementations
|
// SHA-1 is a MUST on JVM implementations
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
sha1.update(localPart.toLowerCase().getBytes(utf8));
|
|
||||||
byte[] digest = sha1.digest();
|
|
||||||
|
|
||||||
String base32KeyHandle = zBase32.encodeAsString(digest);
|
String base32KeyHandle = zBase32.encodeAsString(sha1);
|
||||||
return base32KeyHandle;
|
return base32KeyHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String percentEncode(String localPart) {
|
/**
|
||||||
|
* Encode a string using percent / URL encoding.
|
||||||
|
* @param string string
|
||||||
|
* @return percent encoded string
|
||||||
|
*/
|
||||||
|
private String percentEncode(String string) {
|
||||||
try {
|
try {
|
||||||
return URLEncoder.encode(localPart, "UTF-8");
|
return URLEncoder.encode(string, "UTF-8");
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
// UTF8 is a MUST on JVM implementations
|
// UTF8 is a MUST on JVM implementations
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate an email address string against the regex {@link #PATTERN_EMAIL} and split it into local and domain part.
|
||||||
|
*
|
||||||
|
* @param email email address string
|
||||||
|
* @return validated and split mail address
|
||||||
|
*/
|
||||||
private static MailAddress parseMailAddress(String email) {
|
private static MailAddress parseMailAddress(String email) {
|
||||||
Matcher matcher = PATTERN_EMAIL.matcher(email);
|
Matcher matcher = PATTERN_EMAIL.matcher(email);
|
||||||
if (!matcher.matches()) {
|
if (!matcher.matches()) {
|
||||||
|
@ -94,19 +179,42 @@ public class WKDAddress {
|
||||||
return new MailAddress(localPart, domainPart);
|
return new MailAddress(localPart, domainPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mail Address data class.
|
||||||
|
*/
|
||||||
private static class MailAddress {
|
private static class MailAddress {
|
||||||
private final String localPart;
|
private final String localPart;
|
||||||
private final String domainPart;
|
private final String domainPart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a MailAddress object.
|
||||||
|
* For the email address "alice@pgpainless.org", the local part would be "alice",
|
||||||
|
* while the domain part would be "pgpainless.org".
|
||||||
|
*
|
||||||
|
* @param localPart local part
|
||||||
|
* @param domainPart domain part
|
||||||
|
*/
|
||||||
MailAddress(String localPart, String domainPart) {
|
MailAddress(String localPart, String domainPart) {
|
||||||
this.localPart = localPart;
|
this.localPart = localPart;
|
||||||
this.domainPart = domainPart;
|
this.domainPart = domainPart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the local part of the email address (the part before the '@').
|
||||||
|
* Example: "pgpainless.org"
|
||||||
|
*
|
||||||
|
* @return local part
|
||||||
|
*/
|
||||||
public String getLocalPart() {
|
public String getLocalPart() {
|
||||||
return localPart;
|
return localPart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the domain part of the email address (the part after the '@').
|
||||||
|
* Example: "alice"
|
||||||
|
*
|
||||||
|
* @return domain part
|
||||||
|
*/
|
||||||
public String getDomainPart() {
|
public String getDomainPart() {
|
||||||
return domainPart;
|
return domainPart;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,24 @@ public class WKDAddressHelper {
|
||||||
// we are only interested in "email@address"
|
// we are only interested in "email@address"
|
||||||
private static final Pattern PATTERN_USER_ID = Pattern.compile("^.*\\<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)\\>.*");
|
private static final Pattern PATTERN_USER_ID = Pattern.compile("^.*\\<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)\\>.*");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an email address from a user-id string.
|
||||||
|
* The user-id is herein expected to follow the mail name-addr format described in RFC2822.
|
||||||
|
*
|
||||||
|
* Example User ID (angle normally not escaped):
|
||||||
|
* "Slim Shady <sshady@marshall-mathers.lp> [Yes, the real Shady]"
|
||||||
|
*
|
||||||
|
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.11">
|
||||||
|
* RFC4880 - OpenPGP Message Format - §5.11 User ID Packet</a>
|
||||||
|
* @see <a href="https://datatracker.ietf.org/doc/html/rfc2822#section-3.4">
|
||||||
|
* RFC2882 - Internet Message Format - §3.4 Address Specification</a>
|
||||||
|
*
|
||||||
|
* @param userId user-id
|
||||||
|
* @return email address
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException in case the user-id does not match the expected format
|
||||||
|
* and does not contain an email address.
|
||||||
|
*/
|
||||||
public static String emailFromUserId(String userId) {
|
public static String emailFromUserId(String userId) {
|
||||||
Matcher matcher = PATTERN_USER_ID.matcher(userId);
|
Matcher matcher = PATTERN_USER_ID.matcher(userId);
|
||||||
if (!matcher.matches()) {
|
if (!matcher.matches()) {
|
||||||
|
@ -23,6 +41,12 @@ public class WKDAddressHelper {
|
||||||
return email;
|
return email;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link WKDAddress} by extracting an email address from the given user-id.
|
||||||
|
*
|
||||||
|
* @param userId user-id
|
||||||
|
* @return WKD address for the user-id's email address.
|
||||||
|
*/
|
||||||
public static WKDAddress wkdAddressFromUserId(String userId) {
|
public static WKDAddress wkdAddressFromUserId(String userId) {
|
||||||
String email = emailFromUserId(userId);
|
String email = emailFromUserId(userId);
|
||||||
return WKDAddress.fromEmail(email);
|
return WKDAddress.fromEmail(email);
|
||||||
|
|
Loading…
Reference in a new issue