I have this Katalon Studio codebase, that has a member conversion script, that depends on being able to extract sign-up link from a Gmail account specific for testing. On that Gmail account (let's call it KatalonAutoTesting@gmail.com
), I have the OAuth 2.0 Client IDs and Service Account set up, and have downloaded a gmail-access-credentials.json
for the service account.
My SMDEmailUtils
class for all this, is defined to be:
public final class SMDEmailUtils {
private static Gmail _GmailInstance;
public static String GetMainEmail() {
if (!GeneralWebUIUtils.GlobalVariableExists('emailID'))
return "dev@xxx-dev.com";
return GlobalVariable.emailID.toString();
}
public static String CreateEmailFor(String firstName, String lastName) {
final String[] mainEmailParts = this.GetMainEmail().split('@');
return "${mainEmailParts[0]}+${firstName}${lastName}@${mainEmailParts[1]}"
.replaceAll("\'", "");
}
public static String ExtractSignUpLink() {
String link;
int retryAttempts;
ActionHandler.Handle({
link = this.ProcessHTML(this.GetLatestMessageBody(30),
"//a[.//div[@class = 'sign-mail-btn-text']]/@href");
}, { boolean success, ex ->
if (!success)
sleep(1000 * 2**retryAttempts++);
}, TimeUnit.MINUTES.toSeconds(15))
return link;
}
/**
* Application name.
*/
private static final String AppName = "Gmail Message Accessor";
/**
* Global instance of the JSON factory.
*/
private static final JsonFactory JSONFactory = GsonFactory.getDefaultInstance();
/**
* Directory to store authorization tokens for this application.
*/
private static final String TokensDirectoryPath = "tokens";
/**
* Global instance of the Scopes required by this quickstart.
* If modifying these Scopes, delete your previously saved tokens/ folder.
*/
private static final List<String> Scopes = [GmailScopes.GMAIL_READONLY,];
private static final String CredentialsFilePath = "./gmail-access-credentials.json";
/**
* Creates an authorized Credential object.
*
* @param httpTransport The network HTTP Transport.
* @return An authorized Credential object.
* @throws IOException If the credentials.json file cannot be found.
*/
private static Credential getCredentials(final NetHttpTransport httpTransport)
throws IOException {
// Load client secrets.
InputStream is = new FileInputStream(this.CredentialsFilePath);
if (is == null) {
throw new FileNotFoundException("Resource not found: " + this.CredentialsFilePath);
}
GoogleClientSecrets clientSecrets =
GoogleClientSecrets.load(this.JSONFactory, new InputStreamReader(is));
// Build flow and trigger user authorization request.
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
httpTransport, this.JSONFactory, clientSecrets, this.Scopes)
.setDataStoreFactory(new FileDataStoreFactory(new java.io.File(this.TokensDirectoryPath)))
.setAccessType("offline")
.build();
LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build();
return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
}
public static Gmail GetGmailInstance() {
if (this._GmailInstance == null) {
// Build a new authorized API client service.
final NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
this._GmailInstance = new Gmail.Builder(httpTransport, this.JSONFactory, getCredentials(httpTransport))
.setApplicationName(this.AppName)
.build();
}
return this._GmailInstance;
}
public static String GetLatestMessageBody(int timeOut) {
return this.getContent(this.GetLatestMessage(timeOut));
}
public static Message GetLatestMessage(int timeOut) {
// get the latest thread list
ListThreadsResponse response = this.HandleRequest({
return this.GetGmailInstance()
.users()
.threads()
.list(this.GetMainEmail())
.setQ("is:unread newer_than:1d")
.setIncludeSpamTrash(true)
.execute();
},
{ ListThreadsResponse res -> return !res.getThreads().isEmpty() },
timeOut);
return response.getThreads()
.collect({ Thread thread ->
return this.GetGmailInstance()
.users()
.threads()
.get(this.GetMainEmail(), thread.getId())
.execute()
}).max { Thread thread -> thread.getMessages().last().getInternalDate() }
.getMessages()
.last();
}
/**
* Copied from https://stackoverflow.com/a/58286921
* @param message
* @return
*/
private static String getContent(Message message) {
StringBuilder stringBuilder = new StringBuilder();
try {
getPlainTextFromMessageParts(message.getPayload().getParts(), stringBuilder);
// NOTE: updated by Mike Warren, this was adapted for message that contain URLs in its body
return new String(Base64.getUrlDecoder().decode(stringBuilder.toString()),
StandardCharsets.UTF_8);
} catch (UnsupportedEncodingException e) {
// NOTE: updated by Mike Warren
Logger.getGlobal().severe("UnsupportedEncoding: ${e.toString()}");
return message.getSnippet();
}
}
/**
* Copied from https://stackoverflow.com/a/58286921
* @param messageParts
* @param stringBuilder
*/
private static void getPlainTextFromMessageParts(List<MessagePart> messageParts, StringBuilder stringBuilder) {
for (MessagePart messagePart : messageParts) {
// NOTE: updated by Mike Warren
if (messagePart.getMimeType().startsWith("text/")) {
stringBuilder.append(messagePart.getBody().getData());
}
if (messagePart.getParts() != null) {
getPlainTextFromMessageParts(messagePart.getParts(), stringBuilder);
}
}
}
public static GenericJson HandleRequest(Closure<GenericJson> onDoRequest, Closure<Boolean> onCheckResponse, int timeOut) {
long startTime = System.currentTimeSeconds();
int exponent = 0;
while (System.currentTimeSeconds() < startTime + timeOut) {
GenericJson response = onDoRequest();
if (onCheckResponse(response))
return response;
// wait some time to try again, exponential backoff style
sleep(1000 * 2**exponent++);
}
return null;
}
/**
* **NOTE**: forked from https://stackoverflow.com/a/2269464/2027839 , and then refactored
*
* Processes HTML, using XPath
*
* @param html
* @param xpath
* @return the result
*/
public static String ProcessHTML(String html, String xpath) {
final String properHTML = this.ToProperHTML(html);
final Element document = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(new ByteArrayInputStream( properHTML.bytes ))
.documentElement;
return XPathFactory.newInstance()
.newXPath()
.evaluate( xpath, document );
}
private static String ToProperHTML(String html) {
// SOURCE: https://stackoverflow.com/a/19125599/2027839
String properHTML = html.replaceAll( "(&(?!amp;))", "&" );
if (properHTML.contains('<!DOCTYPE html'))
return properHTML;
return """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head></head>
<body>
${properHTML}
</body>
</html>
""";
}
}
I am tryna hit the KatalonAutoTesting@gmail.com
inbox for the messages/threads.
However, when I hit the SMDEmailUtils.ExtractSignUpLink()
, I face some 403 error, after sign-in from the OAuth Consent Screen, and the message is :
Delegation denied for [my_email]
I have added [my_email]
as a test user on the OAuth Consent Screen set-up Test Users step.
When I use "me"
in the place of this.GetMainEmail()
, it works but it accesses my email inbox instead of KatalonAutoTesting@gmail.com
.
What should I do to remedy this, and get this working?
NOTE: This was working, as written, up until I faced some invalid_grant
issue. I deleted the token, tried to re-create it, and I seem to be facing this issue, and it feels like I can do nothing about it...