/*************************************** * 承認ボタン付きメールシステム(公開サンプル版) * main / date / template / logs 構成 * * 使い方: * 1. Googleスプレッドシートを作成 * 2. 拡張機能 → Apps Script を開く * 3. このコードを丸ごと貼り付けて保存 * 4. スプレッドシートを再読み込み * 5. 「承認メール」メニューから初期シートを作成 ***************************************/ const SHEET_MAIN = 'main'; const SHEET_DATA = 'date'; const SHEET_TEMPLATE = 'template'; const SHEET_LOGS = 'logs'; const STATUS_PENDING = '未承認'; const STATUS_APPROVED = '承認済み'; /** * スプレッドシートを開いた時にメニューを追加 */ function onOpen() { SpreadsheetApp.getUi() .createMenu('承認メール') .addItem('初期シート確認・作成', 'setupApprovalMailSystem') .addSeparator() .addItem('トークン発行', 'issueTokensForSelectedGroup') .addItem('送信前プレビュー', 'previewSelectedGroupEmail') .addItem('テスト送信', 'sendTestEmailForSelectedGroup') .addSeparator() .addItem('本番送信', 'sendProductionEmailsForSelectedGroup') .addSeparator() .addItem('承認状況サマリー表示', 'showApprovalSummaryForSelectedGroup') .addToUi(); } /** * 初期シート確認・作成 * 既存データは消さず、不足シート・不足列だけ追加 */ function setupApprovalMailSystem() { const ss = SpreadsheetApp.getActiveSpreadsheet(); let main = ss.getSheetByName(SHEET_MAIN); if (!main) { main = ss.insertSheet(SHEET_MAIN); main.getRange(1, 1, 1, 2).setValues([['設定項目', '設定値']]); main.getRange(2, 1, 7, 2).setValues([ ['送信グループ', 'サンプル案件'], ['件名', '【確認依頼】$1'], ['テスト送信先', 'sample@example.com'], ['WebアプリURL', 'ここにWebアプリURLを貼り付ける'], ['送信者名', 'サンプル事務局'], ['承認ボタン文言', '承認する'], ['承認ボタン色', '#1a73e8'] ]); } else { ensureMainSettings(main); } main.setFrozenRows(1); main.setColumnWidth(1, 220); main.setColumnWidth(2, 760); main.getRange('A:A').setFontWeight('bold'); let data = ss.getSheetByName(SHEET_DATA); const dataHeaders = [ '区分', '宛先', 'CC', 'BCC', '$0', '$1', '$2', '$3', '$4', '$5', '$6', '$7', '$8', '$9', 'status', 'approved_at', 'token', 'sent_at' ]; if (!data) { data = ss.insertSheet(SHEET_DATA); data.getRange(1, 1, 1, dataHeaders.length).setValues([dataHeaders]); data.getRange(2, 1, 1, dataHeaders.length).setValues([[ 'サンプル案件', 'sample@example.com', '', '', '山田 太郎', 'ホームページ掲載内容の確認', '掲載予定の文章に問題がないかご確認ください。', '', '', '', '', '', '', '', '', '', '', '' ]]); } else { ensureHeaders(data, dataHeaders); } data.setFrozenRows(1); data.autoResizeColumns(1, Math.min(data.getLastColumn(), dataHeaders.length)); let template = ss.getSheetByName(SHEET_TEMPLATE); const templateHeaders = ['件名', 'メール内容', '送信時期', 'メモ']; if (!template) { template = ss.insertSheet(SHEET_TEMPLATE); template.getRange(1, 1, 1, templateHeaders.length).setValues([templateHeaders]); const defaultBody = `$0 様 お世話になっております。 サンプル事務局です。 下記の件について、内容をご確認ください。 【確認事項】 $1 $2 問題がなければ、以下の承認ボタンをクリックしてください。 {{承認ボタン}} 内容に修正が必要な場合は、承認ボタンは押さず、このメールに返信してください。 よろしくお願いいたします。 -- サンプル事務局 メール:sample@example.com URL:https://example.com/`; template.getRange(2, 1, 1, templateHeaders.length).setValues([[ '【確認依頼】$1', defaultBody, '', 'サンプル用テンプレート' ]]); } else { ensureTemplateHeaders(template); } template.setFrozenRows(1); template.setColumnWidth(1, 420); template.setColumnWidth(2, 900); template.setColumnWidth(3, 180); template.setColumnWidth(4, 280); template.getRange('B:B').setWrap(true); let logs = ss.getSheetByName(SHEET_LOGS); if (!logs) { logs = ss.insertSheet(SHEET_LOGS); logs.getRange(1, 1, 1, 8).setValues([[ 'timestamp', 'action', 'group', 'name', 'email', 'status', 'message', 'user_agent' ]]); } logs.setFrozenRows(1); SpreadsheetApp.getUi().alert( '初期シート確認が完了しました。\n\n' + '次に、Apps ScriptをWebアプリとしてデプロイし、mainシートの「WebアプリURL」に貼り付けてください。' ); } /** * mainシートに不足設定があれば追加 */ function ensureMainSettings(sheet) { const values = sheet.getDataRange().getDisplayValues(); const existingKeys = values.map(row => row[0]); const defaults = [ ['送信グループ', 'サンプル案件'], ['件名', '【確認依頼】$1'], ['テスト送信先', 'sample@example.com'], ['WebアプリURL', 'ここにWebアプリURLを貼り付ける'], ['送信者名', 'サンプル事務局'], ['承認ボタン文言', '承認する'], ['承認ボタン色', '#1a73e8'] ]; defaults.forEach(function(item) { if (!existingKeys.includes(item[0])) { const nextRow = sheet.getLastRow() + 1; sheet.getRange(nextRow, 1, 1, 2).setValues([[item[0], item[1]]]); } }); } /** * 指定ヘッダーが不足していれば右端に追加 */ function ensureHeaders(sheet, requiredHeaders) { const lastColumn = Math.max(sheet.getLastColumn(), 1); const headers = sheet.getRange(1, 1, 1, lastColumn).getDisplayValues()[0]; requiredHeaders.forEach(function(header) { if (!headers.includes(header)) { sheet.getRange(1, sheet.getLastColumn() + 1).setValue(header); } }); } /** * templateシートのヘッダー確認 */ function ensureTemplateHeaders(sheet) { ensureHeaders(sheet, ['件名', 'メール内容', '送信時期', 'メモ']); } /** * mainシート設定を取得 */ function getMainConfig() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(SHEET_MAIN); if (!sheet) { throw new Error('mainシートがありません。先に「初期シート確認・作成」を実行してください。'); } const values = sheet.getDataRange().getDisplayValues(); const config = {}; for (let i = 1; i < values.length; i++) { const key = values[i][0]; const value = values[i][1]; if (key) { config[key] = value; } } return config; } /** * ヘッダーマップ作成 */ function getHeaderMap(sheet) { const lastColumn = sheet.getLastColumn(); const headers = sheet.getRange(1, 1, 1, lastColumn).getDisplayValues()[0]; const map = {}; headers.forEach(function(header, index) { if (header) { map[String(header)] = index; } }); return map; } /** * mainの件名に一致するtemplate行を取得 */ function getTemplateBySubject(subjectKey) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(SHEET_TEMPLATE); if (!sheet) { throw new Error('templateシートがありません。'); } const headerMap = getHeaderMap(sheet); if (headerMap['件名'] === undefined) { throw new Error('templateシートに「件名」列がありません。'); } if (headerMap['メール内容'] === undefined) { throw new Error('templateシートに「メール内容」列がありません。'); } const values = sheet.getDataRange().getValues(); const matches = []; for (let i = 1; i < values.length; i++) { const subject = values[i][headerMap['件名']]; if (String(subject) === String(subjectKey)) { matches.push({ rowNumber: i + 1, subject: subject, body: values[i][headerMap['メール内容']] }); } } if (matches.length === 0) { throw new Error('templateシートに件名「' + subjectKey + '」が見つかりません。'); } if (matches.length > 1) { throw new Error( 'templateシートに同じ件名「' + subjectKey + '」が複数あります。\n\n' + '件名でメール本文を選択するため、件名は重複しないようにしてください。' ); } return matches[0]; } /** * mainで選択されている送信グループの行をdateシートから取得 */ function getRowsBySelectedGroup() { const config = getMainConfig(); const group = config['送信グループ']; if (!group) { throw new Error('mainシートの「送信グループ」が空欄です。'); } const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(SHEET_DATA); if (!sheet) { throw new Error('dateシートがありません。'); } const headerMap = getHeaderMap(sheet); if (headerMap['区分'] === undefined) { throw new Error('dateシートに「区分」列がありません。'); } const values = sheet.getDataRange().getDisplayValues(); const rows = []; for (let i = 1; i < values.length; i++) { const rowGroup = values[i][headerMap['区分']]; if (String(rowGroup) === String(group)) { rows.push({ rowNumber: i + 1, values: values[i] }); } } return { config: config, group: group, sheet: sheet, headerMap: headerMap, rows: rows }; } /** * 選択グループにトークン発行 */ function issueTokensForSelectedGroup() { try { const data = getRowsBySelectedGroup(); if (data.rows.length === 0) { SpreadsheetApp.getUi().alert('送信グループ「' + data.group + '」に該当する行がありません。'); return; } const tokenCol = getRequiredColumn(data.headerMap, 'token') + 1; const statusCol = getRequiredColumn(data.headerMap, 'status') + 1; let issuedCount = 0; data.rows.forEach(function(item) { const currentToken = item.values[data.headerMap['token']]; const currentStatus = item.values[data.headerMap['status']]; if (!currentToken) { data.sheet.getRange(item.rowNumber, tokenCol).setValue(Utilities.getUuid()); issuedCount++; } if (!currentStatus) { data.sheet.getRange(item.rowNumber, statusCol).setValue(STATUS_PENDING); } }); SpreadsheetApp.getUi().alert( 'トークン発行完了\n\n' + '送信グループ:' + data.group + '\n' + '対象者:' + data.rows.length + '件\n' + '新規発行:' + issuedCount + '件' ); } catch (err) { SpreadsheetApp.getUi().alert(err.message); } } /** * 送信前プレビュー */ function previewSelectedGroupEmail() { try { const emailData = buildFirstEmailData(true); const html = `
プレビュー対象
送信グループ:${escapeHtml(emailData.group)}
template選択件名:${escapeHtml(emailData.templateSubject)}
宛先:${escapeHtml(emailData.to)}
CC:${escapeHtml(emailData.cc)}
BCC:${escapeHtml(emailData.bcc)}
氏名:${escapeHtml(emailData.name)}
送信件名
${escapeHtml(emailData.subject)}
本文プレビュー
${emailData.htmlBody}
※これはプレビューです。メールは送信されていません。
`; SpreadsheetApp.getUi().showModalDialog( HtmlService.createHtmlOutput(html).setWidth(820).setHeight(760), '送信前プレビュー' ); } catch (err) { SpreadsheetApp.getUi().alert(err.message); } } /** * テスト送信 * テスト送信では承認記録を保存しない test_approve URL を使う */ function sendTestEmailForSelectedGroup() { try { const config = getMainConfig(); const testEmail = config['テスト送信先']; if (!testEmail || String(testEmail).indexOf('@') === -1) { SpreadsheetApp.getUi().alert('mainシートの「テスト送信先」にメールアドレスを入力してください。'); return; } const emailData = buildFirstEmailData(true); const htmlBody = '
' + 'これはテスト送信です。
' + '本番対象者には送信されていません。
' + '実際の宛先想定:' + escapeHtml(emailData.to) + '
' + emailData.htmlBody; const plainBody = `【これはテスト送信です】 本番対象者には送信されていません。 実際の宛先想定:${emailData.to} ------------------------------ ${emailData.plainBody}`; MailApp.sendEmail({ to: testEmail, subject: '【テスト送信】' + emailData.subject, body: plainBody, htmlBody: htmlBody, name: emailData.senderName }); writeLog('test_send', emailData.group, emailData.name, testEmail, 'sent', 'テスト送信'); SpreadsheetApp.getUi().alert('テスト送信が完了しました。\n\n送信先:' + testEmail); } catch (err) { SpreadsheetApp.getUi().alert(err.message); } } /** * 本番送信 */ function sendProductionEmailsForSelectedGroup() { try { const ui = SpreadsheetApp.getUi(); const data = getRowsBySelectedGroup(); if (data.rows.length === 0) { ui.alert('送信対象がありません。'); return; } const subjectKey = data.config['件名']; if (!subjectKey) { ui.alert('mainシートの「件名」が空欄です。'); return; } getTemplateBySubject(subjectKey); const result = ui.alert( '本番送信確認', '送信グループ「' + data.group + '」に本番送信します。\n\n' + '選択件名:' + subjectKey + '\n' + '対象行:' + data.rows.length + '件\n\n' + '実行しますか?', ui.ButtonSet.YES_NO ); if (result !== ui.Button.YES) { return; } sendEmailsForGroup(); } catch (err) { SpreadsheetApp.getUi().alert(err.message); } } /** * グループ送信処理 */ function sendEmailsForGroup() { const data = getRowsBySelectedGroup(); const config = data.config; const webAppUrl = config['WebアプリURL']; if (!webAppUrl || String(webAppUrl).includes('ここに')) { throw new Error('mainシートの「WebアプリURL」にデプロイ後のURLを設定してください。'); } const subjectKey = config['件名']; if (!subjectKey) { throw new Error('mainシートの「件名」が空欄です。'); } const template = getTemplateBySubject(subjectKey); const statusCol = getRequiredColumn(data.headerMap, 'status') + 1; const sentAtCol = getRequiredColumn(data.headerMap, 'sent_at') + 1; const tokenCol = getRequiredColumn(data.headerMap, 'token') + 1; let sentCount = 0; let skipCount = 0; const now = new Date(); data.rows.forEach(function(item) { let token = item.values[data.headerMap['token']]; const status = item.values[data.headerMap['status']]; const sentAt = item.values[data.headerMap['sent_at']]; if (status === STATUS_APPROVED) { skipCount++; return; } if (sentAt) { skipCount++; return; } if (!token) { token = Utilities.getUuid(); data.sheet.getRange(item.rowNumber, tokenCol).setValue(token); item.values[data.headerMap['token']] = token; } if (!status) { data.sheet.getRange(item.rowNumber, statusCol).setValue(STATUS_PENDING); item.values[data.headerMap['status']] = STATUS_PENDING; } const emailData = buildEmailDataFromRow( data.group, item.values, data.headerMap, config, template, false ); if (!emailData.to) { skipCount++; return; } const options = { to: emailData.to, subject: emailData.subject, body: emailData.plainBody, htmlBody: emailData.htmlBody, name: emailData.senderName }; if (emailData.cc) { options.cc = emailData.cc; } if (emailData.bcc) { options.bcc = emailData.bcc; } MailApp.sendEmail(options); data.sheet.getRange(item.rowNumber, sentAtCol).setValue(now); writeLog('send', data.group, emailData.name, emailData.to, 'sent', '本番送信'); sentCount++; }); SpreadsheetApp.getUi().alert( '本番送信完了\n\n' + '送信グループ:' + data.group + '\n' + '送信:' + sentCount + '件\n' + 'スキップ:' + skipCount + '件' ); } /** * プレビュー・テスト送信用に、選択グループの最初の有効行でメールデータ作成 */ function buildFirstEmailData(isTest) { const data = getRowsBySelectedGroup(); if (data.rows.length === 0) { throw new Error('送信グループ「' + data.group + '」に該当する行がdateシートにありません。'); } const subjectKey = data.config['件名']; if (!subjectKey) { throw new Error('mainシートの「件名」が空欄です。'); } const template = getTemplateBySubject(subjectKey); const tokenCol = getRequiredColumn(data.headerMap, 'token') + 1; const statusCol = getRequiredColumn(data.headerMap, 'status') + 1; const first = data.rows[0]; if (!first.values[data.headerMap['token']]) { const newToken = Utilities.getUuid(); data.sheet.getRange(first.rowNumber, tokenCol).setValue(newToken); first.values[data.headerMap['token']] = newToken; } if (!first.values[data.headerMap['status']]) { data.sheet.getRange(first.rowNumber, statusCol).setValue(STATUS_PENDING); first.values[data.headerMap['status']] = STATUS_PENDING; } return buildEmailDataFromRow( data.group, first.values, data.headerMap, data.config, template, isTest ); } /** * 1行分のメールデータ作成 */ function buildEmailDataFromRow(group, rowValues, headerMap, config, template, isTest) { const to = getValue(rowValues, headerMap, '宛先'); const cc = getValue(rowValues, headerMap, 'CC'); const bcc = getValue(rowValues, headerMap, 'BCC'); const token = getValue(rowValues, headerMap, 'token'); const webAppUrl = config['WebアプリURL']; if (!webAppUrl || String(webAppUrl).includes('ここに')) { throw new Error('mainシートの「WebアプリURL」にデプロイ後のURLを設定してください。'); } const action = isTest ? 'test_approve' : 'approve'; // 送信グループ名をURLに含めず、tokenだけで承認対象を判定します。 const approveUrl = webAppUrl + '?action=' + encodeURIComponent(action) + '&token=' + encodeURIComponent(token); const buttonLabel = config['承認ボタン文言'] || '承認する'; const buttonColor = sanitizeColor(config['承認ボタン色'] || '#1a73e8'); const approveButtonHtml = createApproveButtonHtml(approveUrl, buttonLabel, buttonColor); const rowMapPlain = buildRowMap(rowValues, headerMap, false); const rowMapHtml = buildRowMap(rowValues, headerMap, true); rowMapPlain['承認URL'] = approveUrl; rowMapPlain['承認ボタン'] = approveUrl; rowMapHtml['承認URL'] = escapeHtml(approveUrl); rowMapHtml['承認ボタン'] = approveButtonHtml; const subject = mergeTemplate(template.subject, rowMapPlain); const plainBody = mergeTemplate(template.body, rowMapPlain); const htmlBody = createHtmlMailBody( mergeTemplate(template.body, rowMapHtml) ); const warnings = findUnreplacedTags(subject + '\n' + plainBody); if (warnings.length > 0) { Logger.log('未置換タグがあります: ' + warnings.join(', ')); } return { group: group, templateSubject: template.subject, to: to, cc: cc, bcc: bcc, name: getValue(rowValues, headerMap, '$0'), subject: subject, plainBody: plainBody, htmlBody: htmlBody, senderName: config['送信者名'] || 'サンプル事務局' }; } /** * 行データを差し込み用Mapに変換 */ function buildRowMap(rowValues, headerMap, htmlMode) { const map = {}; Object.keys(headerMap).forEach(function(header) { const rawValue = rowValues[headerMap[header]]; map[header] = htmlMode ? escapeHtml(rawValue) : String(rawValue || ''); }); return map; } /** * テンプレート差し込み */ function mergeTemplate(templateText, map) { let result = String(templateText || ''); const dollarKeys = Object.keys(map) .filter(function(key) { return /^\$\d+$/.test(key); }) .sort(function(a, b) { return Number(b.substring(1)) - Number(a.substring(1)); }); dollarKeys.forEach(function(key) { result = result.split(key).join(map[key]); }); Object.keys(map).forEach(function(key) { result = result.split('{{' + key + '}}').join(map[key]); }); return result; } /** * 未置換タグ検出 */ function findUnreplacedTags(text) { const result = []; const regex = /{{[^}]+}}|\$\d+/g; let match; while ((match = regex.exec(text)) !== null) { if (!result.includes(match[0])) { result.push(match[0]); } } return result; } /** * 承認ボタンHTML */ function createApproveButtonHtml(approveUrl, label, color) { return '

' + '' + escapeHtml(label) + '' + '

' + '

' + 'ボタンが押せない場合は、以下のURLをブラウザに貼り付けてください。
' + '' + escapeHtml(approveUrl) + '' + '

'; } /** * ボタン色の簡易チェック */ function sanitizeColor(color) { const value = String(color || '').trim(); if (/^#[0-9a-fA-F]{6}$/.test(value)) { return value; } if (/^#[0-9a-fA-F]{3}$/.test(value)) { return value; } return '#1a73e8'; } /** * HTMLメール本文化 */ function createHtmlMailBody(body) { return '
' + String(body || '').replace(/\n/g, '
') + '
'; } /** * 承認URLクリック時 */ function doGet(e) { const action = e && e.parameter ? e.parameter.action : ''; const token = e && e.parameter ? e.parameter.token : ''; if (action === 'test_approve') { return HtmlService.createHtmlOutput(createMessageHtml( 'テスト用承認ボタンです', 'これはテスト送信用のボタンです。承認記録は保存されていません。' )); } if (action !== 'approve') { return HtmlService.createHtmlOutput(createMessageHtml( '無効なアクセスです', '承認用URLが正しくありません。' )); } if (!token) { return HtmlService.createHtmlOutput(createMessageHtml( '承認できませんでした', '必要な情報が不足しています。' )); } const result = approveByToken(token, e); return HtmlService.createHtmlOutput(createMessageHtml( result.title, result.message )); } /** * 承認処理 */ function approveByToken(token, e) { const lock = LockService.getScriptLock(); try { lock.waitLock(10000); const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName(SHEET_DATA); if (!sheet) { return { success: false, title: '承認できませんでした', message: 'dateシートが見つかりません。' }; } const headerMap = getHeaderMap(sheet); const groupIndex = getRequiredColumn(headerMap, '区分'); const tokenIndex = getRequiredColumn(headerMap, 'token'); const statusIndex = getRequiredColumn(headerMap, 'status'); const approvedAtIndex = getRequiredColumn(headerMap, 'approved_at'); const nameIndex = headerMap['$0']; const emailIndex = headerMap['宛先']; const values = sheet.getDataRange().getDisplayValues(); for (let i = 1; i < values.length; i++) { const rowToken = values[i][tokenIndex]; if (String(rowToken) === String(token)) { const group = values[i][groupIndex]; const name = nameIndex !== undefined ? values[i][nameIndex] : ''; const email = emailIndex !== undefined ? values[i][emailIndex] : ''; const status = values[i][statusIndex]; if (status === STATUS_APPROVED) { writeLog('duplicate_approve', group, name, email, status, 'すでに承認済み', getUserAgent(e)); return { success: true, title: '承認済みです', message: 'この確認依頼はすでに承認済みです。' }; } const now = new Date(); sheet.getRange(i + 1, statusIndex + 1).setValue(STATUS_APPROVED); sheet.getRange(i + 1, approvedAtIndex + 1).setValue(now); writeLog('approve', group, name, email, STATUS_APPROVED, '承認完了', getUserAgent(e)); return { success: true, title: '承認を受け付けました', message: 'ご対応ありがとうございました。この画面は閉じていただいて構いません。' }; } } return { success: false, title: '承認できませんでした', message: '承認対象が見つかりませんでした。URLが無効、または期限切れの可能性があります。' }; } catch (err) { return { success: false, title: '承認できませんでした', message: '処理中にエラーが発生しました。しばらくしてから再度お試しください。' }; } finally { try { lock.releaseLock(); } catch (err) {} } } /** * 承認状況サマリー */ function showApprovalSummaryForSelectedGroup() { try { const data = getRowsBySelectedGroup(); if (data.rows.length === 0) { SpreadsheetApp.getUi().alert('送信グループ「' + data.group + '」に該当する行がありません。'); return; } const statusIndex = getRequiredColumn(data.headerMap, 'status'); let total = 0; let approved = 0; let pending = 0; data.rows.forEach(function(item) { const status = item.values[statusIndex]; total++; if (status === STATUS_APPROVED) { approved++; } else { pending++; } }); SpreadsheetApp.getUi().alert( '承認状況\n\n' + '送信グループ:' + data.group + '\n' + '対象者:' + total + '件\n' + '承認済:' + approved + '件\n' + '未承認:' + pending + '件' ); } catch (err) { SpreadsheetApp.getUi().alert(err.message); } } /** * ログ記録 */ function writeLog(action, group, name, email, status, message, userAgent) { const ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getSheetByName(SHEET_LOGS); if (!sheet) { sheet = ss.insertSheet(SHEET_LOGS); sheet.getRange(1, 1, 1, 8).setValues([[ 'timestamp', 'action', 'group', 'name', 'email', 'status', 'message', 'user_agent' ]]); } sheet.appendRow([ new Date(), action, group, name, email, status, message, userAgent || '' ]); } /** * 必須列取得 */ function getRequiredColumn(headerMap, headerName) { if (headerMap[headerName] === undefined) { throw new Error('必要な列「' + headerName + '」が見つかりません。'); } return headerMap[headerName]; } /** * 値取得 */ function getValue(rowValues, headerMap, headerName) { if (headerMap[headerName] === undefined) { return ''; } return String(rowValues[headerMap[headerName]] || ''); } /** * ユーザーエージェント取得 */ function getUserAgent(e) { try { return e && e.headers && e.headers['User-Agent'] ? e.headers['User-Agent'] : ''; } catch (err) { return ''; } } /** * 完了画面HTML */ function createMessageHtml(title, message) { return ` ${escapeHtml(title)}

${escapeHtml(title)}

${escapeHtml(message)}

この画面は閉じていただいて構いません。

`; } /** * HTMLエスケープ */ function escapeHtml(text) { if (text === null || text === undefined) { return ''; } return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }