Custom Keyword to receive Gmail

We have a test case that needs to receive an email that was generated by our site. We accomplished this using the Gmail Java API described in this tutorial.

https://developers.google.com/gmail/api/quickstart/java

Ignore the gradle prerequisite in the tutorial. Do Step 1 as described in the tutorial to create your credentials. Instead of Steps 2 and 3, run your Katalon test using the custom keyword, below. The first time you run it, it will interactively ask you to confirm the permissions tokens by logging onto Gmail and confirming a series of pages. After that, your test will run without being interactive.

We stored the Gmail credentials and permission tokens under the Katalon project Data files:

/Data Files/gmail/credentials.json
/Data Files/gmail/tokens

Below are our custom keywords for clearing out the Inbox at the start of the test, and for checking for an email by subject line.

Note that our logic waits up to 60 seconds for the mail to arrive, checking at 10 second intervals. So far it has been verify effective and reliable.

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64
import com.google.api.client.repackaged.org.apache.commons.codec.binary.StringUtils
import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.api.services.gmail.Gmail;
import com.google.api.services.gmail.GmailScopes;
import com.google.api.services.gmail.model.ListMessagesResponse;
import com.google.api.services.gmail.model.Message;
import com.google.api.services.gmail.model.MessagePartHeader
import com.kms.katalon.core.annotation.Keyword
import com.kms.katalon.core.configuration.RunConfiguration
import com.kms.katalon.core.util.KeywordUtil
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI

public class GmailUtil {

	private static final int MAX_WAIT_SEC = 60;
	private static final String APPLICATION_NAME = "Gmail API Java Quickstart";
	private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();

	private static final String USER = "me";


	/**
	 * 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 = Collections.singletonList(GmailScopes.MAIL_GOOGLE_COM);

	@Keyword
	public void deleteAllEmail() {

		Gmail service = logonToGmail();
		ListMessagesResponse listResponse = service.users().messages().list(USER).execute();
		List<Message> messages = listResponse.getMessages();
		for (Message message in messages) {

			service.users().messages().delete(USER, message.getId()).execute();
		}
	}

	@Keyword
	public String getEmailLink(String subjectPrefix) {

		Gmail service = logonToGmail();
		Message message = getFirstRelevantMessage(service, subjectPrefix);
		return parseMessage(message);
	}

	private Gmail logonToGmail() {

		// Build a new authorized API client service.
		final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
		return new Gmail.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT))
				.setApplicationName(APPLICATION_NAME)
				.build();
	}

	private Message getFirstRelevantMessage(Gmail service, String subjectPrefix) {

		int waitSec = 10;
		int totalWaitSec = 0;
		while(totalWaitSec < MAX_WAIT_SEC) {

			ListMessagesResponse listResponse = service.users().messages().list(USER).execute();
			List<Message> messages = listResponse.getMessages();
			if (messages) {

				for(Message message in messages) {

					message = service.users().messages().get(USER, message.getId()).execute();

					if(isRelevant(service, message, subjectPrefix)) {

						return message;
					}
				}
			}
			println String.format('No relevant Gmail found, checking again after %d seconds...', waitSec)
			WebUI.delay(waitSec);
			totalWaitSec += waitSec;
		}

		KeywordUtil.markErrorAndStop(String.format('No Gmail found after %d seconds with subject %s', totalWaitSec, subjectPrefix))
	}

	private boolean isRelevant(Gmail service, Message message, String subjectPrefix) {

		List<MessagePartHeader> headers = message.getPayload().getHeaders()

		for(MessagePartHeader header in headers) {

			if('Subject'.equals(header.getName())) {

				String subject = header.getValue();
				if(subject && subject.startsWith(subjectPrefix)) {
					return true;
				}
			}
		}
		return false;
	}

	private String parseMessage(Message message) {

		String data = message.getPayload().getBody().getData();

		if(data) {
			return StringUtils.newStringUtf8(Base64.decodeBase64(data));
		}

		KeywordUtil.markErrorAndStop(String.format('Gmail no body data, message ID %s'), message.getId())
	}

	private Credential getCredentials(NetHttpTransport HTTP_TRANSPORT) {

		'CAUTION: If you change user scopes, delete the previously saved tokens folder.'
		String tokensPath = RunConfiguration.getProjectDir() + '/Data Files/gmail/tokens'
		String credentialsPath = RunConfiguration.getProjectDir() + '/Data Files/gmail/credentials.json'
		InputStream input = new FileInputStream(new File(credentialsPath))

		GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(input));

		// Build flow and trigger user authorization request.
		GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
				HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
				.setDataStoreFactory(new FileDataStoreFactory(new java.io.File(tokensPath)))
				.setAccessType("offline")
				.build();
		LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build();
		return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");

	}
}
3 Likes

Good stuff Harold!

(Now a post in Tips&Tricks that provides a link to this thread and you can consider your work done for the day :sunglasses:)

1 Like

Thank you @Harold_Owen @Russ_Thomas.

I think we can move this post to Tips and Tricks if Harold doesn’t mind.

Moderator! :grin:

True. But it is also a kind-of integration…

1 Like

hello,

what about extract attachment, i guess it’s not handled in this class

Thanks, @Timo_Kuisma for the question. Extracting attachments is not part of our test, but I suspect it is supported by the Gmail Java API. I used stackoverflow.com to find many examples of interacting with Gmail. Good luck.

hi,

my class did it, but now i have issue to log in gmail, maybe google add some new policies to gmail
org.codehaus.groovy.runtime.InvokerInvocationException: javax.mail.AuthenticationFailedException: [AUTHENTICATIONFAILED] Invalid credentials (Failure)

Our test runs several times a day, so I don’t think Gmail is broken. I would check the location of the credentials file.

Also, this bit in my code is supposed to be litterally “me”, NOT your gmail account name. Don’t want anyone to think they need to update the value.

hi,
I resolved the issue
port was changed to 465 was 587

my class 
1.execute testcase and then will send reports to gmail as .zip packet.
2. extract the .zip packet from the gmail and save it to the folder
3. unpack .zip to another folder
4. open html file with browser (test report seen)
1 Like

Hi Harold,

I think I’m missing a step. When I try to run this, I’m getting some errors about some packages:

Example:

Groovy:unable to resolve class com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow```

How do I go about resolving that?

I am running into an issue with file permission using a windows machine.

'com.google.api.client.util.store.FileDataStoreFactory setPermissionsToOwnerOnly

WARNING: unable to change permissions for everybody:’

Anyone else run into this? I can delete mails but can read the content of them.

hi,

is all needed .jar files in your path?
check this
https://developers.google.com/api-client-library/java/apis/oauth2/v2

I added all the JARS in this attachment, still no luck.

there’s another error but i dont know if it’s relevant: java.util.MissingFormatArgumentException: Format specifier ‘%s’

I am able to delete all emails but can’t read them by subject line

Thanks for the help, Tullah. Any idea where to find these?

hello,

this is an syntax error, check you code

something more,

if this not help you, then cp all code to JIDEA (java editor free) and create maven project there.
with maven you are able to get all needed classes adding dependencies to pom file
build artifact will give to you .jar file, add this .jar to your Katalon Drivers folder, restart Katalon,
import package.class to your Katalon testcase or keyword class

if needed more support how to do that, let me hear

Got it working. Had to comment out the error handling:

hello,

I played some of my time to implementing gmailUtil, not plugin, sorry only .jar :slight_smile:

import static com.kms.katalon.core.checkpoint.CheckpointFactory.findCheckpoint
import static com.kms.katalon.core.testcase.TestCaseFactory.findTestCase
import static com.kms.katalon.core.testdata.TestDataFactory.findTestData
import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject
import com.kms.katalon.core.checkpoint.Checkpoint as Checkpoint
import com.kms.katalon.core.cucumber.keyword.CucumberBuiltinKeywords as CucumberKW
import com.kms.katalon.core.mobile.keyword.MobileBuiltInKeywords as Mobile
import com.kms.katalon.core.model.FailureHandling as FailureHandling
import com.kms.katalon.core.testcase.TestCase as TestCase
import com.kms.katalon.core.testdata.TestData as TestData
import com.kms.katalon.core.testobject.TestObject as TestObject
import com.kms.katalon.core.webservice.keyword.WSBuiltInKeywords as WS
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
import internal.GlobalVariable as GlobalVariable

import mail.gmail.util.GmailUtil
import org.joda.time.DateTime

import java.io.File
import java.io.InputStream
import java.net.URI
import java.text.DateFormat
import java.text.SimpleDateFormat
//import java.util.*
import java.util.logging.Logger

import java.util.List
import java.util.regex.Matcher
import java.util.regex.Pattern

Logger LOGGER = Logger.getLogger("InfoLogging");

		Properties prop = new Properties();
		InputStream input = new FileInputStream("C:\\KatalonStudio\\KatalonProject\\Include\\PROPERTIES\\config.properties")
		if (input == null) {
			LOGGER.info("Sorry, unable to find config.properties");
			return;
		}
		prop.load(input);
		String user = prop.getProperty("user");
		String password = prop.getProperty("password");

		GmailUtil util = new GmailUtil();

		List<String> unreadEmails = new ArrayList<>();
		List<String> lastSent = new ArrayList<>();
		List<String> allEmails = new ArrayList<>();
		List<String> emailBySubject = new ArrayList<>();
		List<String> emailByTimeStamp = new ArrayList<>();
		List<String> searchByKeyword = new ArrayList<>();
		List<String> searchByDate = new ArrayList<>();
		List<String> deletedByKeyword = new ArrayList<>();

           //send email with or without attachement (password, username, sender, receiver, 
             subject, body, attachement [if no then ""])
          String path = "C:/Users/xxxxx/Desktop/banners.jpg"
          SendEmail mailSender;
         mailSender = new SendEmail(password,user, user,"Hello Katalon","Testing with gmail sender","");
         mailSender = new SendEmail(password,user, user,"Hello Katalon with 
         attachement","Gmail with attachement",path);

		//will delete email(s) by given search keyword (subject)
		deletedByKeyword = util.deleteMails(user, password, "Katalon");
		LOGGER.info("Deleted email(s) by keyword: " + deletedByKeyword);

		//will delete email(s) by given search keyword (from)
		GmailUtil util3 = new GmailUtil(user);
		deletedByKeyword = util3.deleteMailsByFrom(user, password);
		LOGGER.info("Deleted email(s) by from: " + deletedByKeyword);

		//will return all unread email from INBOX and set flag to SEEN
		unreadEmails = util.getUnreadGmails(user,password);
		LOGGER.info("latest unread message(s) contents: " + unreadEmails);

		//will return last sent email from the INBOX
		lastSent = util.getLatestSentEmail(user,password, "[Gmail]/Lähetetyt viestit");//in English [Gmail]/Sent Mail
		LOGGER.info("last sent message contents: " + lastSent);

		//will return email search criteria by timestamp as "2019-05-10 20:42"
		emailByTimeStamp = util.getEmailByTimeStamp(user,password, "2018-11-13 06:13");
		LOGGER.info("search by timestamp contents: " + emailByTimeStamp);

		String keyword = "Metallican";
		//will return email with search criteria where subject contains keyword and will get first occurs of http tag and open it in browser
		searchByKeyword = util.searchEmailByKeyword(user, password, keyword);

		Pattern urlPattern = Pattern.compile(".*http://.*",Pattern.CASE_INSENSITIVE);

		String urlString = searchByKeyword.get(0);

		Matcher matcher = urlPattern.matcher(urlString);

		while (matcher.find()) {
			String address = matcher.group();

			LOGGER.info("Got URL: " + address);

			Runtime rt = Runtime.getRuntime();
			String url = address;
			rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
			break;
			//WebUI.openBrowser(address)
		}

		//will return all email(s) from INBOX sent by day(s) ago
		Date d = new Date();
		Date daysAgo = new DateTime(d).minusDays(1).toDate();
		GmailUtil util1 = new GmailUtil(daysAgo);
		searchByDate = util1.searchEmailByDate(user, password);
		for (int i = 0; i < searchByDate.size(); i++){
			LOGGER.info("message "+i+" contents search by date: " + searchByDate.get(i));
		}