これで解決したい課題
- 完成版(成形後)の抄録PDFを演題ごとに正確にひも付け、一括でメール送信したい
- 誤送信は絶対に避けたい(ドライラン必須、同じファイルの二重送信防止)
- フォルダ内に同じ演題のPDFがもし仮に複数あっても、最新だけを選んで送りたい
- 送信履歴(いつ・どのファイルを送ったか)をシートに自動記録したい
全体の仕組み
- Google ドライブに「完成版PDFフォルダ」を用意(ID:
FINAL_PDF_FOLDER_ID) - PDF名は
<最終演題番号>(<受付番号>)_abstract_YYYYMMDD_HHMMSS.pdf(以前の記事を参考に進めていれば、このようになっています。) - スプレッドシート「フォームの回答 1」から行ごとに以下を取得(下記のコードでは、「フォームの回答1」のタブ名からデータを取得しています。任意名に変更可能です。)
- 宛先メールアドレス(必須)
- 命名しなおした最終的な演題番号(必須:PDFひも付けのキー)
- 宛名用:筆頭演者の姓/名、所属(任意)
- スクリプトがフォルダ内PDFを走査して**索引(インデックス)**を構築
- 同じ最終演題番号のPDFが複数ある場合、タイムスタンプ(TS)が最大=最新の1件を採用
- 各行についてメールを組み立て、**DRY RUN(SAFE_MODE)**で確認 → 本番送信
- 送ったらシートにステータス/送信時刻/PDFのTS/ファイル名を記録
- 同じTSのPDFは再送しない(差し替わった時だけ再送)
使う列(自動検出)
- 必須
メール最終的な演題番号(または最終演題番号)
- 宛名に使う(任意)
筆頭演者(発表者)の姓、筆頭演者の名前、筆頭演者の所属機関名
- 送信履歴(無ければ自動で追加)
完成版送信ステータス/完成版送信時刻/完成版PDF_TS/完成版PDFファイル名
コードの全体(コピペ可能)
コードの全体を乗せます。
// ---- 安全運転スイッチ(本番で false に) ----
const SAFE_MODE = true;
// ---- 固有名詞(各自の情報に修正してください) ----
const ORG_NAME = '〇〇学会';
const ORG_EMAIL = 'contact@example.org';
const PROGRAM_URL = 'https://example.org/program';
// ---- 完成版PDFフォルダID(目的のGoogle DriveのフォルダのIDに、必ず差し替えてください) ----
const FINAL_PDF_FOLDER_ID = 'REPLACE_WITH_YOUR_FOLDER_ID';
/* ===== 件名・本文テンプレ ===== */
const FINAL_MAIL_SUBJECT = (finalId) =>
`【${ORG_NAME}】演題採択のお知らせ(演題番号:${finalId})`;
const FINAL_MAIL_BODY = (finalId, pdfName) =>
`${ORG_NAME} 運営事務局でございます。
ご登録いただいた演題は、後述の演題番号にて採択されました。
抄録PDFを添付いたしましたので、改めて内容をご確認ください(${pdfName})。
もし誤り等の修正が必要な事項がございましたら、演題番号と合わせて、事務局まで早急にお知らせください。
演題番号:${finalId}
なお、発表日時は学会ホームページをご参照ください。
${PROGRAM_URL}
────────────────────
${ORG_NAME} 運営事務局
E-mail: ${ORG_EMAIL}
────────────────────
※本メールは自動送信です。`;
/* ===== メイン ===== */
function sendFinalAbstractEmails() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sh = ss.getSheetByName('フォームの回答 1');
const values = sh.getDataRange().getValues();
const H = values[0];
// 必須列
const emailCol = findCol(H, /メール/);
const finalIdCol = findCol(H, /(最終的な演題番号|最終演題番号)/);
// 宛名(任意)
const lastNameCol = findCol(H, /筆頭演者(発表者)の姓/, true);
const firstNameCol= findCol(H, /筆頭演者の名前/, true);
const affilCol = findCol(H, /筆頭演者の所属機関名/, true);
// 送信履歴(無ければ1行目に追加)
let statusCol = ensureCol(sh, H, '完成版送信ステータス');
let timeCol = ensureCol(sh, H, '完成版送信時刻');
let tsCol = ensureCol(sh, H, '完成版PDF_TS'); // 例: 20250919_175608
let nameCol = ensureCol(sh, H, '完成版PDFファイル名'); // 実際に送ったPDF名
// Drive: 最終演題番号 → 最新PDF の索引を作る
const pdfIndex = buildFinalPdfIndex_(FINAL_PDF_FOLDER_ID);
// 各行を処理
for (let r = 1; r < values.length; r++) {
const row = values[r];
const email = safe(row[emailCol]);
const finalId = safe(row[finalIdCol]);
if (!email || !finalId) continue;
const info = pdfIndex[finalId];
if (!info) { Logger.log('PDF未発見: %s', finalId); continue; }
// 既送チェック:同じTSは送らない
const prevStatus = safe(row[statusCol]);
const prevTs = safe(row[tsCol]);
const newTs = info.ts;
const alreadyUpToDate = (prevStatus === '送信済み' && prevTs === newTs);
if (alreadyUpToDate) continue;
// 宛名:「所属 氏名 先生」
const fullName = `${safe(row[lastNameCol])} ${safe(row[firstNameCol])}`.trim();
const affil = safe(row[affilCol]);
const recipientLine = (fullName || affil) ? `${affil} ${fullName} 先生\n\n` : '';
const subject = FINAL_MAIL_SUBJECT(finalId);
const body = recipientLine + FINAL_MAIL_BODY(finalId, info.name);
// ドライラン:ここで必ず止める(SAFE_MODE=trueの間は送られない)
if (SAFE_MODE) {
Logger.log('[DRY RUN] to=%s finalId=%s file=%s', email, finalId, info.name);
continue;
}
// 実送信
GmailApp.sendEmail(email, subject, body, {
attachments: [info.file.getBlob()],
name: `${ORG_NAME} 運営事務局`
});
// 履歴を記録
sh.getRange(r+1, statusCol+1).setValue('送信済み');
sh.getRange(r+1, timeCol+1).setValue(new Date());
sh.getRange(r+1, tsCol+1).setValue(newTs);
sh.getRange(r+1, nameCol+1).setValue(info.name);
Utilities.sleep(500); // レート制限対策
}
}
/* ===== Drive内のPDFを索引化:最終演題番号 → 最新1件 ===== */
function buildFinalPdfIndex_(folderId) {
const folder = DriveApp.getFolderById(folderId);
const it = folder.getFiles();
const map = {};
// 例: YIA7(e024XX)_abstract_20250919_175608.pdf
const re = /^([A-Za-z0-9\-]+(?:\([^)]+\))?)_(?:[Aa]bstract)_(\d{8}_\d{6})\.pdf$/;
while (it.hasNext()) {
const f = it.next();
const name = f.getName();
const m = name.match(re);
if (!m) continue;
const idPart = m[1]; // 例 "YIA7(e024XX)" / "S1-2"
const coreId = idPart.replace(/\(.*$/, ''); // → "YIA7" / "S1-2"
const ts = m[2]; // "YYYYMMDD_HHMMSS"
// タイムスタンプの大きい方=最新を保持
if (!map[coreId] || ts > map[coreId].ts) {
map[coreId] = { file: f, ts, name };
}
}
return map;
}
/* ===== ヘルパー ===== */
function findCol(H, regex, optional=false) {
const i = H.findIndex(h => regex.test(String(h)));
if (i === -1 && !optional) throw new Error('必要列が見つかりません: ' + regex);
return i;
}
function ensureCol(sh, H, label) {
let idx = H.indexOf(label);
if (idx !== -1) return idx;
idx = H.length;
sh.getRange(1, idx+1).setValue(label);
H.push(label);
return idx;
}
function safe(v){ return (v == null) ? '' : String(v).trim(); }
コードのポイント解説
1) 誤送防止:SAFE_MODE
const SAFE_MODE = true; // trueの間は送信せず、ログだけ出す
- ループ内で送信直前にガード:
if (SAFE_MODE) { Logger.log('[DRY RUN] to=%s finalId=%s file=%s', email, finalId, info.name); continue; // 以降(実送信&履歴更新)へ進まない } - SAFE_MODE=false に切り替えたときだけ送られます。
2) PDFの正確なひも付け
- フォルダ内を走査して正規表現で解析:
^([A-Za-z0-9\-]+(?:\([^)]+\))?)_(?:[Aa]bstract)_(\d{8}_\d{6})\.pdf$- 例:
Y1A7(e024XX)_abstract_20250919_175608.pdf - 先頭グループから括弧以降を落として「最終演題番号」を抽出
- 同一演題で複数あれば TS(YYYYMMDD_HHMMSS) 最大の1件=最新だけ採用
- 例:
3) 二重送信の防止
- 行に保存された
完成版PDF_TSと、今回選んだPDFのTSを比較- 同じなら スキップ(送らない)
- 変わっていれば 再送(差し替わり)
4) 宛名と文面
- 宛名は「所属 氏名 先生」で冒頭に差し込み:
const fullName = `${safe(row[lastNameCol])} ${safe(row[firstNameCol])}`.trim();
const affil = safe(row[affilCol]);
const recipientLine = (fullName || affil) ? `${affil} ${fullName} 先生\n\n` : '';
const subject = FINAL_MAIL_SUBJECT(finalId);
const body = recipientLine + FINAL_MAIL_BODY(finalId, info.name);
- 件名・本文テンプレ(例):
const FINAL_MAIL_SUBJECT = (finalId) =>
`【〇〇学会】演題採択のお知らせ(演題番号:${finalId})`;
const FINAL_MAIL_BODY = (finalId, pdfName) =>
`〇〇学会 運営事務局でございます。
ご登録いただいた演題は、後述の演題番号にて採択されました。
抄録PDFを添付いたしましたので、改めて内容をご確認ください(${pdfName})。
もし誤り等の修正が必要な事項がございましたら、演題番号と合わせて、事務局まで早急にお知らせください。
演題番号:${finalId}
なお、発表日時は学会ホームページをご参照ください。
https://example.org/program
────────────────────
〇〇学会 運営事務局
E-mail: contact@example.org
────────────────────
※本メールは自動送信です。`;
実行のながれ(DRY RUN → 本番)
- SAFE_MODE = true のまま「▶実行」
- 実行ログに
[DRY RUN] to=... finalId=... file=<選ばれたPDF名>が並ぶ(送信なし/履歴更新なし)
- 実行ログに
- PDFひも付けや宛名が期待通りか確認
- 問題なければ SAFE_MODE = false に変更して再実行 → 実送信
- シートに「送信済み/送信時刻/PDF_TS/PDFファイル名」を記録
送られるメール(例)
件名
【〇〇学会】演題採択のお知らせ(演題番号:Y1A7)
本文
〇〇大学医学部 しろふわ 太郎 先生
〇〇学会 運営事務局でございます。
ご登録いただいた演題は、後述の演題番号にて採択されました。
抄録PDFを添付いたしましたので、改めて内容をご確認ください(Y1A7(e024XX)_abstract_20250919_175608.pdf)。
もし誤り等の修正が必要な事項がございましたら、演題番号と合わせて、事務局まで早急にお知らせください。
演題番号:Y1A7
なお、発表日時は学会ホームページをご参照ください。
https://example.org/program
────────────────────
〇〇学会 運営事務局
E-mail: contact@example.org
────────────────────
※本メールは自動送信です。
トラブルシューティング
- 「PDF未発見: <演題番号>」が出る
- 完成版PDFフォルダIDの誤り/PDF名が規則外/最終演題番号が空
- 同じ人に再送されない
- 既に同じTSで送信済み。PDFを差し替えたらファイル名のTSが更新されるか確認
- 送信が急に止まる
- Gmailの送信上限に注意。レート制限回避で
Utilities.sleep(500)を挿入済み
- Gmailの送信上限に注意。レート制限回避で
- 実行したのにメールが増えない
SAFE_MODE=trueのままかも。ログに[DRY RUN]が出ているはず
セキュリティと拡張のヒント
- より堅牢に:PDF生成時にファイルIDをシートへ保存し、IDで添付(名前一致に依存しない)
- 誤送のさらなる防止:
- スクリプトプロパティで「CONFIRM_SEND=OK」が無いと送れないガードを追加
- 本番前に「下書き作成モード」に切り替える(
GmailApp.createDraft)
- 差出人名の統一:
GmailApp.sendEmail(..., { name: '〇〇学会 運営事務局' })
まとめ
- PDF名のルールを決めておけば、最終演題番号 × 最新TSで正確にひも付け可能
- SAFE_MODE でドライラン → 本番 の二段構えで誤送を防止
- 送信履歴をシートに残すことで、後追い・再送条件も明確に
この設計は、演題採択通知以外(査読結果、座長案内、参加証PDF など)にも横展開できます。文面テンプレとフォルダ、ファイル名の規則を差し替えるだけで再利用できるはず。

コメント