Gmail: メール取得 (Gmail: Get Email Message)
この工程は、Gmail のメールを取得します。
Configs:共通設定
  • 工程名
  • メモ
Configs
  • C1: OAuth2 設定 *
  • C2: メール ID *
  • C3: From メールアドレスを保存する文字型データ項目
  • C4: From 表示名を保存する文字型データ項目
  • C5: To/Cc/Bcc メールアドレス一覧を保存する文字型データ項目
  • C6: To/Cc/Bcc 表示名一覧を保存する文字型データ項目
  • C7: 送信日時を保存する日時型データ項目
  • C8: 件名を保存する文字型データ項目
  • C9: 本文を保存する文字型データ項目
  • C10: 添付ファイルを保存するファイル型データ項目

Notes

  • 本モデリング要素は Google Workspace アカウント用です
    • 一般ユーザー向けアカウント(gmail.com)での利用はできません
  • 事前準備として、Google Workspace の管理者が対象の OAuth アプリを信頼できるアプリとして設定済みである必要があります
    • この設定がなされていない状態での利用はできません
    • 本モデリング要素が使用する OAuth アプリのクライアント ID は 13039123046-t87nmrj499ffoa58asehks3asajvgqnh.apps.googleusercontent.com です
  • メール ID は Gmail 上では確認できません

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)

main();
function main(){
  //// == 工程コンフィグ・ワークフローデータの参照 / Config & Data Retrieving ==
  const auth = configs.get("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.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", "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 "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 "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 `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": ""
    },
    "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 '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 );
  }
  if ( mimeType.startsWith("multipart/") ) { // マルチパートのメール
    message.body = parseMultiPart( payload, message.attachments );
  }

  return message;
}

/**
  * MIME タイプが text/plain, text/html のパートのデータをデコードして文字列として返す
  * @param {Object} part  MIME メールのパート(text/plain または text/html)
  * @return {String} body  パートのデータ(メール本文として保存する文字列の候補)
  */
function parseTextPart( part ) {
  return base64.decodeFromUrlSafeString( part.body.data ); // UTF-8 としてデコード
}

/**
  * MIME タイプが multipart/* のパートを再帰的に解析する
  * 添付ファイルのパートがあればその情報を配列に格納したうえで、本文として保存する文字列の候補を返す
  * @param {Object} part  MIME メールのパート(multipart/*)
  * @param attachments {Array<Object>} attachments  添付ファイルの情報(オブジェクト)を格納する配列
  * @return {String} body  メール本文として保存する文字列の候補
  */
function parseMultiPart( part, attachments ) {
  // 本文の候補
  let textPart = null; // 第一候補のパート
  let htmlPart = null; // 第二候補のパート
  let nestedBody = ""; // 第三候補の文字列(下の階層から取得した本文候補、または空文字列)

  part.parts.forEach( ( part, index ) => {
    // マルチパートの場合
    if ( part.mimeType.startsWith("multipart/") ) {
      nestedBody = parseMultiPart( part, attachments );
      return;
    }

    // 添付ファイルの場合
    const file = attachment( part, index );
    if ( file !== null ) {
      attachments.push( file );
      return;
    }

    // マルチパートでも添付ファイルでも無い場合は、本文パートの可能性がある
    if ( part.mimeType === "text/plain" ) {
      if ( textPart === null ) { // 1つ目の "text/plain" パートを本文パートの第一候補とする
        textPart = part;
      }
      return;
    }
    if ( part.mimeType === "text/html" ) {
      if ( htmlPart === null ) { // 1つ目の "text/html" パートを本文パートの第二候補とする
        htmlPart = part;
      }
      return;
    }
  });

  if ( textPart !== null ) {
    return parseTextPart( textPart );
  }
  if ( htmlPart !== null ) {
    return parseTextPart( htmlPart );
  }
  // "text/plain" パートも "text/html" パートも無い場合は、下の階層から取得した本文、または空文字列
  return nestedBody;
}

/**
  * MIME メールのマルチパート内のパートが添付ファイルかどうかを調べ、添付ファイルであれば
  * その情報をオブジェクトとして返す
  * @param {Object} part  マルチパート内のパート
  * @param index {Number} index  マルチパート内で何番目のパートか
  * @return {Object} file  添付ファイルの情報(添付ファイルのパートでない場合は null)
  */
function attachment( part, index ) {
  // マルチパート内の先頭パートは添付ファイルとみなさない
  if ( index === 0 ) {
    return null;
  }

  const disposition = getDisposition( part );
  const contentType = getContentType( part );
  if ( disposition === "attachment" || part.body.attachmentId !== undefined ) { // ファイル名がなくても添付ファイルとみなす
    let filename = part.filename;
    if ( filename === undefined || filename === null || filename === "" ) {
      filename = "noname"; // Default
    }
    const file = {
      "filename": filename,
      "contentType": contentType,
      "body": part.body
    }
    return file;
  } else { // ファイル名 があれば添付ファイルとみなす
    const filename = part.filename;
    if ( filename === undefined || filename === null || filename === "" ) {
      return null;
    }
    const file = {
      "filename": filename,
      "contentType": contentType,
      "body": part.body
    }
    return file;
  }
}

/**
  * 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();
}

/**
  * MIME メールのパートの "Content-Type" ヘッダの値を返す
  * ヘッダが無い場合はデフォルト値として "application/octet-stream" を返す
  * @param {Object} part  パート
  * @return {String} contentType  "Content-Type" ヘッダの値
  */
function getContentType( part ) {
  const defaultContentType = "application/octet-stream";
  if ( part.headers === undefined ) { // パートにヘッダが無い場合はデフォルト値を返す
    return defaultContentType;
  }
  const contentTypeHeader = part.headers.find( header => header.name.toLowerCase() === "content-type" );
  if ( contentTypeHeader === undefined ) { // 無い場合はデフォルト値を返す
    return defaultContentType;
  }
  return contentTypeHeader.value;
}

/**
  * 添付ファイルの情報に本体データが含まれているかどうかを調べ、
  * ない場合は Gmail REST API に添付ファイル取得の GET リクエストを送信し、
  * 本体データを追加する
  * @param {String} apiUri  API の URI(/messages/{messageId} まで)
  * @param {String} auth  認証設定名
  * @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 ) { return; } // data があれば何もしない
    httpCount++;
    if ( httpCount > httpLimit ) { throw "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 `Failed to get attachment. status: ${status}`;
    }
    // when successful, parse the message content
    const json = JSON.parse(responseJson);
    attachment.body["data"] = json.data;
  });
}

/**
  * 添付ファイルを Qfile に変換し、ファイルの配列に追加する
  * @param {Array<Object>} attachments  添付ファイルの情報(オブジェクト)が格納された配列
  * @param {ListArray<Qfile>} files  ファイルの配列
  */
function convertAndAddAttachments( attachments, files ) {
  attachments.forEach( attachment => {  
    const qfile = new com.questetra.bpms.core.event.scripttask.NewQfile(
      attachment.filename,
      attachment.contentType,
      base64.decodeFromUrlSafeStringToByteArray( attachment.body.data ) // ByteArrayWrapper
    );
    files.add( qfile );
  });
}

/**
  * 保存先データ項目の ProcessDataDefinitionView が null でない場合のみ値を代入する
  * @param {ProcessDataDefinitionView} def  保存先データ項目の ProcessDataDefinitionView
  * @param {Object} value  データ項目に代入する値
  */
function setDataIfNotNull( def, value ) {
  if ( def === null ) return; // データ項目が設定されていなければ何もしない
  engine.setData( def, value );
}

  
%d人のブロガーが「いいね」をつけました。