
Gmail: メール取得
この工程は、Gmail のメールを取得します。
Basic Configs
- 工程名
- メモ
Configs for this Auto Step
- conf_auth
- C1: OAuth2 設定 *
- conf_messageId
- C2: メール ID *
- conf_fromAddress
- C3-A: From メールアドレスを保存するデータ項目
- conf_fromName
- C3-N: From 表示名を保存するデータ項目
- conf_replyToAddresses
- C4-A: Reply-To メールアドレス一覧を保存するデータ項目
- conf_replyToNames
- C4-N: Reply-To 表示名一覧を保存するデータ項目
- conf_recipientAddresses
- C5-A: To/Cc/Bcc メールアドレス一覧を保存するデータ項目
- conf_recipientNames
- C5-N: To/Cc/Bcc 表示名一覧を保存するデータ項目
- conf_sentDatetime
- C6: 送信日時を保存するデータ項目
- conf_subject
- C7: 件名を保存するデータ項目
- conf_body
- C8: 本文を保存するデータ項目
- conf_attachments
- C9: 添付ファイルを保存するデータ項目
Notes
- 本モデリング要素は Google Workspace アカウント用です
- 一般ユーザー向けアカウント(gmail.com)での利用はできません
- 事前準備として、Google Workspace の管理者が対象の OAuth アプリを信頼できるアプリとして設定済みである必要があります
- この設定がなされていない状態での利用はできません
- 本モデリング要素が使用する OAuth アプリのクライアント ID は 13039123046-t87nmrj499ffoa58asehks3asajvgqnh.apps.googleusercontent.com です
- メール ID は Gmail 上では確認できません
- 「開始: Gmail: メール受信時」で取得したメール ID を使用してください
Capture

See also
Script (click to open)
- 次のスクリプトが記述されている XML ファイルをダウンロードできます
- gmail-message-get.xml (C) Questetra, Inc. (MIT License)
- Professional のワークフロー基盤では、ファイル内容を改変しオリジナルのアドオン自動工程として活用できます
// OAuth2 config sample at [OAuth 2.0 Setting]
// - Authorization Endpoint URL: https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force
// - Token Endpoint URL: https://oauth2.googleapis.com/token
// - Scope: https://www.googleapis.com/auth/gmail.readonly
// - Consumer Key: (Get by Google Developer Console)
// - Consumer Secret: (Get by Google Developer Console)
function main(){
//// == 工程コンフィグ・ワークフローデータの参照 / Config & Data Retrieving ==
const auth = configs.getObject("conf_auth");
const messageId = engine.findData( configs.getObject("conf_messageId") );
const defs = retrieveDefs();
//// == 演算 / Calculating ==
const hasNoDefs = Object.values(defs).every( def => def === null );
if ( hasNoDefs ) return; // 保存先データ項目が1つも設定されていなければ何もせず正常終了
const apiUri = determineApiUri( messageId );
const message = getMessage( apiUri, auth );
const files = new java.util.ArrayList(); // もともと添付されていたファイルはクリアする
if ( defs.attachmentsDef !== null ) {
getAttachments( apiUri, auth, message.attachments );
convertAndAddAttachments( message.attachments, files );
}
// To, Cc, Bcc の順に連結
const recipientEmails = message.recipients.to.emails
.concat( message.recipients.cc.emails, message.recipients.bcc.emails );
const recipientNames = message.recipients.to.names
.concat( message.recipients.cc.names, message.recipients.bcc.names );
//// == ワークフローデータへの代入 / Data Updating ==
setDataIfNotNull( defs.fromAddressDef, message.from.email );
setDataIfNotNull( defs.fromNameDef, message.from.name );
setDataIfNotNull( defs.replyToAddressesDef, message.replyTo.emails.join('\n') );
setDataIfNotNull( defs.replyToNamesDef, message.replyTo.names.join('\n') );
setDataIfNotNull( defs.recipientAddressesDef, recipientEmails.join('\n') );
setDataIfNotNull( defs.recipientNamesDef, recipientNames.join('\n') );
setDataIfNotNull( defs.sentDatetimeDef, new java.sql.Timestamp(Date.parse(message.datetime)) );
setDataIfNotNull( defs.subjectDef, message.subject );
setDataIfNotNull( defs.bodyDef, message.body );
setDataIfNotNull( defs.attachmentsDef, files );
}
/**
* config から保存先データ項目の ProcessDataDefinitionView を読み出す
* 値を保存するデータ項目が重複して設定されている場合はエラーとする
* @return {Object} defs 保存先データ項目の ProcessDataDefinitionView を格納した JSON オブジェクト
*/
function retrieveDefs() {
const items = ["fromAddress", "fromName", "replyToAddresses", "replyToNames",
"recipientAddresses", "recipientNames", "sentDatetime", "subject", "body", "attachments"];
const defs = {};
const dataItemNumSet = new Set(); // データ項目の重複確認用
items.forEach( item => {
const dataItemDef = configs.getObject(`conf_${item}`);
if ( dataItemDef !== null ) { // データ項目が設定されている場合は重複を確認する
const dataItemNum = dataItemDef.getNumber();
if ( dataItemNumSet.has( dataItemNum ) ) { // データ項目番号が重複していればエラー
throw new Error("The same data item is set multiple times.");
}
dataItemNumSet.add( dataItemNum ); // データ項目の重複確認用
}
defs[`${item}Def`] = dataItemDef;
});
return defs;
}
/**
* Gmail のメール取得の URI を決定する
* メール ID が空であればエラーとする
* @param {String} messageId メール ID
* @return {String} apiUri API の URI
*/
function determineApiUri( messageId ) {
if ( messageId === "" || messageId === null ) {
throw new Error("Message ID is empty.");
}
const apiUri = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${encodeURI(messageId)}`;
return apiUri;
}
/**
* Gmail REST API にメール取得の GET リクエストを送信し、必要な情報を JSON オブジェクトに格納する
* @param {String} apiUri API の URI
* @param {String} auth 認証設定名
* @return {Object} message メール情報を格納した JSON オブジェクト
*/
function getMessage( apiUri, auth ) {
const response = httpClient.begin()
.authSetting( auth )
.get( apiUri );
// when error thrown
const responseJson = response.getResponseAsString();
const status = response.getStatusCode();
if (status >= 300) {
engine.log(`API URI: ${apiUri}`);
const accessLog = `---GET request--- ${status}\n${responseJson}\n`;
engine.log( accessLog );
throw new Error(`Failed to get message. status: ${status}`);
}
// when successful, parse the message content
const json = JSON.parse(responseJson);
const message = extractFromPayload( json.payload );
return message;
}
/**
* Gmail REST API のメール取得のレスポンスの payload フィールドから
* 必要な情報を抜き出し、JSON オブジェクトに格納する
* @param {Object} payload Gmail REST API のメール取得のレスポンスの payload フィールド
* @return {Object} message メール情報を格納した JSON オブジェクト
*/
function extractFromPayload( payload ) {
const message = {
"from": {
"email": "",
"name": ""
},
"replyTo": {
"emails": [],
"names": []
},
"recipients": {
"to": {
"emails": [],
"names": []
},
"cc": {
"emails": [],
"names": []
},
"bcc": {
"emails": [],
"names": []
}
},
"datetime": "",
"subject": "",
"body": "",
"attachments": []
};
// ヘッダの処理
// From ヘッダは Gmail の仕様で必ず 1 件, Date ヘッダも 1 件
// To, Cc, Bcc は複数件の可能性があるので配列に追加して処理
// Subject が複数件ある場合は最初の Subject ヘッダを採用する
payload.headers.forEach( header => {
const headerNameLower = header.name.toLowerCase();
switch ( headerNameLower ) {
case 'from':
const fromAddresses = emailService.parseAddressHeader( header.value ); // java.util.List<EmailServiceWrapper.InternetAddressWrapper>
if ( fromAddresses !== null && fromAddresses.length !== 0 ) {
message.from.email = fromAddresses[0].getAddress();
message.from.name = fromAddresses[0].getPersonal();
}
break;
case 'reply-to':
const replyToAddresses = emailService.parseAddressHeader( header.value ); // java.util.List<EmailServiceWrapper.InternetAddressWrapper>
if ( replyToAddresses !== null && replyToAddresses.length !== 0 ) {
replyToAddresses.forEach( addr => {
message.replyTo.emails.push( addr.getAddress() );
message.replyTo.names.push( addr.getPersonal() );
});
}
break;
case 'to':
case 'cc':
case 'bcc':
const addresses = emailService.parseAddressHeader( header.value ); // java.util.List<EmailServiceWrapper.InternetAddressWrapper>
if ( addresses !== null && addresses.length !== 0 ) {
addresses.forEach( addr => {
message.recipients[headerNameLower].emails.push( addr.getAddress() );
message.recipients[headerNameLower].names.push( addr.getPersonal() );
});
}
break;
case 'date':
message.datetime = header.value;
break;
case 'subject':
if ( message.subject === "" ) { // 最初の Subject ヘッダを採用する
message.subject = header.value;
}
break;
}
});
// パート(本文、添付ファイル)の処理
const mimeType = payload.mimeType;
if ( mimeType === "text/plain" || mimeType === "text/html" ) { // 本文のみのメール
message.body = parseTextPart( payload );
} else if ( mimeType.startsWith("multipart/") ) { // マルチパートのメール
const bodyParts = [];
parseMultiPart( payload, bodyParts, message.attachments );
if ( bodyParts.length === 0 ) {
engine.log("No body part was found.");
message.body = "";
} else {
message.body = bodyParts.map( p => parseTextPart(p) ).join('\n'); // Gmail の UI では <wbr> で区切られている
}
} else {
throw new Error(`Unsupported MIME type. ${mimeType}`);
}
return message;
}
/**
* MIME タイプが text/plain, text/html のパートのデータをデコードして文字列として返す
* @param {Object} part MIME メールのパート(text/plain または text/html)
* @return {String} body パートのデータ(メール本文として保存する文字列)
*/
function parseTextPart( part ) {
const bodyData = part.body.data ?? '';
return base64.decodeFromUrlSafeString( bodyData ); // UTF-8 としてデコード
}
/**
* MIME タイプが multipart/* のパートを再帰的に解析する
* 本文パートは bodyParts に、添付ファイルは attachments に格納する
* @param {Object} part MIME メールのパート(multipart/*)
* @param {Array<Object>} bodyParts 本文パートを格納する配列
* @param {Array<Object>} attachments 添付ファイルの情報を格納する配列
*/
function parseMultiPart( part, bodyParts, attachments ) {
engine.log(`Multipart found. partId: ${part.partId}, mimeType: ${part.mimeType}`);
if (part.parts === undefined || part.parts.length === 0) {
engine.log(`No inner-part was found in this multipart. partId: ${part.partId}`);
return;
}
if ( part.mimeType === 'multipart/alternative' ) {
parseMultiPartAlternative( part, bodyParts, attachments );
return;
}
if ( part.mimeType === 'multipart/related' ) {
parseMultiPartRelated( part, bodyParts, attachments );
return;
}
// multipart/mixed およびその他の multipart/* はデフォルト処理
parseMultiPartMixed( part, bodyParts, attachments );
}
/**
* multipart/alternative を解析する
* 各パートは同じ内容の異なる表現であるため、採用した1グループだけを bodyParts に push する。
* 優先順位: text/plain > text/html(multipart 内から取得した本文も含む)
* Gmail の UI とは異なり、できるだけ簡易な本文を採用したいので、先頭から走査していく。
* 本文パートとして採用されうるのは、明示的に添付ファイルでないパートのみ。
* 本文パートとして採用されなかったパートは基本的には無視されるが、
* 以下のパートは、例外的に添付ファイルとして保存される。(Gmail の仕様と揃えている)
* - multipart/alternative 内でさらに multipart/* がある場合、その multipart で添付ファイルと判定されたパート
* - ファイル名が設定されているパート
* @param {Object} part
* @param {Array<Object>} bodyParts
* @param {Array<Object>} attachments
*/
function parseMultiPartAlternative( part, bodyParts, attachments ) {
let textParts = []; // text/plain のパート群
let htmlParts = []; // text/html のパート群
part.parts.forEach( innerPart => {
if ( innerPart.mimeType.startsWith('multipart/') ) {
const nestedBodyParts = [];
parseMultiPart( innerPart, nestedBodyParts, attachments ); // 添付ファイルが追加されうる
if ( nestedBodyParts.length === 0 ) {
return;
}
if ( textParts.length === 0 && nestedBodyParts.every( p => p.mimeType === 'text/plain' )) {
nestedBodyParts.forEach( p => outputLogBodyCandidatePart(p) );
textParts = nestedBodyParts;
} else if ( htmlParts.length === 0 ) {
nestedBodyParts.forEach( p => outputLogBodyCandidatePart(p) );
htmlParts = nestedBodyParts;
} else {
nestedBodyParts.forEach( p => outputLogIgnoredPart(p) );
}
return;
}
if ( !isObviouslyAttachment(innerPart) && innerPart.mimeType === 'text/html' ) {
if ( htmlParts.length === 0 ) {
outputLogBodyCandidatePart( innerPart );
htmlParts = [innerPart];
} else {
outputLogIgnoredPart( innerPart );
}
return;
}
if ( !isObviouslyAttachment(innerPart) && innerPart.mimeType === 'text/plain' ) {
if ( textParts.length === 0 ) {
outputLogBodyCandidatePart( innerPart );
textParts = [innerPart];
} else {
outputLogIgnoredPart( innerPart );
}
return;
}
// alternative 直下のパートは、ファイル名がある場合のみ添付ファイルとして扱う
if ( !isFileNameBlank( innerPart ) ) {
addPartAsAttachment( innerPart, attachments );
return;
}
// それ以外のパートは、ログだけ残して無視
outputLogIgnoredPart( innerPart );
});
// 本文パートを選択
// 優先順位: text/plain > text/html
// 採用グループの全パートを push し、落選グループはログに残す
const selected = textParts.length > 0 ? textParts : htmlParts;
const discarded = textParts.length > 0 ? htmlParts : [];
if ( selected.length === 0 ) {
engine.log(`No body part was found in multipart/alternative. partId: ${part.partId}`);
return;
}
bodyParts.push( ...selected );
discarded.forEach( p => outputLogIgnoredPart(p) );
}
/**
* multipart/related を解析する
* 本文パートを 1 件だけ選択し、それ以外の直下のパートは添付ファイルとして扱う(ファイル名がなければ noname)
* 本文パート候補は上から順に探す:
* - 明示的に添付ファイルでない text/plain または text/html → そのパートが本文
* - 本文未確定の段階で multipart が現れた場合 → その multipart 自身の本文選択ロジックで解析し本文と添付ファイルを追加
* - 本文確定後の multipart → 中の全パートを再帰的に添付ファイルとして処理
* @param {Object} part
* @param {Array<Object>} bodyParts
* @param {Array<Object>} attachments
*/
function parseMultiPartRelated( part, bodyParts, attachments ) {
let bodyFound = false;
part.parts.forEach( innerPart => {
if ( bodyFound ) { // すでに本文パートが見つかっている場合
if ( innerPart.mimeType.startsWith('multipart/') ) {
addAllPartsAsAttachments( innerPart, attachments );
} else {
addPartAsAttachment( innerPart, attachments );
}
return;
}
// まだ本文パートが見つかっていない場合
if ( innerPart.mimeType.startsWith('multipart/') ) {
const tempBodyParts = [];
parseMultiPart( innerPart, tempBodyParts, attachments );
if ( tempBodyParts.length > 0 ) {
bodyParts.push( ...tempBodyParts );
bodyFound = true;
}
return;
}
if ( !isObviouslyAttachment(innerPart) && ( innerPart.mimeType === 'text/plain' || innerPart.mimeType === 'text/html' ) ) {
bodyParts.push( innerPart );
bodyFound = true;
return;
}
addPartAsAttachment( innerPart, attachments );
});
if ( !bodyFound ) {
engine.log(`No body part was found in multipart/related. partId: ${part.partId}`);
}
}
/** 保存時のファイル名の最大長 */
const FILE_NAME_MAX_LENGTH = 200;
/**
* パートを添付ファイルとして attachments に追加する
* ファイル名が設定されていなければ noname を使う
* ファイル名が長すぎる場合は省略する
* @param {Object} part MIME メールのパート
* @param {Array<Object>} attachments 添付ファイルの情報を格納する配列
*/
function addPartAsAttachment( part, attachments ) {
let filename = isFileNameBlank( part ) ? 'noname' : part.filename;
if ( filename.length > FILE_NAME_MAX_LENGTH ) {
filename = filename.substring(0, FILE_NAME_MAX_LENGTH - 3) + "...";
}
attachments.push({
filename: filename,
contentType: getContentType( part ),
body: part.body
});
}
/**
* multipart の中のすべてのパートを再帰的に添付ファイルとして処理する(ファイル名がなければ noname)
* multipart/related の本文確定後に現れた multipart の処理に使う
* @param {Object} part multipart/* のパート
* @param {Array<Object>} attachments 添付ファイルの情報を格納する配列
*/
function addAllPartsAsAttachments( part, attachments ) {
if ( part.parts === undefined || part.parts.length === 0 ) {
return;
}
part.parts.forEach( innerPart => {
if ( innerPart.mimeType.startsWith('multipart/') ) {
addAllPartsAsAttachments( innerPart, attachments );
} else {
addPartAsAttachment( innerPart, attachments );
}
});
}
/**
* multipart/mixed およびその他の multipart/* を解析する
* 本文パート(text/plain, text/html)は全て順番に bodyParts に push する
* 添付ファイルは attachments に push する
* @param {Object} part
* @param {Array<Object>} bodyParts
* @param {Array<Object>} attachments
*/
function parseMultiPartMixed( part, bodyParts, attachments ) {
part.parts.forEach( innerPart => {
if ( innerPart.mimeType.startsWith('multipart/') ) {
parseMultiPart( innerPart, bodyParts, attachments );
return;
}
if ( !isObviouslyAttachment(innerPart) && ( innerPart.mimeType === 'text/plain' || innerPart.mimeType === 'text/html' ) ) {
bodyParts.push( innerPart );
return;
}
// 添付ファイルとして処理
addPartAsAttachment( innerPart, attachments );
});
}
/**
* multipart/alternative パース時の本文パート候補の情報をプロセスログに出力する
* @param {Object} part MIME メールのパート
*/
function outputLogBodyCandidatePart(part) {
const disposition = getDisposition(part);
engine.log(`Part ${part.partId} may be a body part. Content-Disposition: ${disposition}, mimeType: ${part.mimeType}`);
}
/**
* 保存しないパートの情報をプロセスログに出力する
* @param {Object} part MIME メールのパート
*/
function outputLogIgnoredPart(part) {
const partInfo = JSON.parse(JSON.stringify(part)); // ディープコピー
const originalData = part.body.data ?? '';
let extractedData = originalData.substring(0, 20); // ログ出力用に先頭 20 文字を抜粋
if (originalData.length > 20) {
extractedData += '...';
}
partInfo.body.data = extractedData;
engine.log(`Part ${part.partId} was ignored.`);
engine.log(JSON.stringify(partInfo));
}
/**
* パートが明らかに添付ファイルかどうかを判定する
* 以下のいずれかに該当すれば true を返す
* - Content-Disposition ヘッダが "attachment"
* - body.attachmentId がある
* - ファイル名が設定されている
* @param {Object} part MIME メールのパート
* @return {Boolean} パートが明らかに添付ファイルかどうか
*/
function isObviouslyAttachment(part) {
const disposition = getDisposition(part);
if (disposition === "attachment" || part.body.attachmentId !== undefined || !isFileNameBlank(part)) {
return true;
}
return false;
}
/**
* パートのファイル名が空かどうかを判定する
* @param {Object} part MIME メールのパート
* @return {Boolean} ファイル名が空かどうか
*/
function isFileNameBlank(part) {
return part.filename === undefined || part.filename === null || part.filename === "";
}
/**
* MIME メールのパートの "Content-Disposition" ヘッダの値を返す
* @param {Object} part パート
* @return {String} disposition "Content-Disposition" ヘッダの値(ヘッダが無い場合は null)
*/
function getDisposition( part ) {
if ( part.headers === undefined ) { // パートにヘッダが無い場合は null を返す
return null;
}
const dispositionHeader = part.headers.find( header => header.name.toLowerCase() === "content-disposition" );
if ( dispositionHeader === undefined ) { // 無い場合は null を返す
return null;
}
return dispositionHeader.value.split(";")[0].toLowerCase();
}
/** 添付ファイルの Content-Type のデフォルト値 */
const DEFAULT_FILE_CONTENT_TYPE = "application/octet-stream";
/**
* MIME メールのパートの "Content-Type" ヘッダの値を返す
* ヘッダが無い場合はデフォルト値として "application/octet-stream" を返す
* @param {Object} part パート
* @return {String} contentType "Content-Type" ヘッダの値
*/
function getContentType( part ) {
if ( part.headers === undefined ) { // パートにヘッダが無い場合はデフォルト値を返す
return DEFAULT_FILE_CONTENT_TYPE;
}
const contentTypeHeader = part.headers.find( header => header.name.toLowerCase() === "content-type" );
if ( contentTypeHeader === undefined ) { // 無い場合はデフォルト値を返す
return DEFAULT_FILE_CONTENT_TYPE;
}
return contentTypeHeader.value;
}
/**
* 添付ファイルの情報に本体データが含まれているかどうかを調べ、
* ない場合は Gmail REST API に添付ファイル取得の GET リクエストを送信し、
* 本体データを追加する
* @param {String} apiUri API の URI(/messages/{messageId} まで)
* @param {AuthSettingWrapper} auth OAuth2 認証設定
* @param {Array<Object>} attachments 添付ファイルの情報(オブジェクト)が格納された配列
* 配列内の添付ファイルの情報の形式は:
* {
* "filename": String,
* "contentType": String,
* "body": {
* "data": String(Base64) or "attachmentId": String
* }
* }
*/
function getAttachments( apiUri, auth, attachments ) {
const httpLimit = httpClient.getRequestingLimit();
let httpCount = 1; // HTTP リクエストの上限超えチェック用のカウンタ(メール取得のリクエスト後なので初期値は1)
attachments.forEach( attachment => {
if ( attachment.body.data !== undefined ) { // data があれば何もしない
return;
}
if ( attachment.body.attachmentId === undefined ) {
attachment.body["data"] = ""; // data も attachmentId も無い場合は空文字列をセット
return;
}
httpCount++;
if ( httpCount > httpLimit ) {
throw new Error("Number of HTTP requests is over the limit.");
}
const response = httpClient.begin()
.authSetting( auth )
.get( `${apiUri}/attachments/${attachment.body.attachmentId}` );
const responseJson = response.getResponseAsString();
const status = response.getStatusCode();
if (status >= 300) { // when error thrown
engine.log(`API URI: ${apiUri}/attachments/${attachment.body.attachmentId}`);
const accessLog = `---GET request--- ${status}\n${responseJson}\n`;
engine.log( accessLog );
throw new Error(`Failed to get attachment. status: ${status}`);
}
// when successful, parse the message content
const json = JSON.parse(responseJson);
attachment.body["data"] = json.data;
});
}
/**
* 添付ファイルを Qfile に変換し、ファイルの配列に追加する
* Content-Type が不正な場合、デフォルトの Content-Type で保存する
* @param {Array<Object>} attachments 添付ファイルの情報(オブジェクト)が格納された配列
* @param {ListArray<Qfile>} files ファイルの配列
*/
function convertAndAddAttachments( attachments, files ) {
attachments.forEach( attachment => {
let qfile;
const bytes = base64.decodeFromUrlSafeStringToByteArray( attachment.body.data ); // ByteArrayWrapper
try {
qfile = new com.questetra.bpms.core.event.scripttask.NewQfile(
attachment.filename,
attachment.contentType,
bytes
);
} catch (e) {
engine.log(`Failed to convert attachment ${attachment.filename} to qfile. ${e.toString()}`);
engine.log(`Trying to save it as ${DEFAULT_FILE_CONTENT_TYPE}.`);
// Content-Type が不正な場合に例外になるので、DEFAULT_FILE_CONTENT_TYPE で作成し直す
qfile = new com.questetra.bpms.core.event.scripttask.NewQfile(
attachment.filename,
DEFAULT_FILE_CONTENT_TYPE,
bytes
);
}
files.add( qfile );
});
}
/**
* 保存先データ項目の ProcessDataDefinitionView が null でない場合のみ値を代入する
* @param {ProcessDataDefinitionView} def 保存先データ項目の ProcessDataDefinitionView
* @param {Object} value データ項目に代入する値
*/
function setDataIfNotNull( def, value ) {
if ( def === null ) return; // データ項目が設定されていなければ何もしない
engine.setData( def, value );
}
