LoginSignup
1
1

More than 1 year has passed since last update.

GoogleForm と GASで年賀状をGmailから自動送信してみた

Posted at

背景

我が家では、数年前から年賀状にGoogleFormのQRコードを貼って、年賀状をメールで受け取りたい方にはメールアドレスを登録して頂き、1月1日に手動でメールを送信しています。

手動でメールを送信するのが面倒くさくなってきたので、これを自動化したいと思います。

目標

  • 1月1日にスプレッドシートに登録されているメールアドレスに 年賀状画像付きのHTMLメールを送信する

  • 送信が完了したアドレスを別のシートにまとめる

  • 12月30日時点でGoogleDriveに今年の年賀状がアップロードされていない時、Slackにメッセージを投げる

詳細

  • 年賀状画像はGoogleDriveの特定フォルダに YYYY.png の形式で保存しておく

  • ファイルが見つからない時はSlackにメッセージを送信する

  • 年賀状はGmailから送信する

Apps Script

スプレッドシートの拡張機能からApps Scriptを選んで適当にコードを書きます。

image.png

スプレッドシートのログから最後に送信した年を取得する

// ログから最後に送信したのが何年か取得する
const getLastYearFromLogSheet = () => {
  const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
  const sheets = spreadSheets.getSheets()

  // 年のリストをログシートの名前から取得
  const logYearList = 
    sheets.map((v) => {
      const name = v.getName();
      const split = name.split("-");

      if (split[0] === "log") return split[1];

      return "";
    }).filter((v) => {
      if (v === "") return false;
      return true;
    });


  // 配列を年で並び替える
  const sortLogYearList = logYearList.sort((a,b) => {
    a = Number(a);
    b = Number(b);

    if (!a || !b) {
      sendLogToSlack("getLastYearFromLogSheet.gsでエラーが発生しました。シート名 log-YYYY を期待しましたが、 Number(YYYY)でエラーが発生しています。" );
    }

    return b - a;

  })


  console.log(`ログに保存されている最後の年 = ${sortLogYearList[0]}`);
  return Number(sortLogYearList[0])
}

Google Drive に年賀状の画像データが保存されているか確認する

Drive API を追加する

image.png

image.png

コード

年賀状の画像ファイルは特定のGoogle DriveフォルダにYYYY.pngという名前で保存されるので来年の画像ファイルがあるか確認します。

function checkGoogleDriveData() {

  //画像ディレクトリのファイルをすべて取得
  const folder = DriveApp.getFolderById(env.googleDriveFolderId)

  const files = folder.getFiles();

  const imageDirFiles = [];

  while(files.hasNext()) {
    imageDirFiles.push(files.next());
  }

  // 来年
  const nextYear = getLastYearFromLogSheet() + 1;

  // 次に送信する年賀状の画像ファイル
  const nextYearImage = imageDirFiles.find((v) => {
    if (v.getName() === `${nextYear}.png`) return true;
    return false;
  })

  if (!nextYearImage) return false; // 来年送信予定の画像ファイルが見つからない

  console.log(`送信予定のファイル : ${nextYearImage.getName()}`);

  return {// 来年送信予定の画像ファイルが見つかった
    year : nextYear,
    imageBlob : nextYearImage.getBlob()
  }; 
}

送信対象者リストを配列で取得する

Google Form からのデータは
image.png

この形式なので、このデータから

[
 {
  name : string,
  email : string
 },
 {
  name : string,
  email : string
 }
]

このような配列を生成します。

コード

// ファイルの中から送信が必要なユーザーを配列で取得し返す

/*
- return のフォーマット

[
  {
    name : string,
    email : string
  },
  {
    name : string,
    email : string
  }
]
 */
const getSendUserList = () => {
    const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = spreadSheets.getSheets()[0];
    let values = sheet.getDataRange().getValues();

    // メールを選択していない列を削除
    values = values.filter ((v,i) => {
      if (i === 0) return false; // 1列目は無視する
      if (!(v[2] === "メールによる受け取り")) return false; // メールによる受け取りを選択していない場合無視する
      if (!(v[1])) return false; // 名前が未設定の場合無視
      if (!(v[3])) return false; // メールアドレスが未設定の場合無視

      return true;
    });

    // オブジェクトに変換
    values = values.map((v) => {
      return ({
        name : v[1],
        email : v[3]
      })
    });

    return (values);
}

メールの送信

送信すべき人リストが取得できるようになったので、次にメールを送信する処理を実装します。

Gmail API を追加する

image.png

コード

const sendEmail = (name, email, imageBlob, year) => {

  sendLogToSlack(`${name}さんの ${email} に年賀状メールを送信しています...`);


  try {
    GmailApp.sendEmail(email, `${env.myName}からの年賀状`, "この端末でHTMLメールは表示できません。HTMLメールの設定を確認するかデバイスを変更してください。", {
      name : env.myName,
      inlineImages : {
        mainImage : imageBlob
      },
      htmlBody : `
        <img src="cid:mainImage" style="position:absolute; width:90%; left:5%">
      `,
    })
    saveLog(name, email, year, "成功");
  } catch (e){
    sendLogToSlack(`${name} さんの ${email} にメールを送信できませんでした。` + e);
    saveLog(name, email, year, "失敗");
  }
}

メールを送信した事を別のシートに記録する

const saveLog = (name = "test name", email = "test@test.com", year = 400, status="不明")  => {
    const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
    const sheets = spreadSheets.getSheets();

    // 今年のシートを探す
    let thisYearLogSheet = sheets.find((v) => {
      if (v.getName() === `log-${year}`) return true;
      return false;
    });

    // 今年のシートが無い場合作成
    if (!thisYearLogSheet) {
      thisYearLogSheet = spreadSheets.insertSheet();
      thisYearLogSheet.setName(`log-${year}`);
      console.log("新しいlogシートを作成しました。");
      thisYearLogSheet.appendRow(["名前", "メールアドレス", "ステータス"])

    };

    // 行を追加
    thisYearLogSheet.appendRow([name, email, status]);

}

定期的に処理を実行する

トリガーが未設定なので
image.png
トリガーを設定して
image.png
完成です。

その他

すべてのファイル

appsscript.json
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
    "enabledAdvancedServices": [
      {
        "userSymbol": "Drive",
        "version": "v2",
        "serviceId": "drive"
      },
      {
        "userSymbol": "Gmail",
        "version": "v1",
        "serviceId": "gmail"
      }
    ]
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

env.gs
/**
 * - 一番最初にあるシートをGoogleフォームと繋がっているシートとして処理します。
 */

const env = {
  myName : "〇〇家", // 送信元の名前
  slackWebSocketUrl : "https://hooks.slack.com/services/**********",// 家族SlackのWebSocketUrl
  googleDriveFolderId : "**********",//年賀状の画像データが保存されているGoogleDriveのフォルダID
  checkSendDay : { // 年賀状を送信する日
    month : 1,
    day : 1
  },
  checkGoogleDriveDataDay: { // 年賀状のデータがGoogleDriveに存在するか確認する日
    month : 12,
    day : 30
  }
}

index.gs
const myFunction = () => {

  console.log("処理が開始されました。");

  if (checkSendDay()) {
    // 年賀状の送信処理を開始
    sendLogToSlack("年賀状の送信処理を開始します。");

    const sendUserList = getSendUserList();

    sendLogToSlack(`${sendUserList.length}人に対して年賀状メールの送信を実行します。`);

    const imageData = checkGoogleDriveData();

    if (imageData === false) {

      sendLogToSlack("メールの送信を開始しようとしましたが、GoogleDriveにファイルが見つからなかった為キャンセルしました。");

      return;
    }

    // 全員に送信
    sendUserList.forEach((v,i) => {
      sendEmail(v.name, v.email, imageData.imageBlob, imageData.year);
    })

  };


  if (checkGoogleDriveDataDay()) {
    // GoogleDriveのデータを確認開始
    sendLogToSlack("Google Drive に年賀状データが保存されているか確認します。 確認結果をお待ち下さい...");

    if (checkGoogleDriveData()) {
      sendLogToSlack("Google Drive に年賀状データが保存されています。");

    } else {
      sendLogToSlack("Google Drive に年賀状データが存在しません。年賀状の送信日までに年賀状のデータを Google Drive にアップロードしてください。 ※pngファイル");

    }
  }
}

checkDay.gs


// 使いやすい形式の月と日を返す
const getDate = () => {
  return (
    {
      month : new Date().getMonth() + 1,
      day : new Date().getDate()
    }
  )
}

// 年賀状を送信する日の場合Trueを返します。
const checkSendDay = () => {
  const date = getDate();

  if (
      env.checkSendDay.month === date.month
      &&
      env.checkSendDay.day === date.day
    ) {
      // envの日付と一致した時
      return true;
    } else {
      // envの日付と一致しなかった時
      return false;
    }

}

// GoogleDriveのデータをチェックする日の場合Trueを返します。
const checkGoogleDriveDataDay = () => {
  const date = getDate();

  if (
      env.checkGoogleDriveDataDay.month === date.month
      &&
      env.checkGoogleDriveDataDay.day === date.day
    ) {
      // envの日付と一致した時
      return true;
    } else {
      // envの日付と一致しなかった時
      return false;
    }
}



getSendUserList.gs
// ファイルの中から送信が必要なユーザーを配列で取得し返す

/*
- return のフォーマット

[
  {
    name : string,
    email : string
  },
  {
    name : string,
    email : string
  }
]
 */
const getSendUserList = () => {
    const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = spreadSheets.getSheets()[0];
    let values = sheet.getDataRange().getValues();

    // メールを選択していない列を削除
    values = values.filter ((v,i) => {
      if (i === 0) return false; // 1列目は無視する
      if (!(v[2] === "メールによる受け取り")) return false; // メールによる受け取りを選択していない場合無視する
      if (!(v[1])) return false; // 名前が未設定の場合無視
      if (!(v[3])) return false; // メールアドレスが未設定の場合無視

      return true;
    });

    // オブジェクトに変換
    values = values.map((v) => {
      return ({
        name : v[1],
        email : v[3]
      })
    });

    return (values);
}

checkGoogleDriveData.gs
function checkGoogleDriveData() {

  //画像ディレクトリのファイルをすべて取得
  const folder = DriveApp.getFolderById(env.googleDriveFolderId)

  const files = folder.getFiles();

  const imageDirFiles = [];

  while(files.hasNext()) {
    imageDirFiles.push(files.next());
  }

  // 来年
  const nextYear = getLastYearFromLogSheet() + 1;

  // 次に送信する年賀状の画像ファイル
  const nextYearImage = imageDirFiles.find((v) => {
    if (v.getName() === `${nextYear}.png`) return true;
    return false;
  })

  if (!nextYearImage) return false; // 来年送信予定の画像ファイルが見つからない

  console.log(`送信予定のファイル : ${nextYearImage.getName()}`);

  return {// 来年送信予定の画像ファイルが見つかった
    year : nextYear,
    imageBlob : nextYearImage.getBlob()
  }; 
}

sendLogToSlack.gs
// Slackにログを送信する
const sendLogToSlack = (message) => {


  const content = JSON.stringify({
     "text" : message,
  });

  const sendData = {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : content
  };

  UrlFetchApp.fetch(env.slackWebSocketUrl, sendData);
  console.log(`${message} : Slack に送信済み`);

}

sendEmail.gs
const sendEmail = (name, email, imageBlob, year) => {

  sendLogToSlack(`${name}さんの ${email} に年賀状メールを送信しています...`);


  try {
    GmailApp.sendEmail(email, `${env.myName}からの年賀状`, "この端末でHTMLメールは表示できません。HTMLメールの設定を確認するかデバイスを変更してください。", {
      name : env.myName,
      inlineImages : {
        mainImage : imageBlob
      },
      htmlBody : `
        <img src="cid:mainImage" style="position:absolute; width:90%; left:5%">
      `,
    })
    saveLog(name, email, year, "成功");
  } catch (e){
    sendLogToSlack(`${name} さんの ${email} にメールを送信できませんでした。` + e);
    saveLog(name, email, year, "失敗");
  }
}

getLastYearFromLogSheet.gs
// ログから最後に送信したのが何年か取得する
const getLastYearFromLogSheet = () => {
  const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
  const sheets = spreadSheets.getSheets()

  // 年のリストをログシートの名前から取得
  const logYearList = 
    sheets.map((v) => {
      const name = v.getName();
      const split = name.split("-");

      if (split[0] === "log") return split[1];

      return "";
    }).filter((v) => {
      if (v === "") return false;
      return true;
    });


  // 配列を年で並び替える
  const sortLogYearList = logYearList.sort((a,b) => {
    a = Number(a);
    b = Number(b);

    if (!a || !b) {
      sendLogToSlack("getLastYearFromLogSheet.gsでエラーが発生しました。シート名 log-YYYY を期待しましたが、 Number(YYYY)でエラーが発生しています。" );
    }

    return b - a;

  })


  console.log(`ログに保存されている最後の年 = ${sortLogYearList[0]}`);
  return Number(sortLogYearList[0])
}

saveLog.gs
const saveLog = (name = "test name", email = "test@test.com", year = 400, status="不明")  => {
    const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
    const sheets = spreadSheets.getSheets();

    // 今年のシートを探す
    let thisYearLogSheet = sheets.find((v) => {
      if (v.getName() === `log-${year}`) return true;
      return false;
    });

    // 今年のシートが無い場合作成
    if (!thisYearLogSheet) {
      thisYearLogSheet = spreadSheets.insertSheet();
      thisYearLogSheet.setName(`log-${year}`);
      console.log("新しいlogシートを作成しました。");
      thisYearLogSheet.appendRow(["名前", "メールアドレス", "ステータス"])

    };

    // 行を追加
    thisYearLogSheet.appendRow([name, email, status]);

}

感想

色々考えて実装したつもりだったけど、後で冷静に考えるとの取得方法は普通にnew Date()から取れば良かったかも。でも結構短時間で実装できたので良しとする。
何より冬場のキーボードは辛い。

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1