GmailAPIを使ってKatalonStudioでメールを取得する

# はじめに
本記事では[こちら](https://forum.katalon.com/t/custom-keyword-to-receive-gmail/21381)でHarold_Owenさんが公開してくれているコードを元にカスタムキーワードを作成し、Google提供のGmaiAPIを使ってGmailのメールを取得・本文をログに表示するデモです。なるべく細かく書こうと思っていますが、わからない部分があればお知らせもらえると助かります。

# Google Workspaceでの作業
[Google Workspace APIのJava向けのクイックスタート](https://developers.google.com/gmail/api/quickstart/java?hl=ja)を確認しながらデスクトップ アプリケーションの認証情報を承認するところまで進めます。


## 新しいプロジェクトを作成
Google Cloud プロジェクトをまだ持っていない場合は作成します
https://console.cloud.google.com/projectcreate
プロジェクト名はなんでもいいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/cdbf2337-e30b-45fd-3c23-9a2e26e1a70c.png)

## API有効化
[クイックスタート](https://developers.google.com/gmail/api/quickstart/java?hl=ja#enable_the_api)の案内から「APIの有効化」をします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/3bd0d8b2-9432-670c-98ad-c7775c7d51d4.png)

用意したプロジェクトが選択されていることを確認して「次へ」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/edac70ce-bc86-fb71-ff3d-913cd433ede3.png)

「Gmail API」を有効にする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/a325268e-af57-ef5c-bdd0-952bdf0c6267.png)

## 認証情報の作成（同意画面の設定）
Google Cloud コンソールで、メニュー アイコン  > [API とサービス] > [認証情報] の順に選択します。
![スクリーンショット 2023-04-22 092639.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/0d5a59a0-d99f-65d5-7d19-0128a27cfd2b.png)

[認証情報を作成] > [OAuth クライアント ID] をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/58acad47-ef91-30ba-4d0e-a60b4aa3535d.png)
同意画面の設定へ遷移します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/2d1664e6-17b2-85f7-d258-f614e2d6ccdd.png)
今回使っているGoogleアカウントは組織はなく個人利用なので「外部」を選択しています。
使いたいほうを選択して「作成」します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/f46e332c-dc8d-4bce-8e21-cd46a3d9f940.png)

アプリ情報を入力します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/895c50b8-0022-160b-10e2-fc4ca07fece0.png)
任意のアプリ名、サポートメールアドレス、デベロッパーの連絡先メールアドレスを設定します。

スコープを追加します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/6106db9e-7251-19bd-abe7-b543ea147067.png)

GmailAPIで絞込をかけて[.../auth/gmail.readonly]を追加します
![スクリーンショット 2023-04-22 172045.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/4c376a1e-b145-4746-2687-60333305c91a.png)

テストユーザーの設定をします。「ADD USERS」から使用するメールアドレスを追加します。
![スクリーンショット 2023-04-22 172312.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/b05e9f7c-068a-8dab-254d-7a130330a007.png)

保存して次へ進み、入力情報を見直して「ダッシュボードに戻る」で完了します。

## 認証情報の作成（同意画面の設定後）
[認証情報を作成] > [OAuth クライアント ID] をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/58acad47-ef91-30ba-4d0e-a60b4aa3535d.png)

[アプリケーションの種類] > [デスクトップ アプリ] をクリックします。
[名前] フィールドに、認証情報の名前を入力します。この名前は Google Cloud コンソールにのみ表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/01eee5a0-9836-e7a3-4cd9-99b21ef50214.png)
[作成] をクリックします。

OAuth クライアントの作成画面が表示され、新しいクライアント ID とクライアント シークレットが表示されます。
[JSONをダウンロード]してから[OK] をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/2e2b8f02-ff7b-c2e9-80de-febd08b7b8ca.png)

新しく作成された認証情報が [OAuth 2.0 Client ID] に表示されます。


ダウンロードした JSON ファイルを [credentials.json]として保存します。
Katalonのプロジェクトにある[Data Files]ディレクトリに[gmail]ディレクトリを作成し[credentials.json]を配置します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/c56c204e-c8e1-9c7a-05fb-c14ba6987dc5.png)

以上でGoogle Workspaceでの作業は終わりです

# Katalon Studioでの作業
## Custom keyword（カスタムキーワード）の作成
Gmailの受信処理を各テストケースで使いますためにキーワードを作成します。
プロジェクトの[Keywords]から任意パッケージを右クリックし[new]>[keyword]で作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/0a6332f8-c2d5-da6c-ef40-3f1448c48499.png)

クラス名は今回[GmailUtil]としています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/666e262c-585f-065a-a665-3b22de707bf2.png)


```
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.gson.GsonFactory;
import com.google.common.io.BaseEncoding
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 String APPLICATION_NAME = "gmail-api-sample";
	private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
	private static final int MAX_WAIT_SEC = 60;

	private static final String USER = "me";


	/**
	 *  このクイックスタートで必要なスコープのGlobal instance
	 *  これらのスコープを変更する場合は、以前に保存したtokens/ folderを削除してください。
	 */
	private static final List<String> SCOPES = Collections.singletonList(GmailScopes.MAIL_GOOGLE_COM);

	/**
	 * 指定件名にヒットするメールの本文を取得する
	 * @param subjectPrefix メール件名
	 * @return　message　メール本文
	 */
	@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;
	}

	/**
	 * 取得したJSONデータから本文を抜き取る
	 */
	private String parseMessage(Message message) {

		System.out.println(message.toPrettyString());//デバッグ用
		// bodyの取得
        //String data = message.getPayload().getBody().getData();
		String data = message.getPayload().getParts().get(0).getBody().get("data").toString();

		if(data) {
			return new String(BaseEncoding.base64Url().decode(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");

	}
}

```


:::note warn
追記（2023.06.05）
本文を取り出す``String data = ``のところですが、残念ながらメールの種類によって本文の階層が異なるため失敗することがあるようでした。デバッグ用の``System.out.println(message.toPrettyString())``でデータの内容が見えるはずなので失敗した場合はデータに合わせて``String data = ``の処理を変更してください。
注意すべきは本文はエンコードされているため読める形式ではないことです。他の方がデータの例を合わせて説明してくれている[コチラ](https://qiita.com/hsn/items/5779fc8b41d4aecaeb83#%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E5%8F%96%E5%BE%97)の記事が参考になります。
:::

## 必要ライブラリの準備
このままだと上述で記載のコードはこのように依存関係のエラーになると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/bbd1ff76-54ae-a8eb-0916-3714136f5603.png)

依存関係を解決するためにgradleでライブラリを準備します。gradleの導入については[別記事](https://qiita.com/m-kinjo/items/e018b6b044c407d1d66c)で説明していますため、そちらを参照ください。

https://qiita.com/m-kinjo/items/e018b6b044c407d1d66c

### build.gradleへ記述
[build.gradle]にてdependencies部分に以下を追加します
```
dependencies {
  implementation 'com.google.api-client:google-api-client:2.0.0'
  implementation 'com.google.oauth-client:google-oauth-client-java6:1.34.1'
  implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
  implementation 'com.google.apis:google-api-services-gmail:v1-rev20230403-2.0.0'
}
```
Katalonの画面から見ると以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/080c8e9f-313a-484f-ee8d-7e2a44f38add.png)

### katalonCopyDependenciesの実行
一度Katalonを閉じてからコマンドプロンプトでプロジェクトへ移動`cd プロジェクトのパス`してから`gradle katalonCopyDependencies`を実行します。
以下のようにBUILD SUCCESSFULが表示されればOKです。
```
C:\Users\user>cd C:\Users\user\Katalon Studio\sample20230420

C:\Users\user\Katalon Studio\sample20230420>gradle katalonCopyDependencies
Starting a Gradle Daemon (subsequent builds will be faster)

BUILD SUCCESSFUL in 30s
1 actionable task: 1 executed
```

再度Katalonを開き、Keywordsの「GmailUtil.groovy」を確認するとimport ~の部分のエラー表示が消えていると思います。


## テストケースの作成
いよいよテストケースでカスタムキーワードを呼び出して使ってみます。
既存のテストケースへ記述してもいいですが、問題が起きた時に切り分けやすいように最初は新規テストケースの作成で試すことをお勧めします。

テストケースで必要な記述は以下の通りです。先に作成したCustomKeywordsを呼び出し、メールの件名を渡すことで本文を取得します。
```
String subject = 'ここにメールの件名をいれましょう'
String message = CustomKeywords.'GmailUtil.getEmailLink'(subject)

WebUI.comment(message)
```
今回は取得したメッセージをWebUI.comment()で出力していますが、テキストに書き出すなり用意した文言と比較するなり使い方はお任せします。

## 実行
実行すると初回はブラウザが開きアクセス許可が求められますので認証処理を行う必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/918049da-6c5b-90bb-6ed3-bf718568bf7c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/19e238db-b2e4-3037-aeab-9818f530d066.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/0610ab9a-23d4-33f4-0c2f-5c87f210bde8.png)

許可ができた場合に以下の画面が表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/cef158ff-a0dd-d3b9-21ff-bd615fce7ee5.png)

ブラウザから許可したことで、プロジェクトのData Filesディレクトリにトークン情報(tokens)が保管されるため、二回目以降はブラウザからの許可は不要になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/3ca059c0-0389-7e09-d6f4-9f13e6aca017.png)

:::note warn
注意
Gitを使っている場合、作成した[ tokens ]ディレクトリ内は[ .gitignore ]に追加するなどして共有で使わないことをお勧めします。

tokensディレクトリ内をGitに入れない場合以下追加
`Data Files/gmail/tokens/`
gmailディレクトリ内からGitに入れない場合以下追加
`Data Files/gmail/`
:::

実行完了すると以下のようにメール本文を確認できます
※NoSuchMethodErrorがある場合は[コチラの記事](https://qiita.com/m-kinjo/items/ab20fb310c8805386569)を参照
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1058523/83d67831-bf34-715f-7b4f-829d39a9a0ae.png)

# お疲れさまでした
お疲れさまでした！！！