見出し画像

無料で使える!LINE ○○とGmailを連携!LINE Notifyの代替えに! コラム2

割引あり

前回の続きになります。
今回で完結となります。私も数ヶ月、自身で使用してみて安定動作を確認できましたので、ご案内できるボーダーは達することができたと思います。


LINEWORKSに転送する条件を指定する


転送するメールアドレスの設定方法

対象のメールアドレスを設定する

監視するメールアドレス

こちらは、個々のメールアドレスを設定する場合に使用します。
アドレスごとに改行が必要となります。入力する際は以下のような形となります。

メールアドレスは、誰からかのメールアドレスを!!になるので、誰のメールを監視するかなので、お間違いにならないようにしてください。

例)
xxx@gmail.com
xxx1@gmail.com
xxx2@gmail.com
xxx3@gmail.com
入力すると以下のようになります。


メールアドレスを入力する

監視するラベル

こちらのはラベルごとに監視する場合に使用します。
下の画像のように表示されているかと思います。

方法で入力内容が若干違いますので、ご自身にあった入力方法で入力ください。

パターン1 通常のラベル
通常のラベルの場合は、そのままラベル名を1行づつ入力します。
サンプルラベル1
サンプルラベル2
サンプルラベル3
サンプルラベル4
パターン2 ネストされたラベル
サンプルラベル5/ネストラベルA

上記のようにネストされた場合は「/」を挟みます。

設定例

トリガーの設定

ここでは監視対象となったメールをチェックする時間を設定します。
設定は以下の画像を参考にしてください。難しい設定はありません。直感的に操作が行えます。


トリガー設定

トリガースケジュールのポイント

一般的なメールレスポンスと対応時間を考慮した時間数を設定しています。

監視メールアドレス数やラベル、受信するメールが多い方は、「1時間」「3時間」に設定してください。これは、GASの稼働処理時間が関係があるためまとめて処理を行わないための配慮となります。

通常であれば、「3時間」に設定しておけばビジネスユースでも、処理時間やメールレスポンスとしても問題ないかと思われます。

見落としがちなので念頭に!!「未読」です。

トリガータイミングとしてご利用に合わせた調整をしてください。
タイトルの通り、未読メールを対象にしてるので、監視対象のメールをgmailのアプリやブラウザで既読してしまうとLINEWORKSに転送されません。
私はメールを確認するのが面倒なので、使用し続けた結果「1時間」に設定してLINEWORKSに転送されたメッセージを確認したのち対応するケースが利用タイミングとしてちょうどいい感じになりました。

ご利用上の注意事項

ご利用いただくにあたり、以下の点にご注意ください。

  1. 必要なアカウントと権限

    • 本ツールの利用には、Googleアカウント、各API利用権限が必要になります。

  2. APIの制限と仕様変更

    • 本ツールはAPIを使用して情報を取得しております。場合によっては、一時的に情報が取得できない場合や、情報の取得が失敗するケースもございます。予めご了承ください。

    • APIの仕様変更に伴い、ツール自体の仕様が変更になる可能性や、情報が取得・操作できなくなる可能性がございます。

  3. 情報取得の制限

    • プランによって、APIの制限が発生する場合があるため、全ての履歴が取得できない場合があります。

    • また、時間内の情報取得・操作制限もあります。大量のメッセージを取得する際には、処理が完了しない可能性があります。

  4. スプレッドシートとGoogleドライブの容量制限

    • 出力されたメッセージが大量になった場合、スプレッドシートの上限セル数に達する可能性がございます。スプレッドシートの上限に至った場合、出力先ファイルを変更するなどして対応してください。

    • ファイルダウンロードを利用する場合、Googleドライブの容量にもご注意ください。

  5. 処理時間の制限

    • 本ツールは処理時間に上限があります(約6分)。そのため、取得するメッセージが大量である場合には、処理を完了できない可能性もあります。必要に応じて、取得対象のルームを絞り込んでご利用ください。

  6. 認証と権限の許可

    • 本ツールは初回利用時に**「認証・許可プロセス」**が発生します。指示に従って認証・許可を進めてください。

  7. 免責事項

    • 本ツールの利用で発生した問題の責任は負いかねます。事前に使い方をテストし、理解した上でご利用ください。

    • 大切なデータは適宜バックアップを取り、万が一に備えてください。

  8. 著作権と利用規約

    • 無断での転載・再販・配布・共有やソースコードのコピーは固く禁止しております。

    • スクリプトのコード自体は誰でも使用できるものですが、こちらのコードは皆様のお役に立てればと考案しているものなので、利用にあたってはモラルある配慮をしていただけると幸いです。

コードの配布

ここから、使用コードになりますのでファイル名とコードを記述いたしますのでコピペしていただければと思います。

今後の作成意欲につながるので、有料エリアよりご購入いただけると幸いです。m(_ _)m

Constants.gs

// 定数の定義
const CONFIG = {
  PROPERTY_STORE: PropertiesService.getScriptProperties(),
  
  // LineWorks設定のキー
  KEYS: {
    CLIENT_ID: 'CLIENT_ID',
    CLIENT_SECRET: 'CLIENT_SECRET',
    REDIRECT_URL: 'REDIRECT_URL',
    SERVICE_ACCOUNT: 'SERVICE_ACCOUNT',
    PRIVATE_KEY: 'PRIVATE_KEY',
    OAUTH_SCOPES: 'OAUTH_SCOPES',
    BOT_ID: 'BOT_ID',
    BOT_SECRET: 'BOT_SECRET',
    ROOM_ID: 'ROOM_ID',
    MAIL_ADDRESSES: 'MAIL_ADDRESSES',
    MAIL_LABELS: 'MAIL_LABELS'
  }
};
Settings.gs

// 設定の管理
function saveSettings(settings) {
  const normalizedPrivateKey = Utils.normalizePrivateKey(settings.privateKey);
  
  CONFIG.PROPERTY_STORE.setProperties({
    [CONFIG.KEYS.CLIENT_ID]: settings.clientId,
    [CONFIG.KEYS.CLIENT_SECRET]: settings.clientSecret,
    [CONFIG.KEYS.REDIRECT_URL]: settings.redirectUrl,
    [CONFIG.KEYS.SERVICE_ACCOUNT]: settings.serviceAccount,
    [CONFIG.KEYS.PRIVATE_KEY]: normalizedPrivateKey,
    [CONFIG.KEYS.OAUTH_SCOPES]: JSON.stringify(settings.oauthScopes),
    [CONFIG.KEYS.BOT_ID]: settings.botId,
    [CONFIG.KEYS.BOT_SECRET]: settings.botSecret,
    [CONFIG.KEYS.ROOM_ID]: settings.roomId,
    [CONFIG.KEYS.MAIL_ADDRESSES]: JSON.stringify(settings.mailAddresses),
    [CONFIG.KEYS.MAIL_LABELS]: JSON.stringify(settings.mailLabels)
  });
}

function getSettings() {
  const privateKey = getConfig(CONFIG.KEYS.PRIVATE_KEY);
  
  return {
    clientId: getConfig(CONFIG.KEYS.CLIENT_ID),
    clientSecret: getConfig(CONFIG.KEYS.CLIENT_SECRET),
    redirectUrl: getConfig(CONFIG.KEYS.REDIRECT_URL),
    serviceAccount: getConfig(CONFIG.KEYS.SERVICE_ACCOUNT),
    privateKey: privateKey ? Utils.denormalizePrivateKey(privateKey) : '',
    oauthScopes: JSON.parse(getConfig(CONFIG.KEYS.OAUTH_SCOPES) || '[]'),
    botId: getConfig(CONFIG.KEYS.BOT_ID),
    botSecret: getConfig(CONFIG.KEYS.BOT_SECRET),
    roomId: getConfig(CONFIG.KEYS.ROOM_ID),
    mailAddresses: JSON.parse(getConfig(CONFIG.KEYS.MAIL_ADDRESSES) || '[]'),
    mailLabels: JSON.parse(getConfig(CONFIG.KEYS.MAIL_LABELS) || '[]')
  };
}

function getConfig(key) {
  return CONFIG.PROPERTY_STORE.getProperty(key);
}
MailService.gs

const MailService = {
  checkNewEmails() {
    try {
      Logger.log('メールチェックを開始します');

      // Gmail APIの認証確認
      try {
        const user = Session.getActiveUser().getEmail();
        Logger.log(`実行ユーザー: ${user}`);
        GmailApp.getInboxUnreadCount();
        Logger.log('Gmail APIの認証が正常です');
      } catch (error) {
        Logger.log('Gmail APIの認証に失敗しました: ' + error);
        throw new Error('Gmail APIの認証に失敗しました: ' + error);
      }

      // 設定の取得と検証
      const settings = getSettings();
      const addresses = this._validateMailAddresses(settings.mailAddresses);
      const labels = this._validateLabels(settings.mailLabels);
      
      if (addresses.length === 0 && labels.length === 0) {
        Logger.log('有効なメールアドレスまたはラベルが設定されていません');
        throw new Error('有効なメールアドレスまたはラベルが設定されていません');
      }

      // 検索クエリの生成と実行
      const searchQuery = this._buildSearchQuery(addresses, labels);
      Logger.log('生成された検索クエリ: ' + searchQuery);

      // Gmail UIで直接検索できる形式のクエリも表示
      Logger.log('Gmail UI用検索クエリ: ' + searchQuery.replace('in:anywhere', 'in:all'));

      // 未読メールの検索
      const threads = GmailApp.search(searchQuery);
      Logger.log(`検索結果: ${threads.length}件のスレッドが見つかりました`);

      let processedCount = 0;

      // スレッドごとの処理
      for (const thread of threads) {
        const messages = thread.getMessages();
        Logger.log(`スレッド内のメッセージ数: ${messages.length}`);
        
        for (const message of messages) {
          if (!message.isUnread()) {
            continue;
          }

          try {
            const from = message.getFrom();
            Logger.log(`未読メッセージを処理します - 件名: ${message.getSubject()}, From: ${from}`);
            
            const notification = this._createNotification(message);
            LineWorksService.sendMessage(notification);
            message.markRead();
            processedCount++;
          } catch (error) {
            Logger.log(`メッセージの処理中にエラーが発生しました: ${error}`);
            continue;
          }
        }
      }

      Logger.log(`${processedCount}件のメールを処理しました`);
      return { success: true, message: `${processedCount}件のメールを処理しました` };

    } catch (error) {
      Logger.log(`エラーが発生しました: ${error}`);
      throw error;
    }
  },

  _validateMailAddresses(addresses) {
    return addresses
      .map(addr => addr.trim().toLowerCase())
      .filter(addr => {
        const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(addr);
        if (!isValid) {
          Logger.log(`無効なメールアドレス形式がスキップされました: ${addr}`);
        }
        return isValid;
      });
  },

  _validateLabels(labels) {
    return labels
      .map(label => label.trim())
      .filter(Boolean);
  },

  _buildSearchQuery(addresses, labels) {
    const conditions = [];
    const searchConditions = [];
    
    // メールアドレスの条件を追加
    if (addresses.length > 0) {
        const fromConditions = addresses.map(addr => `from:(${addr})`);
        if (fromConditions.length > 1) {
            searchConditions.push(`(${fromConditions.join(' OR ')})`);
        } else {
            searchConditions.push(fromConditions[0]);
        }
    }
    
    // ラベルの条件を追加
    if (labels.length > 0) {
        const labelConditions = labels.map(label => {
            if (label.toLowerCase().startsWith('category/')) {
                return label;
            } else {
                return `label:${label}`;
            }
        });
        
        if (labelConditions.length > 1) {
            searchConditions.push(`(${labelConditions.join(' OR ')})`);
        } else {
            searchConditions.push(labelConditions[0]);
        }
    }
    
    // メールアドレスとラベルの条件をORで結合
    if (searchConditions.length > 0) {
        conditions.push(`(${searchConditions.join(' OR ')})`);
    }
    
    // 未読条件を追加
    conditions.push('is:unread');
    
    // 全てのメールを検索対象にする
    conditions.push('in:all');

    // 迷惑メールとゴミ箱を除外
    conditions.push('-in:spam');  // 迷惑メールを除外
    conditions.push('-in:trash'); // ゴミ箱を除外
    
    // 条件をANDで結合
    return conditions.join(' AND ');
  },

  _createNotification(message) {
    const subject = message.getSubject() || '(件名なし)';
    const sender = message.getFrom() || '(送信者不明)';
    const body = message.getPlainBody() || '';
    const date = message.getDate();

    Logger.log(`通知を作成 - 件名: ${subject}, 送信者: ${sender}`);
    return {
      subject: subject,
      sender: sender,
      body: body.substring(0, 1000),
      date: date
    };
  }
};
LineWorksService.gs

const LineWorksService = {
  sendMessage(notification) {
    try {
      Logger.log('LineWorksメッセージ送信を開始します');
      const settings = getSettings();
      const message = this._formatMessage(notification);
      const accessToken = AuthService.getAccessToken();
      
      const botId = settings.botId;
      const roomId = settings.roomId;
      
      if (!botId || !roomId) {
        throw new Error('Bot IDまたはRoom IDが設定されていません');
      }
      
      // APIエンドポイントを修正
      const apiUrl = `https://www.worksapis.com/v1.0/bots/${botId}/channels/${roomId}/messages`;
      
      const options = {
        method: 'post',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        muteHttpExceptions: true,
        payload: JSON.stringify({
          content: {
            type: 'text',
            text: message
          }
        })
      };
      
      Logger.log('LineWorks APIリクエストを送信します');
      const response = UrlFetchApp.fetch(apiUrl, options);
      const responseCode = response.getResponseCode();
      const responseText = response.getContentText();
      
      Logger.log(`API Response Code: ${responseCode}`);
      Logger.log(`API Response: ${responseText}`);
      
      if (responseCode < 200 || responseCode >= 300) {
        throw new Error(`LineWorks APIエラー: ${responseText}`);
      }
      
      Logger.log('メッセージ送信が成功しました');
      return true;
    } catch (error) {
      Logger.log(`LineWorksメッセージ送信エラー: ${error.toString()}`);
      throw error;
    }
  },

  _formatMessage(notification) {
    const date = Utils.formatDate(notification.date);
    return `新着メール\n\n件名: ${notification.subject}\n送信者: ${notification.sender}\n受信日時: ${date}\n\n${notification.body}...`;
  },

  testConnection() {
    try {
      Logger.log('接続テストを開始します');
      const testMessage = {
        subject: '接続テスト',
        sender: 'system@test',
        body: 'LineWorks接続テストです。この通知が届けば設定は正常です。',
        date: new Date()
      };
      
      this.sendMessage(testMessage);
      Logger.log('接続テストが成功しました');
      return true;
    } catch (error) {
      Logger.log(`接続テストが失敗しました: ${error.toString()}`);
      throw error;
    }
  }
};
AuthService.gs

// 認証サービス
const AuthService = {
  getAccessToken() {
    const settings = getSettings();
    const jwt = this._createJWT(settings);
    return this._fetchAccessToken(jwt, settings);
  },

  _createJWT(settings) {
    const header = {
      alg: 'RS256',
      typ: 'JWT'
    };

    const now = Math.floor(Date.now() / 1000);
    const payload = {
      iss: settings.clientId,
      sub: settings.serviceAccount,
      iat: now,
      exp: now + 3600 // 1時間後に期限切れ
    };

    // Base64URLエンコード
    const base64Header = this._base64URLEncode(JSON.stringify(header));
    const base64Payload = this._base64URLEncode(JSON.stringify(payload));
    
    // 署名対象の文字列を作成
    const signatureInput = `${base64Header}.${base64Payload}`;
    
    // 秘密鍵で署名
    const signature = this._sign(signatureInput, settings.privateKey);
    
    // JWTの作成
    return `${signatureInput}.${signature}`;
  },

  _base64URLEncode(str) {
    const base64 = Utilities.base64EncodeWebSafe(str);
    return base64.replace(/=+$/, ''); // パディングを削除
  },

  _sign(input, privateKey) {
    // 秘密鍵の形式を整える
    const normalizedKey = privateKey.replace(/\\n/g, '\n');
    
    // 署名を作成
    const signature = Utilities.computeRsaSha256Signature(input, normalizedKey);
    return this._base64URLEncode(signature);
  },

  _fetchAccessToken(jwt, settings) {
    const response = UrlFetchApp.fetch('https://auth.worksmobile.com/oauth2/v2.0/token', {
      method: 'post',
      muteHttpExceptions: true, // エラーレスポンスの詳細を取得するため
      payload: {
        assertion: jwt,
        grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        client_id: settings.clientId,
        client_secret: settings.clientSecret,
        scope: 'bot'  // LineWorks Botに必要なスコープを指定
      }
    });
    
    const responseText = response.getContentText();
    Logger.log('Token Response: ' + responseText); // デバッグ用ログ
    
    if (response.getResponseCode() !== 200) {
      throw new Error(`認証エラー: ${responseText}`);
    }
    
    const token = JSON.parse(responseText);
    return token.access_token;
  }
};
Utils.gs

// ユーティリティ関数
const Utils = {
  formatDate(date) {
    return Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
  },

  formatPrivateKey(key) {
    const cleanKey = key.replace(/[\r\n]/g, '');
    const header = '-----BEGIN PRIVATE KEY-----';
    const footer = '-----END PRIVATE KEY-----';
    
    let keyBody = cleanKey;
    if (keyBody.includes(header)) {
      keyBody = keyBody.split(header)[1];
    }
    if (keyBody.includes(footer)) {
      keyBody = keyBody.split(footer)[0];
    }
    
    const formattedBody = keyBody.match(/.{1,64}/g).join('\n');
    return `${header}\n${formattedBody}\n${footer}`;
  },

  normalizePrivateKey(key) {
    return key.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
  },

  denormalizePrivateKey(key) {
    return key.replace(/\\n/g, '\n');
  }
};
UI.gs

// UI関連の処理
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('LineWorks メール連携')
    .addItem('設定', 'showSettingsDialog')
    .addItem('トリガー設定', 'showTriggerDialog')
    .addItem('今すぐチェック', 'checkMailsNow')
    .addToUi();
}

function showSettingsDialog() {
  const html = HtmlService.createHtmlOutputFromFile('SettingsDialog')
    .setWidth(600)
    .setHeight(800)
    .setTitle('LineWorks メール連携設定');
  
  SpreadsheetApp.getUi().showModalDialog(html, 'LineWorks メール連携設定');
}

function showTriggerDialog() {
  const html = HtmlService.createHtmlOutputFromFile('TriggerDialog')
    .setWidth(400)
    .setHeight(300)
    .setTitle('トリガー設定');
  
  SpreadsheetApp.getUi().showModalDialog(html, 'トリガー設定');
}

function checkMailsNow() {
  try {
    const ui = SpreadsheetApp.getUi();
    const result = ui.alert(
      'メールチェック',
      '現在の設定に基づいてメールをチェックし、該当するメールをLineWorksに転送します。\n\n続行しますか?',
      ui.ButtonSet.YES_NO
    );
    
    if (result === ui.Button.YES) {
      const checkResult = MailService.checkNewEmails();
      ui.alert('処理結果', checkResult.message, ui.ButtonSet.OK);
    }
  } catch (error) {
    SpreadsheetApp.getUi().alert('エラーが発生しました: ' + error.toString());
  }
}


// トリガー関連の関数
function setupTrigger(interval) {
  return TriggerService.setupTrigger(interval);
}

function deleteTrigger() {
  return TriggerService.deleteTrigger();
}

function getTriggerStatus() {
  return TriggerService.getTriggerStatus();
}

// テスト接続用の関数
function testConnection() {
  return LineWorksService.testConnection();
}
SettingsDialog.html

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 20px;
      color: #333;
    }
    .section {
      margin-bottom: 30px;
      padding: 20px;
      border: 1px solid #ddd;
      border-radius: 5px;
    }
    .input-group {
      margin-bottom: 15px;
    }
    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }
    input[type="text"], 
    textarea {
      width: 100%;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      box-sizing: border-box;
    }
    textarea {
      height: 100px;
      resize: vertical;
    }
    .button-container {
      margin-top: 20px;
      text-align: right;
    }
    .action {
      padding: 10px 20px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .action:hover {
      background-color: #45a049;
    }
    h3 {
      margin-top: 0;
      color: #2c3e50;
    }
  </style>
</head>
<body>
  <div class="section">
    <h3>LineWorks API設定</h3>
    <div class="input-group">
      <label for="clientId">Client ID:</label>
      <input type="text" id="clientId">
    </div>
    <div class="input-group">
      <label for="clientSecret">Client Secret:</label>
      <input type="text" id="clientSecret">
    </div>
    <div class="input-group">
      <label for="redirectUrl">Redirect URL:</label>
      <input type="text" id="redirectUrl">
    </div>
    <div class="input-group">
      <label for="serviceAccount">Service Account:</label>
      <input type="text" id="serviceAccount">
    </div>
    <div class="input-group">
      <label for="privateKey">Private Key:</label>
      <textarea id="privateKey"></textarea>
    </div>
  </div>

  <div class="section">
    <h3>LineWorks Bot設定</h3>
    <div class="input-group">
      <label for="botId">Bot ID:</label>
      <input type="text" id="botId">
    </div>
    <div class="input-group">
      <label for="botSecret">Bot Secret:</label>
      <input type="text" id="botSecret">
    </div>
    <div class="input-group">
      <label for="roomId">Room ID:</label>
      <input type="text" id="roomId">
    </div>
  </div>

  <div class="section">
    <h3>メール監視設定</h3>
    <div class="input-group">
      <label for="mailAddresses">監視するメールアドレス(1行に1つ):</label>
      <textarea id="mailAddresses"></textarea>
    </div>
    <div class="input-group">
      <label for="mailLabels">監視するラベル(1行に1つ):</label>
      <textarea id="mailLabels"></textarea>
    </div>
  </div>

  <div class="button-container">
    <button onclick="testConnection()" class="action" style="margin-right: 10px;">接続テスト</button>
    <button onclick="saveSettings()" class="action">保存</button>
  </div>

  <script>
    function saveSettings() {
      const settings = {
        clientId: document.getElementById('clientId').value,
        clientSecret: document.getElementById('clientSecret').value,
        redirectUrl: document.getElementById('redirectUrl').value,
        serviceAccount: document.getElementById('serviceAccount').value,
        privateKey: document.getElementById('privateKey').value,
        botId: document.getElementById('botId').value,
        botSecret: document.getElementById('botSecret').value,
        roomId: document.getElementById('roomId').value,
        mailAddresses: document.getElementById('mailAddresses').value.split('\n').filter(Boolean),
        mailLabels: document.getElementById('mailLabels').value.split('\n').filter(Boolean)
      };

      google.script.run
        .withSuccessHandler(() => alert('設定を保存しました'))
        .withFailureHandler(error => alert('エラーが発生しました: ' + error))
        .saveSettings(settings);
    }

    function loadSettings() {
      google.script.run
        .withSuccessHandler(settings => {
          document.getElementById('clientId').value = settings.clientId || '';
          document.getElementById('clientSecret').value = settings.clientSecret || '';
          document.getElementById('redirectUrl').value = settings.redirectUrl || '';
          document.getElementById('serviceAccount').value = settings.serviceAccount || '';
          document.getElementById('privateKey').value = settings.privateKey || '';
          document.getElementById('botId').value = settings.botId || '';
          document.getElementById('botSecret').value = settings.botSecret || '';
          document.getElementById('roomId').value = settings.roomId || '';
          document.getElementById('mailAddresses').value = (settings.mailAddresses || []).join('\n');
          document.getElementById('mailLabels').value = (settings.mailLabels || []).join('\n');
        })
        .getSettings();
    }

    function testConnection() {
      google.script.run
        .withSuccessHandler(() => alert('テストメッセージの送信に成功しました'))
        .withFailureHandler(error => alert('エラーが発生しました: ' + error))
        .testConnection();
    }

    window.onload = loadSettings;
  </script>
</body>
</html>
TriggerService.gs

// トリガー管理サービス
const TriggerService = {
  INTERVALS: {
    '1hour': { hours: 1, label: '1時間' },
    '3hours': { hours: 3, label: '3時間' },
    '6hours': { hours: 6, label: '6時間' }
  },

  // トリガーの設定
  setupTrigger(interval) {
    try {
      Logger.log(`トリガー設定開始: ${interval}`);
      
      if (!this.INTERVALS[interval]) {
        throw new Error('無効な間隔が指定されました');
      }

      // 既存のトリガーを削除
      this.deleteTrigger();
      
      // triggerCheckMails 関数を実行するトリガーを作成
      const trigger = ScriptApp.newTrigger('triggerCheckMails')
        .timeBased()
        .everyHours(this.INTERVALS[interval].hours)
        .create();
      
      // トリガーの間隔を保存
      PropertiesService.getScriptProperties().setProperty('TRIGGER_INTERVAL', interval);
      
      Logger.log(`メールチェックトリガーを設定しました(間隔: ${this.INTERVALS[interval].label})`);
      return { 
        success: true, 
        message: `メールチェックを${this.INTERVALS[interval].label}間隔で開始しました` 
      };
    } catch (error) {
      Logger.log(`トリガー設定エラー: ${error}`);
      return { 
        success: false, 
        message: `トリガーの設定に失敗しました: ${error}` 
      };
    }
  },

  // 既存のトリガーを削除
  deleteTrigger() {
    try {
      Logger.log('トリガー削除開始');
      const triggers = ScriptApp.getProjectTriggers();
      triggers.forEach(trigger => {
        if (trigger.getHandlerFunction() === 'triggerCheckMails') {
          ScriptApp.deleteTrigger(trigger);
          Logger.log('トリガーを削除しました');
        }
      });
      
      // 保存された間隔を削除
      PropertiesService.getScriptProperties().deleteProperty('TRIGGER_INTERVAL');
      
      return { 
        success: true, 
        message: 'メールチェックを停止しました' 
      };
    } catch (error) {
      Logger.log(`トリガー削除エラー: ${error}`);
      return { 
        success: false, 
        message: `トリガーの削除に失敗しました: ${error}` 
      };
    }
  },

  // トリガーの状態を確認
  getTriggerStatus() {
    try {
      Logger.log('トリガー状態確認開始');
      const triggers = ScriptApp.getProjectTriggers();
      const mailCheckTrigger = triggers.find(trigger => 
        trigger.getHandlerFunction() === 'triggerCheckMails'
      );
      
      const interval = PropertiesService.getScriptProperties().getProperty('TRIGGER_INTERVAL');
      const status = {
        success: true,
        isActive: !!mailCheckTrigger,
        interval: interval && this.INTERVALS[interval] ? this.INTERVALS[interval].label : null
      };
      
      Logger.log(`トリガー状態: ${JSON.stringify(status)}`);
      return status;
    } catch (error) {
      Logger.log(`トリガー状態確認エラー: ${error}`);
      return {
        success: false,
        isActive: false,
        interval: null,
        error: error.toString()
      };
    }
  }
};
TriggerDialog.html

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 20px;
      color: #333;
    }
    .status {
      margin-bottom: 20px;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
      background-color: #f9f9f9;
    }
    .interval-selector {
      margin-bottom: 20px;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    .interval-selector select {
      width: 100%;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      margin-top: 5px;
    }
    .button-container {
      display: flex;
      gap: 10px;
      justify-content: center;
    }
    .action {
      padding: 10px 20px;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .start {
      background-color: #4CAF50;
    }
    .start:hover {
      background-color: #45a049;
    }
    .stop {
      background-color: #f44336;
    }
    .stop:hover {
      background-color: #da190b;
    }
    .disabled {
      opacity: 0.6;
      cursor: not-allowed;
    }
  </style>
</head>
<body>
  <div class="status" id="statusArea">
    ステータス確認中...
  </div>
  
  <div class="interval-selector">
    <label for="interval">チェック間隔:</label>
    <select id="interval">
      <option value="1hour">1時間</option>
      <option value="3hours">3時間</option>
      <option value="6hours">6時間</option>
    </select>
  </div>
  
  <div class="button-container">
    <button onclick="startTrigger()" class="action start" id="startButton">開始</button>
    <button onclick="stopTrigger()" class="action stop" id="stopButton">停止</button>
  </div>

  <script>
    let isProcessing = false;

    function setProcessing(processing) {
      isProcessing = processing;
      document.getElementById('startButton').disabled = processing;
      document.getElementById('stopButton').disabled = processing;
      if (processing) {
        document.getElementById('startButton').classList.add('disabled');
        document.getElementById('stopButton').classList.add('disabled');
      } else {
        document.getElementById('startButton').classList.remove('disabled');
        document.getElementById('stopButton').classList.remove('disabled');
      }
    }

    function updateStatus() {
      setProcessing(true);
      google.script.run
        .withSuccessHandler(status => {
          if (status.success) {
            const statusText = status.isActive
              ? `メールチェックが有効です(間隔: ${status.interval})`
              : 'メールチェックは停止中です';
            document.getElementById('statusArea').textContent = statusText;
            
            document.getElementById('startButton').disabled = status.isActive;
            document.getElementById('stopButton').disabled = !status.isActive;
          } else {
            document.getElementById('statusArea').textContent = 
              'ステータスの取得に失敗しました: ' + (status.error || '不明なエラー');
          }
          setProcessing(false);
        })
        .withFailureHandler(error => {
          document.getElementById('statusArea').textContent = 
            'エラーが発生しました: ' + error;
          setProcessing(false);
        })
        .getTriggerStatus();
    }

    function startTrigger() {
      if (isProcessing) return;
      setProcessing(true);
      
      const interval = document.getElementById('interval').value;
      google.script.run
        .withSuccessHandler(result => {
          if (result.success) {
            updateStatus();
          }
          alert(result.message);
          setProcessing(false);
        })
        .withFailureHandler(error => {
          alert('エラーが発生しました: ' + error);
          setProcessing(false);
        })
        .setupTrigger(interval);
    }

    function stopTrigger() {
      if (isProcessing) return;
      setProcessing(true);
      
      google.script.run
        .withSuccessHandler(result => {
          if (result.success) {
            updateStatus();
          }
          alert(result.message);
          setProcessing(false);
        })
        .withFailureHandler(error => {
          alert('エラーが発生しました: ' + error);
          setProcessing(false);
        })
        .deleteTrigger();
    }

    window.onload = updateStatus;
  </script>
</body>
</html>
TriggerHandlers.gs

// トリガーから実行される関数
function triggerCheckMails() {
  try {
    Logger.log('トリガーによるメールチェックを開始します');
    const checkResult = MailService.checkNewEmails();
    Logger.log(`メールチェック完了: ${checkResult.message}`);
  } catch (error) {
    Logger.log(`トリガー実行中にエラーが発生: ${error}`);
  }
}

// UI から手動実行する関数
function checkMailsNow() {
  try {
    const ui = SpreadsheetApp.getUi();
    const result = ui.alert(
      'メールチェック',
      '現在の設定に基づいてメールをチェックし、該当するメールをLineWorksに転送します。\n\n続行しますか?',
      ui.ButtonSet.YES_NO
    );
    
    if (result === ui.Button.YES) {
      const checkResult = MailService.checkNewEmails();
      ui.alert('処理結果', checkResult.message, ui.ButtonSet.OK);
    }
  } catch (error) {
    SpreadsheetApp.getUi().alert('エラーが発生しました: ' + error.toString());
  }
}

有料エリアについてのご案内


有料エリアのお申し込みをいただいた方には、スプレッドシートのURLをおってお知らせいたします。配布されたURLをコピーを行うことで、そのまま利用できます。
申し込みフォームより購入者アカウントと配布先のメールアドレスをご連絡ください。

配布用のためメールアドレスをお預かりしいますが、配布用以外での使用は一切おこないません。
捨てアドなどで私でチェックしたのですが、セキュリティー等で受信できなかったので、普段お使いのメールアドレスやgmailでの利用が良いかと思います。
そのままURLを配布できればよいのですが、スクリプト付きのURLは共有できないため、以下のURLより必要事項を入力いただき返信メールをお待ち下さい。

購入後に進むと下記画像のような、フォームが出てくるので、必要事項を入力の上お進みください。


必要事項を入力ください。
すべての内容を入力後に、「送信ボタン」をクリックしてください。
送信ボタンは連打せずに1回のみクリックください。
ご購入者様専用のスプレッドシートを作成するまでに30秒ほど時間がかかります。

処理が完了しますと以下のようにメッセージが表示されます。


フォームに入力したメールアドレスを確認ください。
通常の受信メールやすべてのメールにメールが届いていない場合は、「迷惑メールのフォルダ」を確認してみてください。
スクリプト送信しているので、迷惑メールに振り分けられる可能性がございます。

私から送信するメールアドレスは

offloadtask@gmail.com

となっております。

しばらくお待ちいただいて、メールが届かない場合は、お手数ですが、上記メールアドレスにご連絡いただけると幸いです。

ここから先は

217字

この記事が気に入ったらチップで応援してみませんか?