Merge changes I477eda7b,I29539813,I12bb9d13

* changes:
  Add REST endpoint to check if an email is a code owner of a path
  BranchCodeOwners: JavaDoc fixes
  CodeOwnerResolverResult: Include debug messages
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
index 22bc543..0508bc7 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
@@ -31,6 +31,7 @@
   /** Create a request to retrieve code owner config files from the branch. */
   CodeOwnerConfigFilesRequest codeOwnerConfigFiles() throws RestApiException;
 
+  /** Request to retrieve code owner config files from the branch. */
   abstract class CodeOwnerConfigFilesRequest {
     private boolean includeNonParsableFiles;
     private String email;
@@ -57,7 +58,7 @@
       return this;
     }
 
-    /** Returns the email that should appear in the returned code owner config files/ */
+    /** Returns the email that should appear in the returned code owner config files. */
     @Nullable
     public String getEmail() {
       return email;
@@ -74,13 +75,13 @@
       return this;
     }
 
-    /** Returns the path glob that should be matched by the returned code owner config files/ */
+    /** Returns the path glob that should be matched by the returned code owner config files. */
     @Nullable
     public String getPath() {
       return path;
     }
 
-    /** Executes the request and retrieves the paths of the requested code owner config file */
+    /** Executes the request and retrieves the paths of the requested code owner config file. */
     public abstract List<String> paths() throws RestApiException;
   }
 
@@ -88,6 +89,69 @@
   RenameEmailResultInfo renameEmailInCodeOwnerConfigFiles(RenameEmailInput input)
       throws RestApiException;
 
+  /** Checks the code ownership of a user for a path in a branch. */
+  CodeOwnerCheckRequest checkCodeOwner() throws RestApiException;
+
+  /** Request for checking the code ownership of a user for a path in a branch. */
+  abstract class CodeOwnerCheckRequest {
+    private String email;
+    private String path;
+    private String user;
+
+    /**
+     * Sets the email for which the code ownership should be checked.
+     *
+     * @param email the email for which the code ownership should be checked
+     */
+    public CodeOwnerCheckRequest email(String email) {
+      this.email = email;
+      return this;
+    }
+
+    /** Returns the email for which the code ownership should be checked. */
+    @Nullable
+    public String getEmail() {
+      return email;
+    }
+
+    /**
+     * Sets the path for which the code ownership should be checked.
+     *
+     * @param path the path for which the code ownership should be checked
+     */
+    public CodeOwnerCheckRequest path(String path) {
+      this.path = path;
+      return this;
+    }
+
+    /** Returns the path for which the code ownership should be checked. */
+    @Nullable
+    public String getPath() {
+      return path;
+    }
+
+    /**
+     * Sets the user for which the code owner visibility should be checked.
+     *
+     * <p>If not specified the code owner visibility is not checked.
+     *
+     * @param user the user for which the code owner visibility should be checked
+     */
+    public CodeOwnerCheckRequest user(@Nullable String user) {
+      this.user = user;
+      return this;
+    }
+
+    /** Returns the user for which the code owner visibility should be checked. */
+    @Nullable
+    public String getUser() {
+      return user;
+    }
+
+    /** Executes the request and retrieves the result. */
+    public abstract CodeOwnerCheckInfo check() throws RestApiException;
+  }
+
   /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
@@ -108,5 +172,10 @@
         throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public CodeOwnerCheckRequest checkCodeOwner() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
index 7052a1b..beae1f6 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwnersImpl.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwner;
 import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerBranchConfig;
 import com.google.gerrit.plugins.codeowners.restapi.GetCodeOwnerConfigFiles;
 import com.google.gerrit.plugins.codeowners.restapi.RenameEmail;
@@ -35,6 +36,7 @@
   private final GetCodeOwnerBranchConfig getCodeOwnerBranchConfig;
   private final Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider;
   private final RenameEmail renameEmail;
+  private final Provider<CheckCodeOwner> checkCodeOwnerProvider;
   private final BranchResource branchResource;
 
   @Inject
@@ -42,10 +44,12 @@
       GetCodeOwnerBranchConfig getCodeOwnerBranchConfig,
       Provider<GetCodeOwnerConfigFiles> getCodeOwnerConfigFilesProvider,
       RenameEmail renameEmail,
+      Provider<CheckCodeOwner> checkCodeOwnerProvider,
       @Assisted BranchResource branchResource) {
     this.getCodeOwnerConfigFilesProvider = getCodeOwnerConfigFilesProvider;
     this.getCodeOwnerBranchConfig = getCodeOwnerBranchConfig;
     this.renameEmail = renameEmail;
+    this.checkCodeOwnerProvider = checkCodeOwnerProvider;
     this.branchResource = branchResource;
   }
 
@@ -81,4 +85,22 @@
       throw asRestApiException("Cannot rename email", e);
     }
   }
+
+  @Override
+  public CodeOwnerCheckRequest checkCodeOwner() throws RestApiException {
+    return new CodeOwnerCheckRequest() {
+      @Override
+      public CodeOwnerCheckInfo check() throws RestApiException {
+        CheckCodeOwner checkCodeOwner = checkCodeOwnerProvider.get();
+        checkCodeOwner.setEmail(getEmail());
+        checkCodeOwner.setPath(getPath());
+        checkCodeOwner.setUser(getUser());
+        try {
+          return checkCodeOwner.apply(branchResource).value();
+        } catch (Exception e) {
+          throw asRestApiException("Cannot check code owner", e);
+        }
+      }
+    };
+  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
new file mode 100644
index 0000000..4b33e11
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
@@ -0,0 +1,74 @@
+// Copyright (C) 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 com.google.gerrit.plugins.codeowners.api;
+
+import java.util.List;
+
+/**
+ * REST API representation of the result of checking a code owner via {code
+ * com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwner}.
+ *
+ * <p>This class determines the JSON format of check result in the REST API.
+ */
+public class CodeOwnerCheckInfo {
+  /**
+   * Whether the given email owns the specified path in the branch.
+   *
+   * <p>True if:
+   *
+   * <ul>
+   *   <li>the given email is resolvable (see {@link #isResolvable}) and
+   *   <li>any code owner config file assigns codeownership to the email for the path (see {@link
+   *       #codeOwnerConfigFilePaths}) or the email is configured as default code owner (see {@link
+   *       CodeOwnerCheckInfo#isDefaultCodeOwner} field) or the email is configured as global code
+   *       owner (see {@link #isGlobalCodeOwner} field)
+   * </ul>
+   */
+  public boolean isCodeOwner;
+
+  /**
+   * Whether the given email is resolvable for the specified user or the calling user if no user was
+   * specified.
+   */
+  public boolean isResolvable;
+
+  /**
+   * Paths of the code owner config files that assign code ownership to the given email for the
+   * specified path.
+   *
+   * <p>If code ownership is assigned to the email via a code owner config files, but the email is
+   * not resolvable (see {@link #isResolvable} field), the user is not a code owner.
+   */
+  public List<String> codeOwnerConfigFilePaths;
+
+  /**
+   * Whether the given email is configured as a default code owner.
+   *
+   * <p>If the email is configured as default code owner, but the email is not resolvable (see
+   * {@link #isResolvable} field), the user is not a code owner.
+   */
+  public boolean isDefaultCodeOwner;
+
+  /**
+   * Whether the given email is configured as a global code owner.
+   *
+   * <p>If the email is configured as global code owner, but the email is not resolvable (see {@link
+   * #isResolvable} field), the user is not a code owner.
+   */
+  public boolean isGlobalCodeOwner;
+
+  /** Debug logs that may help to understand why the user is or isn't a code owner. */
+  public List<String> debugLogs;
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index da32379..8e1d906 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -19,6 +19,7 @@
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
@@ -42,6 +43,8 @@
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -156,7 +159,7 @@
       PathCodeOwnersResult pathCodeOwnersResult =
           pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).resolveCodeOwnerConfig();
       return resolve(
-          pathCodeOwnersResult.getPathCodeOwners(), pathCodeOwnersResult.hasUnresolvedImports());
+          pathCodeOwnersResult.getPathCodeOwners(), pathCodeOwnersResult.unresolvedImports());
     }
   }
 
@@ -178,22 +181,26 @@
    * @see #resolve(CodeOwnerReference)
    */
   public CodeOwnerResolverResult resolve(Set<CodeOwnerReference> codeOwnerReferences) {
-    return resolve(codeOwnerReferences, /* hasUnresolvedImports= */ false);
+    return resolve(codeOwnerReferences, /* unresolvedImports= */ ImmutableList.of());
   }
 
   /**
    * Resolves the given {@link CodeOwnerReference}s to {@link CodeOwner}s.
    *
    * @param codeOwnerReferences the code owner references that should be resolved
-   * @param hasUnresolvedImports whether there are unresolved imports
+   * @param unresolvedImports list of unresolved imports
    * @return the {@link CodeOwner} for the given code owner references
    * @see #resolve(CodeOwnerReference)
    */
   private CodeOwnerResolverResult resolve(
-      Set<CodeOwnerReference> codeOwnerReferences, boolean hasUnresolvedImports) {
+      Set<CodeOwnerReference> codeOwnerReferences, List<UnresolvedImport> unresolvedImports) {
     requireNonNull(codeOwnerReferences, "codeOwnerReferences");
+    requireNonNull(unresolvedImports, "unresolvedImports");
     AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
     AtomicBoolean hasUnresolvedCodeOwners = new AtomicBoolean(false);
+    List<String> messages = new ArrayList<>();
+    unresolvedImports.forEach(
+        unresolvedImport -> messages.add(unresolvedImport.format(codeOwnersPluginConfiguration)));
     ImmutableSet<CodeOwner> codeOwners =
         codeOwnerReferences.stream()
             .filter(
@@ -204,19 +211,27 @@
                   }
                   return true;
                 })
-            .map(this::resolve)
+            .map(this::resolveWithMessages)
             .filter(
-                codeOwner -> {
-                  if (!codeOwner.isPresent()) {
+                resolveResult -> {
+                  messages.addAll(resolveResult.messages());
+                  if (!resolveResult.isPresent()) {
                     hasUnresolvedCodeOwners.set(true);
                     return false;
                   }
                   return true;
                 })
-            .map(Optional::get)
+            .map(OptionalResultWithMessages::get)
             .collect(toImmutableSet());
-    return CodeOwnerResolverResult.create(
-        codeOwners, ownedByAllUsers.get(), hasUnresolvedCodeOwners.get(), hasUnresolvedImports);
+    CodeOwnerResolverResult codeOwnerResolverResult =
+        CodeOwnerResolverResult.create(
+            codeOwners,
+            ownedByAllUsers.get(),
+            hasUnresolvedCodeOwners.get(),
+            !unresolvedImports.isEmpty(),
+            messages);
+    logger.atFine().log("resolve result = %s", codeOwnerResolverResult);
+    return codeOwnerResolverResult;
   }
 
   /**
@@ -261,33 +276,55 @@
    *     Optional#empty()}
    */
   public Optional<CodeOwner> resolve(CodeOwnerReference codeOwnerReference) {
+    OptionalResultWithMessages<CodeOwner> resolveResult = resolveWithMessages(codeOwnerReference);
+    logger.atFine().log("resolve result = %s", resolveResult);
+    return resolveResult.result();
+  }
+
+  public OptionalResultWithMessages<CodeOwner> resolveWithMessages(
+      CodeOwnerReference codeOwnerReference) {
     String email = requireNonNull(codeOwnerReference, "codeOwnerReference").email();
-    logger.atFine().log("resolving code owner reference %s", codeOwnerReference);
 
-    if (!isEmailDomainAllowed(email)) {
-      logger.atFine().log("domain of email %s is not allowed", email);
-      return Optional.empty();
+    List<String> messages = new ArrayList<>();
+    messages.add(String.format("resolving code owner reference %s", codeOwnerReference));
+
+    OptionalResultWithMessages<Boolean> emailDomainAllowedResult = isEmailDomainAllowed(email);
+    messages.addAll(emailDomainAllowedResult.messages());
+    if (!emailDomainAllowedResult.get()) {
+      return OptionalResultWithMessages.createEmpty(messages);
     }
 
-    Optional<AccountState> accountState =
-        lookupEmail(email).flatMap(accountId -> lookupAccount(accountId, email));
-    if (!accountState.isPresent()) {
-      logger.atFine().log("no account for email %s", email);
-      return Optional.empty();
-    }
-    if (!accountState.get().account().isActive()) {
-      logger.atFine().log("account for email %s is inactive", email);
-      return Optional.empty();
-    }
-    if (enforceVisibility && !isVisible(accountState.get(), email)) {
-      logger.atFine().log(
-          "account %d of email %s is not visible", accountState.get().account().id().get(), email);
-      return Optional.empty();
+    OptionalResultWithMessages<Account.Id> lookupEmailResult = lookupEmail(email);
+    messages.addAll(lookupEmailResult.messages());
+    if (lookupEmailResult.isEmpty()) {
+      return OptionalResultWithMessages.createEmpty(messages);
     }
 
-    CodeOwner codeOwner = CodeOwner.create(accountState.get().account().id());
-    logger.atFine().log("resolved to code owner %s", codeOwner);
-    return Optional.of(codeOwner);
+    Account.Id accountId = lookupEmailResult.get();
+    OptionalResultWithMessages<AccountState> lookupAccountResult = lookupAccount(accountId, email);
+    messages.addAll(lookupAccountResult.messages());
+    if (lookupAccountResult.isEmpty()) {
+      return OptionalResultWithMessages.createEmpty(messages);
+    }
+
+    AccountState accountState = lookupAccountResult.get();
+    if (!accountState.account().isActive()) {
+      messages.add(String.format("account %s for email %s is inactive", accountId, email));
+      return OptionalResultWithMessages.createEmpty(messages);
+    }
+    if (enforceVisibility) {
+      OptionalResultWithMessages<Boolean> isVisibleResult = isVisible(accountState, email);
+      messages.addAll(isVisibleResult.messages());
+      if (!isVisibleResult.get()) {
+        return OptionalResultWithMessages.createEmpty(messages);
+      }
+    } else {
+      messages.add("code owner visibility is not checked");
+    }
+
+    CodeOwner codeOwner = CodeOwner.create(accountState.account().id());
+    messages.add(String.format("resolved to account %s", codeOwner.accountId()));
+    return OptionalResultWithMessages.create(codeOwner, messages);
   }
 
   /** Whether the given account can be seen. */
@@ -306,7 +343,7 @@
    * @param email the email that should be looked up
    * @return the ID of the account to which the email belongs if was found
    */
-  private Optional<Account.Id> lookupEmail(String email) {
+  private OptionalResultWithMessages<Account.Id> lookupEmail(String email) {
     ImmutableSet<ExternalId> extIds;
     try {
       extIds = externalIds.byEmail(email);
@@ -315,17 +352,17 @@
     }
 
     if (extIds.isEmpty()) {
-      logger.atFine().log(
-          "cannot resolve code owner email %s: no account with this email exists", email);
-      return Optional.empty();
+      return OptionalResultWithMessages.createEmpty(
+          String.format(
+              "cannot resolve code owner email %s: no account with this email exists", email));
     }
 
     if (extIds.stream().map(ExternalId::accountId).distinct().count() > 1) {
-      logger.atFine().log("cannot resolve code owner email %s: email is ambiguous", email);
-      return Optional.empty();
+      return OptionalResultWithMessages.createEmpty(
+          String.format("cannot resolve code owner email %s: email is ambiguous", email));
     }
 
-    return Optional.of(extIds.stream().findFirst().get().accountId());
+    return OptionalResultWithMessages.create(extIds.stream().findFirst().get().accountId());
   }
 
   /**
@@ -336,16 +373,17 @@
    * @param email the email that was resolved to the account ID
    * @return the {@link AccountState} of the account with the given account ID, if it exists
    */
-  private Optional<AccountState> lookupAccount(Account.Id accountId, String email) {
+  private OptionalResultWithMessages<AccountState> lookupAccount(
+      Account.Id accountId, String email) {
     Optional<AccountState> accountState = accountCache.get(accountId);
     if (!accountState.isPresent()) {
-      logger.atFine().log(
-          "cannot resolve code owner email %s: email belongs to account %s,"
-              + " but no account with this ID exists",
-          email, accountId);
-      return Optional.empty();
+      return OptionalResultWithMessages.createEmpty(
+          String.format(
+              "cannot resolve code owner email %s: email belongs to account %s,"
+                  + " but no account with this ID exists",
+              email, accountId));
     }
-    return accountState;
+    return OptionalResultWithMessages.create(accountState.get());
   }
 
   /**
@@ -365,14 +403,15 @@
    * @return {@code true} if the given account and email are visible to the user, otherwise {@code
    *     false}
    */
-  private boolean isVisible(AccountState accountState, String email) {
+  private OptionalResultWithMessages<Boolean> isVisible(AccountState accountState, String email) {
     if (!canSee(accountState)) {
-      logger.atFine().log(
-          "cannot resolve code owner email %s: account %s is not visible to user %s",
-          email,
-          accountState.account().id(),
-          user != null ? user.getLoggableName() : currentUser.get().getLoggableName());
-      return false;
+      return OptionalResultWithMessages.create(
+          false,
+          String.format(
+              "cannot resolve code owner email %s: account %s is not visible to user %s",
+              email,
+              accountState.account().id(),
+              user != null ? user.getLoggableName() : currentUser.get().getLoggableName()));
     }
 
     if (!email.equals(accountState.account().preferredEmail())) {
@@ -380,14 +419,23 @@
 
       if (user != null) {
         if (user.hasEmailAddress(email)) {
-          // it's a secondary email of the user, users can always see their own secondary emails
-          return true;
+          return OptionalResultWithMessages.create(
+              true,
+              String.format(
+                  "email %s is visible to user %s: email is a secondary email that is owned by this"
+                      + " user",
+                  email, user.getLoggableName()));
         }
       } else if (currentUser.get().isIdentifiedUser()
           && currentUser.get().asIdentifiedUser().hasEmailAddress(email)) {
         // it's a secondary email of the calling user, users can always see their own secondary
         // emails
-        return true;
+        return OptionalResultWithMessages.create(
+            true,
+            String.format(
+                "email %s is visible to the calling user %s: email is a secondary email that is"
+                    + " owned by this user",
+                email, currentUser.get().getLoggableName()));
       }
 
       // the email is a secondary email of another account, check if the user can see secondary
@@ -395,18 +443,33 @@
       try {
         if (user != null) {
           if (!permissionBackend.user(user).test(GlobalPermission.MODIFY_ACCOUNT)) {
-            logger.atFine().log(
-                "cannot resolve code owner email %s: account %s is referenced by secondary email,"
-                    + " but user %s cannot see secondary emails",
-                email, accountState.account().id(), user.getLoggableName());
-            return false;
+            return OptionalResultWithMessages.create(
+                false,
+                String.format(
+                    "cannot resolve code owner email %s: account %s is referenced by secondary email"
+                        + " but user %s cannot see secondary emails",
+                    email, accountState.account().id(), user.getLoggableName()));
           }
+          return OptionalResultWithMessages.create(
+              true,
+              String.format(
+                  "resolved code owner email %s: account %s is referenced by secondary email"
+                      + " and user %s can see secondary emails",
+                  email, accountState.account().id(), user.getLoggableName()));
         } else if (!permissionBackend.currentUser().test(GlobalPermission.MODIFY_ACCOUNT)) {
-          logger.atFine().log(
-              "cannot resolve code owner email %s: account %s is referenced by secondary email,"
-                  + " but the calling user %s cannot see secondary emails",
-              email, accountState.account().id(), currentUser.get().getLoggableName());
-          return false;
+          return OptionalResultWithMessages.create(
+              false,
+              String.format(
+                  "cannot resolve code owner email %s: account %s is referenced by secondary email"
+                      + " but the calling user %s cannot see secondary emails",
+                  email, accountState.account().id(), currentUser.get().getLoggableName()));
+        } else {
+          return OptionalResultWithMessages.create(
+              true,
+              String.format(
+                  "resolved code owner email %s: account %s is referenced by secondary email"
+                      + " and the calling user %s can see secondary emails",
+                  email, accountState.account().id(), currentUser.get().getLoggableName()));
         }
       } catch (PermissionBackendException e) {
         throw new StorageException(
@@ -415,8 +478,12 @@
             e);
       }
     }
-
-    return true;
+    return OptionalResultWithMessages.create(
+        true,
+        String.format(
+            "account %s is visible to user %s",
+            accountState.account().id(),
+            user != null ? user.getLoggableName() : currentUser.get().getLoggableName()));
   }
 
   /**
@@ -426,29 +493,30 @@
    * @return {@code true} if the domain of the given email is allowed for code owners, otherwise
    *     {@code false}
    */
-  public boolean isEmailDomainAllowed(String email) {
+  public OptionalResultWithMessages<Boolean> isEmailDomainAllowed(String email) {
     requireNonNull(email, "email");
 
     ImmutableSet<String> allowedEmailDomains =
         codeOwnersPluginConfiguration.getAllowedEmailDomains();
     if (allowedEmailDomains.isEmpty()) {
-      // all domains are allowed
-      return true;
+      return OptionalResultWithMessages.create(true, "all domains are allowed");
     }
 
     if (email.equals(ALL_USERS_WILDCARD)) {
-      return true;
+      return OptionalResultWithMessages.create(true, "all users wildcard is allowed");
     }
 
     int emailAtIndex = email.lastIndexOf('@');
     if (emailAtIndex >= 0 && emailAtIndex < email.length() - 1) {
       String emailDomain = email.substring(emailAtIndex + 1);
-      logger.atFine().log("email domain = %s", emailDomain);
-      return allowedEmailDomains.contains(emailDomain);
+      boolean isEmailDomainAllowed = allowedEmailDomains.contains(emailDomain);
+      return OptionalResultWithMessages.create(
+          isEmailDomainAllowed,
+          String.format(
+              "domain %s of email %s is %s",
+              emailDomain, email, isEmailDomainAllowed ? "allowed" : "not allowed"));
     }
 
-    // email has no domain
-    logger.atFine().log("email %s has no domain", email);
-    return false;
+    return OptionalResultWithMessages.create(false, String.format("email %s has no domain", email));
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
index b5b71c0..4a9e372 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverResult.java
@@ -18,8 +18,10 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
+import java.util.List;
 
 /** The result of resolving code owner references via {@link CodeOwnerResolver}. */
 @AutoValue
@@ -49,6 +51,9 @@
   /** Whether there are imports which couldn't be resolved. */
   public abstract boolean hasUnresolvedImports();
 
+  /** Gets messages that were collected while resolving the code owners. */
+  public abstract ImmutableList<String> messages();
+
   /**
    * Whether there are any code owners defined for the path, regardless of whether they can be
    * resolved or not.
@@ -67,6 +72,7 @@
         .add("ownedByAllUsers", ownedByAllUsers())
         .add("hasUnresolvedCodeOwners", hasUnresolvedCodeOwners())
         .add("hasUnresolvedImports", hasUnresolvedImports())
+        .add("messages", messages())
         .toString();
   }
 
@@ -75,9 +81,14 @@
       ImmutableSet<CodeOwner> codeOwners,
       boolean ownedByAllUsers,
       boolean hasUnresolvedCodeOwners,
-      boolean hasUnresolvedImports) {
+      boolean hasUnresolvedImports,
+      List<String> messages) {
     return new AutoValue_CodeOwnerResolverResult(
-        codeOwners, ownedByAllUsers, hasUnresolvedCodeOwners, hasUnresolvedImports);
+        codeOwners,
+        ownedByAllUsers,
+        hasUnresolvedCodeOwners,
+        hasUnresolvedImports,
+        ImmutableList.copyOf(messages));
   }
 
   /** Creates a empty {@link CodeOwnerResolverResult} instance. */
@@ -86,6 +97,7 @@
         /* codeOwners= */ ImmutableSet.of(),
         /* ownedByAllUsers= */ false,
         /* hasUnresolvedCodeOwners= */ false,
-        /* hasUnresolvedImports= */ false);
+        /* hasUnresolvedImports= */ false,
+        /* messages= */ ImmutableList.of());
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OptionalResultWithMessages.java b/java/com/google/gerrit/plugins/codeowners/backend/OptionalResultWithMessages.java
new file mode 100644
index 0000000..66fce67
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OptionalResultWithMessages.java
@@ -0,0 +1,83 @@
+// Copyright (C) 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 com.google.gerrit.plugins.codeowners.backend;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * An optional result of an operation with optional messages.
+ *
+ * @param <T> type of the optional result
+ */
+@AutoValue
+public abstract class OptionalResultWithMessages<T> {
+  /** Gets the result. */
+  public abstract Optional<T> result();
+
+  /** Whether the result is present. */
+  public boolean isPresent() {
+    return result().isPresent();
+  }
+
+  /** Whether the result is empty. */
+  public boolean isEmpty() {
+    return !result().isPresent();
+  }
+
+  /** Returns the result value, if present. Fails if the result is not present. */
+  public T get() {
+    return result().get();
+  }
+
+  /** Gets the messages. */
+  public abstract ImmutableList<String> messages();
+
+  /** Creates a {@link OptionalResultWithMessages} instance without messages. */
+  public static <T> OptionalResultWithMessages<T> create(T result) {
+    return create(result, ImmutableList.of());
+  }
+
+  /** Creates an empty {@link OptionalResultWithMessages} instance with a single message. */
+  public static <T> OptionalResultWithMessages<T> createEmpty(String message) {
+    requireNonNull(message, "message");
+    return createEmpty(ImmutableList.of(message));
+  }
+
+  /** Creates an empty {@link OptionalResultWithMessages} instance with messages. */
+  public static <T> OptionalResultWithMessages<T> createEmpty(List<String> messages) {
+    requireNonNull(messages, "messages");
+    return new AutoValue_OptionalResultWithMessages<>(
+        Optional.empty(), ImmutableList.copyOf(messages));
+  }
+
+  /** Creates a {@link OptionalResultWithMessages} instance with messages. */
+  public static <T> OptionalResultWithMessages<T> create(T result, String message) {
+    requireNonNull(message, "message");
+    return create(result, ImmutableList.of(message));
+  }
+
+  /** Creates a {@link OptionalResultWithMessages} instance with messages. */
+  public static <T> OptionalResultWithMessages<T> create(T result, List<String> messages) {
+    requireNonNull(result, "result");
+    requireNonNull(messages, "messages");
+    return new AutoValue_OptionalResultWithMessages<>(
+        Optional.of(result), ImmutableList.copyOf(messages));
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
new file mode 100644
index 0000000..1bc41d7
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -0,0 +1,240 @@
+// Copyright (C) 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 com.google.gerrit.plugins.codeowners.restapi;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigHierarchy;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.OptionalResultWithMessages;
+import com.google.gerrit.plugins.codeowners.backend.PathCodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.PathCodeOwnersResult;
+import com.google.gerrit.plugins.codeowners.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.BranchResource;
+import com.google.gerrit.server.restapi.account.AccountsCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Option;
+
+/**
+ * REST endpoint that checks the code ownership of a user for a path in a branch.
+ *
+ * <p>This REST endpoint handles {@code GET
+ * /projects/<project-name>/branches/<branch-name>/code_owners.check} requests.
+ */
+public class CheckCodeOwner implements RestReadView<BranchResource> {
+  private final PermissionBackend permissionBackend;
+  private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+  private final CodeOwnerConfigHierarchy codeOwnerConfigHierarchy;
+  private final PathCodeOwners.Factory pathCodeOwnersFactory;
+  private final Provider<CodeOwnerResolver> codeOwnerResolverProvider;
+  private final CodeOwners codeOwners;
+  private final AccountsCollection accountsCollection;
+
+  private String email;
+  private String path;
+  private String user;
+  private IdentifiedUser identifiedUser;
+
+  @Inject
+  public CheckCodeOwner(
+      PermissionBackend permissionBackend,
+      CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
+      CodeOwnerConfigHierarchy codeOwnerConfigHierarchy,
+      PathCodeOwners.Factory pathCodeOwnersFactory,
+      Provider<CodeOwnerResolver> codeOwnerResolverProvider,
+      CodeOwners codeOwners,
+      AccountsCollection accountsCollection) {
+    this.permissionBackend = permissionBackend;
+    this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+    this.codeOwnerConfigHierarchy = codeOwnerConfigHierarchy;
+    this.pathCodeOwnersFactory = pathCodeOwnersFactory;
+    this.codeOwnerResolverProvider = codeOwnerResolverProvider;
+    this.codeOwners = codeOwners;
+    this.accountsCollection = accountsCollection;
+  }
+
+  @Option(name = "--email", usage = "email for which the code ownership should be checked")
+  public void setEmail(String email) {
+    this.email = email;
+  }
+
+  @Option(name = "--path", usage = "path for which the code ownership should be checked")
+  public void setPath(String path) {
+    this.path = path;
+  }
+
+  @Option(
+      name = "--user",
+      usage =
+          "user for which the code owner visibility should be checked,"
+              + " if not specified the code owner visibility is not checked")
+  public void setUser(String user) {
+    this.user = user;
+  }
+
+  @Override
+  public Response<CodeOwnerCheckInfo> apply(BranchResource branchResource)
+      throws BadRequestException, AuthException, IOException, ConfigInvalidException,
+          PermissionBackendException {
+    permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+
+    validateInput();
+
+    Path absolutePath = JgitPath.of(path).getAsAbsolutePath();
+    List<String> messages = new ArrayList<>();
+    List<Path> codeOwnerConfigFilePaths = new ArrayList<>();
+    AtomicBoolean isCodeOwnershipAssignedToEmail = new AtomicBoolean(false);
+    AtomicBoolean isDefaultCodeOwner = new AtomicBoolean(false);
+    codeOwnerConfigHierarchy.visit(
+        branchResource.getBranchKey(),
+        ObjectId.fromString(branchResource.getRevision()),
+        absolutePath,
+        codeOwnerConfig -> {
+          Path codeOwnerConfigFilePath = codeOwners.getFilePath(codeOwnerConfig.key());
+          messages.add(
+              String.format(
+                  "checking code owner config file %s:%s:%s",
+                  codeOwnerConfig.key().project(),
+                  codeOwnerConfig.key().shortBranchName(),
+                  codeOwnerConfigFilePath));
+          PathCodeOwnersResult pathCodeOwnersResult =
+              pathCodeOwnersFactory.create(codeOwnerConfig, absolutePath).resolveCodeOwnerConfig();
+          pathCodeOwnersResult
+              .unresolvedImports()
+              .forEach(
+                  unresolvedImport ->
+                      messages.add(unresolvedImport.format(codeOwnersPluginConfiguration)));
+          Optional<CodeOwnerReference> codeOwnerReference =
+              pathCodeOwnersResult.getPathCodeOwners().stream()
+                  .filter(cor -> cor.email().equals(email))
+                  .findAny();
+          if (codeOwnerReference.isPresent()) {
+            isCodeOwnershipAssignedToEmail.set(true);
+
+            if (RefNames.isConfigRef(codeOwnerConfig.key().ref())) {
+              messages.add(
+                  String.format(
+                      "found email %s as code owner in default code owner config", email));
+              isDefaultCodeOwner.set(true);
+            } else {
+              messages.add(
+                  String.format(
+                      "found email %s as code owner in %s", email, codeOwnerConfigFilePath));
+              codeOwnerConfigFilePaths.add(codeOwnerConfigFilePath);
+            }
+          }
+
+          if (pathCodeOwnersResult.ignoreParentCodeOwners()) {
+            messages.add("parent code owners are ignored");
+          }
+
+          return !pathCodeOwnersResult.ignoreParentCodeOwners();
+        });
+
+    boolean isGlobalCodeOwner = isGlobalCodeOwner(branchResource.getNameKey());
+    if (isGlobalCodeOwner) {
+      messages.add(String.format("found email %s as global code owner", email));
+      isCodeOwnershipAssignedToEmail.set(true);
+    }
+
+    OptionalResultWithMessages<Boolean> isResolvableResult = isResolvable();
+    boolean isResolvable = isResolvableResult.get();
+    messages.addAll(isResolvableResult.messages());
+
+    CodeOwnerCheckInfo codeOwnerCheckInfo = new CodeOwnerCheckInfo();
+    codeOwnerCheckInfo.isCodeOwner = isCodeOwnershipAssignedToEmail.get() && isResolvable;
+    codeOwnerCheckInfo.isResolvable = isResolvable;
+    codeOwnerCheckInfo.codeOwnerConfigFilePaths =
+        codeOwnerConfigFilePaths.stream().map(Path::toString).collect(toList());
+    codeOwnerCheckInfo.isDefaultCodeOwner = isDefaultCodeOwner.get();
+    codeOwnerCheckInfo.isGlobalCodeOwner = isGlobalCodeOwner;
+    codeOwnerCheckInfo.debugLogs = messages;
+    return Response.ok(codeOwnerCheckInfo);
+  }
+
+  private void validateInput()
+      throws BadRequestException, AuthException, IOException, ConfigInvalidException {
+    if (email == null) {
+      throw new BadRequestException("email required");
+    }
+    if (path == null) {
+      throw new BadRequestException("path required");
+    }
+    if (user != null) {
+      try {
+        identifiedUser =
+            accountsCollection
+                .parse(TopLevelResource.INSTANCE, IdString.fromDecoded(user))
+                .getUser();
+      } catch (ResourceNotFoundException e) {
+        throw new BadRequestException(String.format("user %s not found", user), e);
+      }
+    }
+  }
+
+  private boolean isGlobalCodeOwner(Project.NameKey projectName) {
+    return codeOwnersPluginConfiguration.getGlobalCodeOwners(projectName).stream()
+        .filter(cor -> cor.email().equals(email))
+        .findAny()
+        .isPresent();
+  }
+
+  private OptionalResultWithMessages<Boolean> isResolvable() {
+    if (email.equals(CodeOwnerResolver.ALL_USERS_WILDCARD)) {
+      return OptionalResultWithMessages.create(true);
+    }
+
+    CodeOwnerResolver codeOwnerResolver = codeOwnerResolverProvider.get();
+    if (identifiedUser != null) {
+      codeOwnerResolver.forUser(identifiedUser);
+    } else {
+      codeOwnerResolver.enforceVisibility(false);
+    }
+    OptionalResultWithMessages<CodeOwner> resolveResult =
+        codeOwnerResolver.resolveWithMessages(CodeOwnerReference.create(email));
+
+    List<String> messages = new ArrayList<>();
+    messages.add(String.format("trying to resolve email %s", email));
+    messages.addAll(resolveResult.messages());
+    return OptionalResultWithMessages.create(resolveResult.isPresent(), messages);
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
index 03d8858..68d057e 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
@@ -32,6 +32,7 @@
     get(BRANCH_KIND, "code_owners.config_files").to(GetCodeOwnerConfigFiles.class);
     get(BRANCH_KIND, "code_owners.branch_config").to(GetCodeOwnerBranchConfig.class);
     post(BRANCH_KIND, "code_owners.rename").to(RenameEmail.class);
+    get(BRANCH_KIND, "code_owners.check").to(CheckCodeOwner.class);
 
     factory(CodeOwnerJson.Factory.class);
     DynamicMap.mapOf(binder(), CodeOwnersInBranchCollection.PathResource.PATH_KIND);
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
new file mode 100644
index 0000000..b060198
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
@@ -0,0 +1,98 @@
+// Copyright (C) 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 com.google.gerrit.plugins.codeowners.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+
+/** {@link Subject} for doing assertions on {@link CodeOwnerCheckInfo}s. */
+public class CodeOwnerCheckInfoSubject extends Subject {
+  /**
+   * Starts fluent chain to do assertions on a {@link CodeOwnerCheckInfo}.
+   *
+   * @param codeOwnerCheckInfo the code owner check info on which assertions should be done
+   * @return the created {@link CodeOwnerCheckInfoSubject}
+   */
+  public static CodeOwnerCheckInfoSubject assertThat(CodeOwnerCheckInfo codeOwnerCheckInfo) {
+    return assertAbout(codeOwnerCheckInfos()).that(codeOwnerCheckInfo);
+  }
+
+  private static Factory<CodeOwnerCheckInfoSubject, CodeOwnerCheckInfo> codeOwnerCheckInfos() {
+    return CodeOwnerCheckInfoSubject::new;
+  }
+
+  private final CodeOwnerCheckInfo codeOwnerCheckInfo;
+
+  private CodeOwnerCheckInfoSubject(
+      FailureMetadata metadata, CodeOwnerCheckInfo codeOwnerCheckInfo) {
+    super(metadata, codeOwnerCheckInfo);
+    this.codeOwnerCheckInfo = codeOwnerCheckInfo;
+  }
+
+  public void isCodeOwner() {
+    check("isCodeOwner").that(codeOwnerCheckInfo().isCodeOwner).isTrue();
+  }
+
+  public void isNotCodeOwner() {
+    check("isCodeOwner").that(codeOwnerCheckInfo().isCodeOwner).isFalse();
+  }
+
+  public void isResolvable() {
+    check("isResolvable").that(codeOwnerCheckInfo().isResolvable).isTrue();
+  }
+
+  public void isNotResolvable() {
+    check("isResolvable").that(codeOwnerCheckInfo().isResolvable).isFalse();
+  }
+
+  public IterableSubject hasCodeOwnerConfigFilePathsThat() {
+    return check("codeOwnerConfigFilePaths").that(codeOwnerCheckInfo().codeOwnerConfigFilePaths);
+  }
+
+  public void isDefaultCodeOwner() {
+    check("isDefaultCodeOwner").that(codeOwnerCheckInfo().isDefaultCodeOwner).isTrue();
+  }
+
+  public void isNotDefaultCodeOwner() {
+    check("isDefaultCodeOwner").that(codeOwnerCheckInfo().isDefaultCodeOwner).isFalse();
+  }
+
+  public void isGlobalCodeOwner() {
+    check("isGlobalCodeOwner").that(codeOwnerCheckInfo().isGlobalCodeOwner).isTrue();
+  }
+
+  public void isNotGlobalCodeOwner() {
+    check("isGlobalCodeOwner").that(codeOwnerCheckInfo().isGlobalCodeOwner).isFalse();
+  }
+
+  public void hasDebugLogsThatContainAllOf(String... expectedMessages) {
+    for (String expectedMessage : expectedMessages) {
+      check("debugLogs").that(codeOwnerCheckInfo().debugLogs).contains(expectedMessage);
+    }
+  }
+
+  public IterableSubject hasDebugLogsThat() {
+    return check("debugLogs").that(codeOwnerCheckInfo().debugLogs);
+  }
+
+  private CodeOwnerCheckInfo codeOwnerCheckInfo() {
+    isNotNull();
+    return codeOwnerCheckInfo;
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/OptionalResultWithMessagesSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/OptionalResultWithMessagesSubject.java
new file mode 100644
index 0000000..a0a9082
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/testing/OptionalResultWithMessagesSubject.java
@@ -0,0 +1,72 @@
+// Copyright (C) 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 com.google.gerrit.plugins.codeowners.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.gerrit.truth.OptionalSubject.optionals;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Subject;
+import com.google.gerrit.plugins.codeowners.backend.OptionalResultWithMessages;
+
+/** {@link Subject} for doing assertions on {@link OptionalResultWithMessages}s. */
+public class OptionalResultWithMessagesSubject extends Subject {
+  /**
+   * Starts fluent chain to do assertions on a {@link OptionalResultWithMessages}.
+   *
+   * @param optionalResultWithMessages the optionalResultWithMessages instance on which assertions
+   *     should be done
+   * @return the created {@link OptionalResultWithMessagesSubject}
+   */
+  public static OptionalResultWithMessagesSubject assertThat(
+      OptionalResultWithMessages<?> optionalResultWithMessages) {
+    return assertAbout(optionalResultsWithMessages()).that(optionalResultWithMessages);
+  }
+
+  /**
+   * Creates subject factory for mapping {@link OptionalResultWithMessages}s to {@link
+   * OptionalResultWithMessagesSubject}s.
+   */
+  private static Subject.Factory<OptionalResultWithMessagesSubject, OptionalResultWithMessages<?>>
+      optionalResultsWithMessages() {
+    return OptionalResultWithMessagesSubject::new;
+  }
+
+  private final OptionalResultWithMessages<?> optionalResultWithMessages;
+
+  private OptionalResultWithMessagesSubject(
+      FailureMetadata metadata, OptionalResultWithMessages<?> optionalResultWithMessages) {
+    super(metadata, optionalResultWithMessages);
+    this.optionalResultWithMessages = optionalResultWithMessages;
+  }
+
+  public void isPresent() {
+    check("result()").about(optionals()).that(optionalResultWithMessages().result()).isPresent();
+  }
+
+  public void isEmpty() {
+    check("result()").about(optionals()).that(optionalResultWithMessages().result()).isEmpty();
+  }
+
+  public IterableSubject hasMessagesThat() {
+    return check("messages()").that(optionalResultWithMessages().messages());
+  }
+
+  private OptionalResultWithMessages<?> optionalResultWithMessages() {
+    isNotNull();
+    return optionalResultWithMessages;
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index e659580..b2dd664 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -734,7 +734,7 @@
   private Optional<CommitValidationMessage> validateCodeOwnerReference(
       IdentifiedUser user, Path codeOwnerConfigFilePath, CodeOwnerReference codeOwnerReference) {
     CodeOwnerResolver codeOwnerResolver = codeOwnerResolverProvider.get().forUser(user);
-    if (!codeOwnerResolver.isEmailDomainAllowed(codeOwnerReference.email())) {
+    if (!codeOwnerResolver.isEmailDomainAllowed(codeOwnerReference.email()).get()) {
       return error(
           String.format(
               "the domain of the code owner email '%s' in '%s' is not allowed for code owners",
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
new file mode 100644
index 0000000..ea4fc3a
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -0,0 +1,682 @@
+// Copyright (C) 2021 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 com.google.gerrit.plugins.codeowners.acceptance.api;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerCheckInfoSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.plugins.codeowners.JgitPath;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.backend.findowners.FindOwnersBackend;
+import com.google.gerrit.plugins.codeowners.backend.proto.ProtoBackend;
+import com.google.gerrit.plugins.codeowners.config.BackendConfig;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Acceptance test for the {@link com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwner} REST
+ * endpoint.
+ */
+public class CheckCodeOwnerIT extends AbstractCodeOwnersIT {
+  private static final String ROOT_PATH = "/";
+
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private AccountOperations accountOperations;
+  @Inject private ProjectOperations projectOperations;
+  @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdate;
+  @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
+
+  private BackendConfig backendConfig;
+
+  @Before
+  public void setUpCodeOwnersPlugin() throws Exception {
+    backendConfig = plugin.getSysInjector().getInstance(BackendConfig.class);
+  }
+
+  @Test
+  public void requiresEmail() throws Exception {
+    BadRequestException exception =
+        assertThrows(BadRequestException.class, () -> checkCodeOwner("/", /* email= */ null));
+    assertThat(exception).hasMessageThat().isEqualTo("email required");
+  }
+
+  @Test
+  public void requiresPath() throws Exception {
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class, () -> checkCodeOwner(/* path= */ null, user.email()));
+    assertThat(exception).hasMessageThat().isEqualTo("path required");
+  }
+
+  @Test
+  public void requiresCallerToBeAdmin() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    AuthException authException =
+        assertThrows(AuthException.class, () -> checkCodeOwner(ROOT_PATH, user.email()));
+    assertThat(authException).hasMessageThat().isEqualTo("administrate server not permitted");
+  }
+
+  @Test
+  public void checkCodeOwner() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+    setAsCodeOwners("/foo/", codeOwner);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
+            String.format("resolved to account %s", codeOwner.id()));
+  }
+
+  @Test
+  public void checkCodeOwnerThatHasCodeOwnershipThroughMultipleFiles() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+    setAsRootCodeOwners(codeOwner);
+    setAsCodeOwners("/foo/", codeOwner);
+    setAsCodeOwners("/foo/bar/", codeOwner);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(
+            getCodeOwnerConfigFilePath("/foo/bar/"),
+            getCodeOwnerConfigFilePath("/foo/"),
+            getCodeOwnerConfigFilePath(ROOT_PATH))
+        .inOrder();
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                codeOwner.email(), getCodeOwnerConfigFilePath("/foo/bar/")),
+            String.format(
+                "found email %s as code owner in %s",
+                codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
+            String.format(
+                "found email %s as code owner in %s",
+                codeOwner.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format("resolved to account %s", codeOwner.id()));
+  }
+
+  @Test
+  public void checkCodeOwnerWithParentCodeOwnersIgnored() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+    setAsRootCodeOwners(codeOwner);
+
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath("/foo/")
+        .ignoreParentCodeOwners()
+        .addCodeOwnerEmail(codeOwner.email())
+        .create();
+
+    setAsCodeOwners("/foo/bar/", codeOwner);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(
+            getCodeOwnerConfigFilePath("/foo/bar/"), getCodeOwnerConfigFilePath("/foo/"))
+        .inOrder();
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                codeOwner.email(), getCodeOwnerConfigFilePath("/foo/bar/")),
+            String.format(
+                "found email %s as code owner in %s",
+                codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
+            "parent code owners are ignored",
+            String.format("resolved to account %s", codeOwner.id()));
+  }
+
+  @Test
+  public void checkCodeOwner_secondaryEmail() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+    String secondaryEmail = "codeOwnerSecondary@example.com";
+    accountOperations
+        .account(codeOwner.id())
+        .forUpdate()
+        .addSecondaryEmail(secondaryEmail)
+        .update();
+
+    setAsRootCodeOwner(secondaryEmail);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, secondaryEmail);
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                secondaryEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format("resolved to account %s", codeOwner.id()));
+  }
+
+  @Test
+  public void checkNonCodeOwner() throws Exception {
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, user.email());
+    assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(String.format("resolved to account %s", user.id()));
+  }
+
+  @Test
+  public void checkNonExistingEmail() throws Exception {
+    String nonExistingEmail = "non-exiting@example.com";
+
+    setAsRootCodeOwner(nonExistingEmail);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, nonExistingEmail);
+    assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                nonExistingEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format(
+                "cannot resolve code owner email %s: no account with this email exists",
+                nonExistingEmail));
+  }
+
+  @Test
+  public void checkAmbiguousExistingEmail() throws Exception {
+    String ambiguousEmail = "ambiguous@example.com";
+
+    setAsRootCodeOwner(ambiguousEmail);
+
+    // Add the email to 2 accounts to make it ambiguous.
+    addEmail(user.id(), ambiguousEmail);
+    addEmail(admin.id(), ambiguousEmail);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, ambiguousEmail);
+    assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                ambiguousEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format(
+                "cannot resolve code owner email %s: email is ambiguous", ambiguousEmail));
+  }
+
+  @Test
+  public void checkOrphanedEmail() throws Exception {
+    // Create an external ID with an email for a non-existing account.
+    String orphanedEmail = "orphaned@example.com";
+    Account.Id accountId = Account.id(999999);
+    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+      ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+      extIdNotes.upsert(ExternalId.createEmail(accountId, orphanedEmail));
+      extIdNotes.commit(md);
+    }
+
+    setAsRootCodeOwner(orphanedEmail);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, orphanedEmail);
+    assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                orphanedEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format(
+                "cannot resolve code owner email %s: email belongs to account %s,"
+                    + " but no account with this ID exists",
+                orphanedEmail, accountId));
+  }
+
+  @Test
+  public void checkInactiveAccount() throws Exception {
+    TestAccount inactiveUser =
+        accountCreator.create(
+            "inactiveUser", "inactiveUser@example.com", "Inactive User", /* displayName= */ null);
+    accountOperations.account(inactiveUser.id()).forUpdate().inactive().update();
+
+    setAsRootCodeOwners(inactiveUser);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, inactiveUser.email());
+    assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                inactiveUser.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format(
+                "account %s for email %s is inactive", inactiveUser.id(), inactiveUser.email()));
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.allowedEmailDomain", value = "example.net")
+  public void checkEmailWithAllowedDomain() throws Exception {
+    String emailWithAllowedEmailDomain = "foo@example.net";
+    TestAccount userWithAllowedEmail =
+        accountCreator.create(
+            "userWithAllowedEmail",
+            emailWithAllowedEmailDomain,
+            "User with allowed emil",
+            /* displayName= */ null);
+
+    setAsRootCodeOwners(userWithAllowedEmail);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, emailWithAllowedEmailDomain);
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                emailWithAllowedEmailDomain, getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format(
+                "domain %s of email %s is allowed",
+                emailWithAllowedEmailDomain.substring(emailWithAllowedEmailDomain.indexOf('@') + 1),
+                emailWithAllowedEmailDomain));
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.allowedEmailDomain", value = "example.net")
+  public void checkEmailWithNonAllowedDomain() throws Exception {
+    String emailWithNonAllowedEmailDomain = "foo@example.com";
+    TestAccount userWithAllowedEmail =
+        accountCreator.create(
+            "userWithNonAllowedEmail",
+            emailWithNonAllowedEmailDomain,
+            "User with non-allowed emil",
+            /* displayName= */ null);
+
+    setAsRootCodeOwners(userWithAllowedEmail);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo =
+        checkCodeOwner(ROOT_PATH, emailWithNonAllowedEmailDomain);
+    assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                emailWithNonAllowedEmailDomain, getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format(
+                "domain %s of email %s is not allowed",
+                emailWithNonAllowedEmailDomain.substring(
+                    emailWithNonAllowedEmailDomain.indexOf('@') + 1),
+                emailWithNonAllowedEmailDomain));
+  }
+
+  @Test
+  public void checkAllUsersWildcard() throws Exception {
+    CodeOwnerCheckInfo checkCodeOwnerInfo =
+        checkCodeOwner(ROOT_PATH, CodeOwnerResolver.ALL_USERS_WILDCARD);
+    assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+  }
+
+  @Test
+  public void checkAllUsersWildcard_ownedByAllUsers() throws Exception {
+    setAsRootCodeOwner(CodeOwnerResolver.ALL_USERS_WILDCARD);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo =
+        checkCodeOwner(ROOT_PATH, CodeOwnerResolver.ALL_USERS_WILDCARD);
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                CodeOwnerResolver.ALL_USERS_WILDCARD, getCodeOwnerConfigFilePath(ROOT_PATH)));
+  }
+
+  @Test
+  public void checkDefaultCodeOwner() throws Exception {
+    TestAccount defaultCodeOwner =
+        accountCreator.create(
+            "defaultCodeOwner",
+            "defaultCodeOwner@example.com",
+            "Default Code Owner",
+            /* displayName= */ null);
+    setAsDefaultCodeOwners(defaultCodeOwner);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, defaultCodeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+    assertThat(checkCodeOwnerInfo).isDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in default code owner config",
+                defaultCodeOwner.email()),
+            String.format("resolved to account %s", defaultCodeOwner.id()));
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "globalCodeOwner@example.com")
+  public void checkGlobalCodeOwner() throws Exception {
+    TestAccount globalCodeOwner =
+        accountCreator.create(
+            "globalCodeOwner",
+            "globalCodeOwner@example.com",
+            "Global Code Owner",
+            /* displayName= */ null);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, globalCodeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format("found email %s as global code owner", globalCodeOwner.email()),
+            String.format("resolved to account %s", globalCodeOwner.id()));
+  }
+
+  @Test
+  public void checkCodeOwnerForOtherUser() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+    setAsCodeOwners("/foo/", codeOwner);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email(), user.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                codeOwner.email(), getCodeOwnerConfigFilePath("/foo/")),
+            String.format("account %s is visible to user %s", codeOwner.id(), user.username()),
+            String.format("resolved to account %s", codeOwner.id()));
+  }
+
+  @Test
+  public void cannotCheckForNonExistingUser() throws Exception {
+    String nonExistingEmail = "non-existing@example.com";
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class, () -> checkCodeOwner("/", user.email(), nonExistingEmail));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(String.format("user %s not found", nonExistingEmail));
+  }
+
+  @Test
+  @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+  public void checkNonVisibleCodeOwnerForOtherUser() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+
+    setAsRootCodeOwners(codeOwner);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo =
+        checkCodeOwner(ROOT_PATH, codeOwner.email(), user.email());
+    assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                codeOwner.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format(
+                "cannot resolve code owner email %s: account %s is not visible to user %s",
+                codeOwner.email(), codeOwner.id(), user.username()));
+  }
+
+  @Test
+  public void checkNonVisibleCodeOwnerForOtherUser_secondaryEmail() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+    String secondaryEmail = "codeOwnerSecondary@example.com";
+    accountOperations
+        .account(codeOwner.id())
+        .forUpdate()
+        .addSecondaryEmail(secondaryEmail)
+        .update();
+
+    setAsRootCodeOwner(secondaryEmail);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, secondaryEmail, user.email());
+    assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                secondaryEmail, getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format(
+                "cannot resolve code owner email %s: account %s is referenced by secondary email"
+                    + " but user %s cannot see secondary emails",
+                secondaryEmail, codeOwner.id(), user.username()));
+  }
+
+  @Test
+  public void debugLogsContainUnresolvedImports() throws Exception {
+    // imports are not supported for the proto backend
+    assume().that(backendConfig.getDefaultBackend()).isNotInstanceOf(ProtoBackend.class);
+
+    CodeOwnerConfigReference unresolvableCodeOwnerConfigReference =
+        CodeOwnerConfigReference.create(
+            CodeOwnerConfigImportMode.ALL, "non-existing/" + getCodeOwnerConfigFileName());
+    CodeOwnerConfig.Key codeOwnerConfigKey =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath(ROOT_PATH)
+            .addImport(unresolvableCodeOwnerConfigReference)
+            .create();
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, user.email());
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThat()
+        .contains(
+            String.format(
+                "The import of %s:%s:%s in %s:%s:%s cannot be resolved:"
+                    + " code owner config does not exist (revision = %s)",
+                project,
+                "master",
+                JgitPath.of(unresolvableCodeOwnerConfigReference.filePath()).getAsAbsolutePath(),
+                project,
+                "master",
+                getCodeOwnerConfigFilePath(codeOwnerConfigKey.folderPath().toString()),
+                projectOperations.project(project).getHead("master").name()));
+  }
+
+  private CodeOwnerCheckInfo checkCodeOwner(String path, String email) throws RestApiException {
+    return checkCodeOwner(path, email, null);
+  }
+
+  private CodeOwnerCheckInfo checkCodeOwner(String path, String email, @Nullable String user)
+      throws RestApiException {
+    return projectCodeOwnersApiFactory
+        .project(project)
+        .branch("master")
+        .checkCodeOwner()
+        .path(path)
+        .email(email)
+        .user(user)
+        .check();
+  }
+
+  private String getCodeOwnerConfigFilePath(String folderPath) {
+    assertThat(folderPath).startsWith("/");
+    assertThat(folderPath).endsWith("/");
+    return folderPath + getCodeOwnerConfigFileName();
+  }
+
+  private void setAsRootCodeOwner(String email) {
+    codeOwnerConfigOperations
+        .newCodeOwnerConfig()
+        .project(project)
+        .branch("master")
+        .folderPath(ROOT_PATH)
+        .addCodeOwnerEmail(email)
+        .create();
+  }
+
+  private String getCodeOwnerConfigFileName() {
+    CodeOwnerBackend backend = backendConfig.getDefaultBackend();
+    if (backend instanceof FindOwnersBackend) {
+      return FindOwnersBackend.CODE_OWNER_CONFIG_FILE_NAME;
+    } else if (backend instanceof ProtoBackend) {
+      return ProtoBackend.CODE_OWNER_CONFIG_FILE_NAME;
+    }
+    throw new IllegalStateException("unknown code owner backend: " + backend.getClass().getName());
+  }
+
+  private void addEmail(Account.Id accountId, String email) throws Exception {
+    accountsUpdate
+        .get()
+        .update(
+            "Test update",
+            accountId,
+            (a, u) ->
+                u.addExternalId(
+                    ExternalId.create(
+                        "foo",
+                        "bar" + accountId.get(),
+                        accountId,
+                        email,
+                        /* hashedPassword= */ null)));
+  }
+}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
index d70be9f..7d2bd66 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CodeOwnersRestApiBindingsIT.java
@@ -53,7 +53,8 @@
       ImmutableList.of(
           RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.config_files"),
           RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.branch_config"),
-          RestCall.post("/projects/%s/branches/%s/code-owners~code_owners.rename"));
+          RestCall.post("/projects/%s/branches/%s/code-owners~code_owners.rename"),
+          RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.check"));
 
   private static final ImmutableList<RestCall> BRANCH_CODE_OWNER_CONFIGS_ENDPOINTS =
       ImmutableList.of(RestCall.get("/projects/%s/branches/%s/code-owners~code_owners.config/%s"));
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
index 1210ae5..0297426 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerSubject.assertThat;
+import static com.google.gerrit.plugins.codeowners.testing.OptionalResultWithMessagesSubject.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.google.common.collect.ImmutableSet;
@@ -82,25 +83,41 @@
 
   @Test
   public void resolveCodeOwnerReferenceForNonExistingEmail() throws Exception {
-    assertThat(
-            codeOwnerResolver.get().resolve(CodeOwnerReference.create("non-existing@example.com")))
-        .isEmpty();
+    String nonExistingEmail = "non-existing@example.com";
+    OptionalResultWithMessages<CodeOwner> result =
+        codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(nonExistingEmail));
+    assertThat(result).isEmpty();
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "cannot resolve code owner email %s: no account with this email exists",
+                nonExistingEmail));
   }
 
   @Test
   public void resolveCodeOwnerReferenceForEmail() throws Exception {
-    Optional<CodeOwner> codeOwner =
-        codeOwnerResolver.get().resolve(CodeOwnerReference.create(admin.email()));
-    assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(admin.id());
+    OptionalResultWithMessages<CodeOwner> result =
+        codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(admin.email()));
+    assertThat(result.get()).hasAccountIdThat().isEqualTo(admin.id());
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(String.format("account %s is visible to user %s", admin.id(), admin.username()));
   }
 
   @Test
   public void cannotResolveCodeOwnerReferenceForStarAsEmail() throws Exception {
-    Optional<CodeOwner> codeOwner =
+    OptionalResultWithMessages<CodeOwner> result =
         codeOwnerResolver
             .get()
-            .resolve(CodeOwnerReference.create(CodeOwnerResolver.ALL_USERS_WILDCARD));
-    assertThat(codeOwner).isEmpty();
+            .resolveWithMessages(CodeOwnerReference.create(CodeOwnerResolver.ALL_USERS_WILDCARD));
+    assertThat(result).isEmpty();
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "cannot resolve code owner email %s: no account with this email exists",
+                CodeOwnerResolver.ALL_USERS_WILDCARD));
   }
 
   @Test
@@ -116,27 +133,47 @@
                     ExternalId.create(
                         "foo", "bar", user.id(), admin.email(), /* hashedPassword= */ null)));
 
-    assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(admin.email()))).isEmpty();
+    OptionalResultWithMessages<CodeOwner> result =
+        codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(admin.email()));
+    assertThat(result).isEmpty();
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format("cannot resolve code owner email %s: email is ambiguous", admin.email()));
   }
 
   @Test
   public void resolveCodeOwnerReferenceForOrphanedEmail() throws Exception {
     // Create an external ID with an email for a non-existing account.
     String email = "foo.bar@example.com";
+    Account.Id accountId = Account.id(999999);
     try (Repository allUsersRepo = repoManager.openRepository(allUsers);
         MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
       ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
-      extIdNotes.upsert(ExternalId.createEmail(Account.id(999999), email));
+      extIdNotes.upsert(ExternalId.createEmail(accountId, email));
       extIdNotes.commit(md);
     }
 
-    assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(email))).isEmpty();
+    OptionalResultWithMessages<CodeOwner> result =
+        codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(email));
+    assertThat(result).isEmpty();
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "cannot resolve code owner email %s: email belongs to account %s, but no account with this ID exists",
+                email, accountId));
   }
 
   @Test
   public void resolveCodeOwnerReferenceForInactiveUser() throws Exception {
     accountOperations.account(user.id()).forUpdate().inactive().update();
-    assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(user.email()))).isEmpty();
+    OptionalResultWithMessages<CodeOwner> result =
+        codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(user.email()));
+    assertThat(result).isEmpty();
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(String.format("account %s for email %s is inactive", user.id(), user.email()));
   }
 
   @Test
@@ -149,7 +186,15 @@
 
     // user2 cannot see the admin account since they do not share any group and
     // "accounts.visibility" is set to "SAME_GROUP".
-    assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(admin.email()))).isEmpty();
+    OptionalResultWithMessages<CodeOwner> result =
+        codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(admin.email()));
+    assertThat(result).isEmpty();
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "cannot resolve code owner email %s: account %s is not visible to user %s",
+                admin.email(), admin.id(), user2.username()));
   }
 
   @Test
@@ -162,33 +207,57 @@
 
     // admin has the "Modify Account" global capability and hence can see the secondary email of the
     // user account.
-    Optional<CodeOwner> codeOwner =
-        codeOwnerResolver.get().resolve(CodeOwnerReference.create(secondaryEmail));
-    assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(user.id());
+    OptionalResultWithMessages<CodeOwner> result =
+        codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+    assertThat(result.get()).hasAccountIdThat().isEqualTo(user.id());
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "resolved code owner email %s: account %s is referenced by secondary email and the calling user %s can see secondary emails",
+                secondaryEmail, user.id(), admin.username()));
 
     // admin has the "Modify Account" global capability and hence can see the secondary email of the
     // user account if another user is the calling user
     requestScopeOperations.setApiUser(user2.id());
-    codeOwner =
+    result =
         codeOwnerResolver
             .get()
             .forUser(identifiedUserFactory.create(admin.id()))
-            .resolve(CodeOwnerReference.create(secondaryEmail));
-    assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(user.id());
+            .resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+    assertThat(result.get()).hasAccountIdThat().isEqualTo(user.id());
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "resolved code owner email %s: account %s is referenced by secondary email and user %s can see secondary emails",
+                secondaryEmail, user.id(), admin.username()));
 
     // user can see its own secondary email.
     requestScopeOperations.setApiUser(user.id());
-    codeOwner = codeOwnerResolver.get().resolve(CodeOwnerReference.create(secondaryEmail));
-    assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(user.id());
+    result = codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+    assertThat(result.get()).hasAccountIdThat().isEqualTo(user.id());
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "email %s is visible to the calling user %s: email is a secondary email that is owned by this user",
+                secondaryEmail, user.username()));
 
     // user can see its own secondary email if another user is the calling user.
     requestScopeOperations.setApiUser(user2.id());
-    codeOwner =
+    result =
         codeOwnerResolver
             .get()
             .forUser(identifiedUserFactory.create(user.id()))
-            .resolve(CodeOwnerReference.create(secondaryEmail));
-    assertThat(codeOwner).value().hasAccountIdThat().isEqualTo(user.id());
+            .resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+    assertThat(result.get()).hasAccountIdThat().isEqualTo(user.id());
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "email %s is visible to user %s: email is a secondary email that is owned by this user",
+                secondaryEmail, user.username()));
   }
 
   @Test
@@ -200,18 +269,31 @@
     // user doesn't have the "Modify Account" global capability and hence cannot see the secondary
     // email of the admin account.
     requestScopeOperations.setApiUser(user.id());
-    assertThat(codeOwnerResolver.get().resolve(CodeOwnerReference.create(secondaryEmail)))
-        .isEmpty();
+    OptionalResultWithMessages<CodeOwner> result =
+        codeOwnerResolver.get().resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+    assertThat(result).isEmpty();
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "cannot resolve code owner email %s: account %s is referenced by secondary email but the calling user %s cannot see secondary emails",
+                secondaryEmail, admin.id(), user.username()));
 
     // user doesn't have the "Modify Account" global capability and hence cannot see the secondary
     // email of the admin account if another user is the calling user
     requestScopeOperations.setApiUser(admin.id());
-    assertThat(
-            codeOwnerResolver
-                .get()
-                .forUser(identifiedUserFactory.create(user.id()))
-                .resolve(CodeOwnerReference.create(secondaryEmail)))
-        .isEmpty();
+    result =
+        codeOwnerResolver
+            .get()
+            .forUser(identifiedUserFactory.create(user.id()))
+            .resolveWithMessages(CodeOwnerReference.create(secondaryEmail));
+    assertThat(result).isEmpty();
+    assertThat(result)
+        .hasMessagesThat()
+        .contains(
+            String.format(
+                "cannot resolve code owner email %s: account %s is referenced by secondary email but user %s cannot see secondary emails",
+                secondaryEmail, admin.id(), user.username()));
   }
 
   @Test
@@ -400,30 +482,43 @@
       name = "plugin.code-owners.allowedEmailDomain",
       values = {"example.com", "example.net"})
   public void configuredEmailDomainsAreAllowed() throws Exception {
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.com")).isTrue();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.net")).isTrue();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.org@example.com"))
-        .isTrue();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.org")).isFalse();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo")).isFalse();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.com@example.org"))
-        .isFalse();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed(CodeOwnerResolver.ALL_USERS_WILDCARD))
-        .isTrue();
+    assertIsEmailDomainAllowed(
+        "foo@example.com", true, "domain example.com of email foo@example.com is allowed");
+    assertIsEmailDomainAllowed(
+        "foo@example.net", true, "domain example.net of email foo@example.net is allowed");
+    assertIsEmailDomainAllowed(
+        "foo@example.org@example.com",
+        true,
+        "domain example.com of email foo@example.org@example.com is allowed");
+    assertIsEmailDomainAllowed(
+        "foo@example.org", false, "domain example.org of email foo@example.org is not allowed");
+    assertIsEmailDomainAllowed("foo", false, "email foo has no domain");
+    assertIsEmailDomainAllowed(
+        "foo@example.com@example.org",
+        false,
+        "domain example.org of email foo@example.com@example.org is not allowed");
+    assertIsEmailDomainAllowed(
+        CodeOwnerResolver.ALL_USERS_WILDCARD, true, "all users wildcard is allowed");
   }
 
   @Test
   public void allEmailDomainsAreAllowed() throws Exception {
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.com")).isTrue();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.net")).isTrue();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.org@example.com"))
-        .isTrue();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.org")).isTrue();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo")).isTrue();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed("foo@example.com@example.org"))
-        .isTrue();
-    assertThat(codeOwnerResolver.get().isEmailDomainAllowed(CodeOwnerResolver.ALL_USERS_WILDCARD))
-        .isTrue();
+    String expectedMessage = "all domains are allowed";
+    assertIsEmailDomainAllowed("foo@example.com", true, expectedMessage);
+    assertIsEmailDomainAllowed("foo@example.net", true, expectedMessage);
+    assertIsEmailDomainAllowed("foo@example.org@example.com", true, expectedMessage);
+    assertIsEmailDomainAllowed("foo@example.org", true, expectedMessage);
+    assertIsEmailDomainAllowed("foo", true, expectedMessage);
+    assertIsEmailDomainAllowed("foo@example.com@example.org", true, expectedMessage);
+    assertIsEmailDomainAllowed(CodeOwnerResolver.ALL_USERS_WILDCARD, true, expectedMessage);
+  }
+
+  private void assertIsEmailDomainAllowed(
+      String email, boolean expectedResult, String expectedMessage) {
+    OptionalResultWithMessages<Boolean> isEmailDomainAllowedResult =
+        codeOwnerResolver.get().isEmailDomainAllowed(email);
+    assertThat(isEmailDomainAllowedResult.get()).isEqualTo(expectedResult);
+    assertThat(isEmailDomainAllowedResult.messages()).containsExactly(expectedMessage);
   }
 
   @Test
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index f6cd213..8902d36 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -219,6 +219,66 @@
   ]
 ```
 
+### <a id="check-code-owner">Check Code Owner
+_'GET /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/branches/[\{branch-id\}](../../../Documentation/rest-api-projects.html#branch-id)/code_owners.check/'_
+
+Checks the code ownership of a user for a path in a branch.
+
+The following request parameters can be specified:
+
+| Field Name  |           | Description |
+| ----------- | --------- | ----------- |
+| `email`     | mandatory | Email for which the code ownership should be checked.
+| `path`      | mandatory | Path for which the code ownership should be checked.
+| `user`      | optional  | User for which the code owner visibility should be checked. If not specified the code owner visibility is not checked. Can be used to investigate why a code owner is not shown/suggested to this user.
+
+Requires that the caller has the [Administrate
+Server](../../../Documentation/access-control.html#capability_administrateServer)
+global capability.
+
+This REST endpoint is intended to investigate code owner configurations that do
+not work as intended. The response contains debug logs that may point out issues
+with the code owner configuration. For example, with this REST endpoint it is
+possible to find out why a certain email that is listed as code owner in a code
+owner config file is ignored (e.g. because it is ambiguous or because it belongs
+to an inactive account).
+
+#### Request
+
+```
+  GET /projects/foo%2Fbar/branches/master/code_owners.check?email=xyz@example.com&path=/foo/bar/baz.md HTTP/1.0
+```
+
+#### Response
+
+As response a [CodeOwnerCheckInfo](#code-owner-check-info) entity is returned.
+
+```
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "is_code_owner": "false",
+    "is_resolvable": "false",
+    "code_owner_config_file_paths": [
+      "/OWNERS",
+    ],
+    "is_default_code_owner": "false",
+    "is_global_code_owner": "false",
+    "debug_logs": [
+      "checking code owner config file foo/bar:master:/OWNERS",
+      "found email xyz@example.com as code owner in /OWNERS",
+      "trying to resolve email xyz@example.com",
+      "resolving code owner reference CodeOwnerReference{email=xyz@example.com}",
+      "all domains are allowed",
+      "cannot resolve code owner email xyz@example.com: email is ambiguous",
+      "email xyz@example.com is not a code owner of path '/foo/bar/baz.md'"
+    ]
+  }
+```
+
 ### <a id="rename-email-in-code-owner-config-files">Rename Email In Code Owner Config Files
 _'POST /projects/[\{project-name\}](../../../Documentation/rest-api-projects.html#project-name)/branches/[\{branch-id\}](../../../Documentation/rest-api-projects.html#branch-id)/code_owners.rename/'_
 
@@ -617,11 +677,26 @@
 
 ---
 
+### <a id="code-owner-check-info"> CodeOwnerCheckInfo
+The `CodeOwnerCheckInfo` entity contains the result of checking the code
+ownership of a user for a path in a branch.
+
+| Field Name      | Description |
+| --------------- | ----------- |
+| `is_code_owner` | Whether the given email owns the specified path in the branch. True if: a) the given email is resolvable (see field `is_resolvable') and b) any code owner config file assigns codeownership to the email for the path (see field `code_owner_config_file_paths`) or the email is configured as default code owner (see field `is_default_code_owner` or the email is configured as global code owner (see field `is_global_code_owner`).
+| `is_resolvable` | Whether the given email is resolvable for the specified user or the calling user if no user was specified.
+| `code_owner_config_file_paths` | Paths of the code owner config files that assign code ownership to the specified email and path as a list. Note that if code ownership is assigned to the email via a code owner config files, but the email is not resolvable (see field `is_resolvable` field), the user is not a code owner.
+| `is_default_code_owner` | Whether the given email is configured as a default code owner in the code owner config file in `refs/meta/config`. Note that if the email is configured as default code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner.
+| `is_global_code_owner` | Whether the given email is configured as a global
+code owner. Note that if the email is configured as global code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner.
+| `debug_logs` | List of debug logs that may help to understand why the user is or isn't a code owner.
+
+---
+
 ### <a id="code-owner-config-info"> CodeOwnerConfigInfo
 The `CodeOwnerConfigInfo` entity contains information about a code owner config
 for a path.
 
-
 | Field Name  |          | Description |
 | ----------- | -------- | ----------- |
 | `ignore_parent_code_owners` | optional, not set if `false` | Whether code owners from parent code owner configs (code owner configs in parent folders) should be ignored.