Gmailの特定メールをLINEグループへ自動転送する方法【Google Apps Script+Messaging API】
Gmailに届く特定のメールについて、「件名と本文だけLINEグループにも通知したい」ということがあります。
たとえば、
- 学校や塾から届くメールを家族のLINEグループにも知らせたい
- 特定サービスからの重要なメールだけチームのLINEグループへ送りたい
- Gmailを毎回確認しなくても、LINEで内容を確認できるようにしたい
といった用途です。
以前であればLINE Notifyを使う方法が簡単でしたが、LINE Notifyは2025年3月31日でサービスを終了しています。現在はLINE公式アカウントのMessaging APIを使うのが代替方法になります。
そこで今回は、
Gmail → Google Apps Script → LINE Messaging API → LINEグループ
という構成で、特定の送信元から届いたGmailの「件名+本文」をLINEグループへ自動転送してみます。
一度設定してしまえば、あとは自動です。
今回作るもの
完成後の流れは次のようになります。
特定の送信元
│
│ メール
▼
┌──────────────┐
│ Gmail │
└──────────────┘
│
│ 1分ごとに確認
▼
┌─────────────────────┐
│ Google Apps Script │
│ │
│ ・送信元を確認 │
│ ・件名を取得 │
│ ・本文を取得 │
│ ・転送済みか確認 │
└─────────────────────┘
│
│ Messaging API
▼
┌─────────────────────┐
│ LINE公式アカウント │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ LINEグループ │
└─────────────────────┘
LINEには、このようなメッセージが届きます。
📩 Gmail
件名: ○○についてのお知らせ
本文:
お世話になっております。
○○についてご連絡いたします。
……
LINE公式アカウントが参加しているグループであれば、Messaging APIのPush Messageを使ってグループID宛てにメッセージを送信できます。送信されたメッセージはグループのメンバー全員に表示されます。
LINE公式アカウントは、個人でも作成可能です。
必要なもの
今回必要になるのは次の3つです。
- Gmail
- Google Apps Script
- LINE公式アカウント+Messaging API
サーバーを自分で用意する必要はありません。
Google Apps ScriptをGmailのチェック処理とLINEへの送信処理の両方に利用します。
1.LINE公式アカウントを作成する
まず、Gmail通知専用のLINE公式アカウントを作成します。
LINE Official Account Managerから新しいLINE公式アカウントを作成します。
名前は、
Gmail通知
などでよいでしょう。
Messaging APIを利用するためには、まずLINE公式アカウントを作成し、そのアカウントでMessaging APIを有効にする必要があります。
2.Messaging APIを有効にする
LINE Official Account Managerで、作成した公式アカウントのMessaging APIを有効にします。


Messaging APIを有効にすると、LINE Developers Console側にMessaging APIチャネルが作成されます。
LINE Developers Consoleへ移動し、作成されたチャネルを確認します。
3.グループへの参加を許可する
今回の目的は個人宛てではなく、LINEグループへの自動送信です。
そのため、Messaging APIチャネルの設定で、
「グループトーク・複数人トークへの参加を許可する」
を有効にします。
この設定はデフォルトでは無効になっています。

また、1つのLINEグループに同時に参加できるLINE公式アカウントは1つです。
設定したら、「Gmail通知」として作成したLINE公式アカウントを、実際にメールを転送したいLINEグループへ招待しておきます。
4.チャネルアクセストークンを取得する
次にLINE Developers Consoleから、Messaging APIで使用するチャネルアクセストークンを取得します。
Messaging APIチャネルの「Messaging API設定」を開き、チャネルアクセストークンを発行します。
例えば次のような長い文字列です。
eyJhbGciOiJIUzI1NiJ9.xxxxxxxxxxxxxxxxxxxxx
これはパスワードに近い重要な情報なので、第三者には公開しないようにします。

LINEには複数種類のチャネルアクセストークンがあります。長期チャネルアクセストークンはMessaging APIチャネルで利用でき、有効期限はありません。一方、LINEではセキュリティの観点から、より短い有効期限を持つトークンなどの利用も推奨しています。
個人用途の小規模な自動通知では設定の簡単な長期トークンを使う方法もありますが、漏洩した可能性がある場合は必ず無効化・再発行します。
5.Google Apps Scriptを作成する
Google Apps Scriptを開いて「新しいプロジェクト」を作成します。

プロジェクト名は、
Gmail to LINE Group
などでよいでしょう。
コード.gs に最初から入っているコードをすべて削除して、後述する完成版コードを貼り付けます。
6.スクリプトプロパティを設定する
メールアドレスやLINEのアクセストークンをプログラム内へ直接書くのは避けます。
Apps Scriptの、
プロジェクトの設定 → スクリプト プロパティ

を開き、次の3つを登録します。
| プロパティ名 | 内容 |
|---|---|
TARGET_SENDER | LINEへ転送したいメールの送信元 |
LINE_CHANNEL_ACCESS_TOKEN | LINEのチャネルアクセストークン |
GROUP_REGISTRATION_CODE | LINEグループ登録用の秘密コード |
例えば、
TARGET_SENDER
information@example.com
GROUP_REGISTRATION_CODE
M8q2Xk7R4p
のように登録します。
GROUP_REGISTRATION_CODE は、自由に設定できますが、
1234
のような単純なものではなく、10文字程度以上のランダムな文字列にすることをおすすめします。
後ほどLINEグループから自動取得して保存します。
7.完成版Google Apps Script

プロジェクト名を任意に入力し、以下をコード.gsへ貼り付けます。
/**
* Gmail -> LINE Group Forwarder
*
* Script Properties required:
* TARGET_SENDER
* LINE_CHANNEL_ACCESS_TOKEN
* GROUP_REGISTRATION_CODE
*
* Auto-created:
* LINE_GROUP_ID
* LINE_GROUP_NAME
*/
const CONFIG = {
// Gmailで何日前まで検索するか
SEARCH_DAYS: 7,
// 一度に検索する最大スレッド数
MAX_THREADS: 500,
// LINEは5000文字まで。
// 安全のため4500文字に制限する。
MAX_LINE_CHARS: 4500,
// 処理済みIDを何日保持するか
PROCESSED_RETENTION_DAYS: 10,
// 処理済みメールID保存用
PROCESSED_PREFIX: 'PROCESSED_',
// LINEグループ登録を何分間有効にするか
REGISTRATION_WINDOW_MINUTES: 10
};
/**
* Webアプリの動作確認用
*/
function doGet() {
return ContentService
.createTextOutput(
'Gmail -> LINE Group webhook is running.'
)
.setMimeType(
ContentService.MimeType.TEXT
);
}
/**
* LINEからWebhookを受信
*/
function doPost(e) {
try {
if (
!e ||
!e.postData ||
!e.postData.contents
) {
return textResponse_('OK');
}
const body =
JSON.parse(
e.postData.contents
);
const events =
Array.isArray(body.events)
? body.events
: [];
events.forEach(event => {
if (
event.type === 'message' &&
event.message &&
event.message.type === 'text' &&
event.source &&
event.source.type === 'group'
) {
handleGroupRegistrationEvent_(
event
);
}
});
} catch (err) {
console.error(
err && err.stack
? err.stack
: err
);
}
return textResponse_('OK');
}
/**
* LINEグループ登録を10分間有効にする
*/
function enableGroupRegistration() {
const props =
PropertiesService
.getScriptProperties();
const code =
(
props.getProperty(
'GROUP_REGISTRATION_CODE'
) || ''
).trim();
const token =
(
props.getProperty(
'LINE_CHANNEL_ACCESS_TOKEN'
) || ''
).trim();
if (!code) {
throw new Error(
'GROUP_REGISTRATION_CODE が設定されていません。'
);
}
if (!token) {
throw new Error(
'LINE_CHANNEL_ACCESS_TOKEN が設定されていません。'
);
}
const expiresAt =
Date.now() +
CONFIG.REGISTRATION_WINDOW_MINUTES *
60 *
1000;
props.setProperty(
'GROUP_REGISTRATION_EXPIRES_AT',
String(expiresAt)
);
console.log(
'グループ登録を有効にしました。\n' +
CONFIG.REGISTRATION_WINDOW_MINUTES +
'分以内にLINEグループで次の文字列を送信してください。\n\n' +
'転送先登録 ' +
code
);
}
/**
* LINEグループ登録を無効にする
*/
function disableGroupRegistration() {
PropertiesService
.getScriptProperties()
.deleteProperty(
'GROUP_REGISTRATION_EXPIRES_AT'
);
console.log(
'グループ登録を無効にしました。'
);
}
/**
* グループ登録メッセージを処理
*/
function handleGroupRegistrationEvent_(
event
) {
const props =
PropertiesService
.getScriptProperties();
const expiresAt =
Number(
props.getProperty(
'GROUP_REGISTRATION_EXPIRES_AT'
) || '0'
);
if (
!expiresAt ||
Date.now() > expiresAt
) {
return;
}
const code =
(
props.getProperty(
'GROUP_REGISTRATION_CODE'
) || ''
).trim();
if (!code) {
return;
}
const expected =
'転送先登録 ' + code;
const received =
(
event.message.text || ''
).trim();
if (received !== expected) {
return;
}
const groupId =
event.source.groupId;
if (!groupId) {
return;
}
const token =
(
props.getProperty(
'LINE_CHANNEL_ACCESS_TOKEN'
) || ''
).trim();
if (!token) {
return;
}
let groupName = '';
try {
groupName =
getLineGroupName_(
groupId,
token
);
} catch (err) {
console.error(
'グループ名取得に失敗: ' +
err
);
}
props.setProperty(
'LINE_GROUP_ID',
groupId
);
props.setProperty(
'LINE_GROUP_NAME',
groupName ||
'(グループ名取得失敗)'
);
props.deleteProperty(
'GROUP_REGISTRATION_EXPIRES_AT'
);
if (event.replyToken) {
const message =
'✅ Gmail転送先として登録しました。' +
(
groupName
? '\nグループ: ' +
groupName
: ''
) +
'\n\n以後、対象メールの件名と本文をこのグループへ送信します。';
try {
replyLineMessage_(
event.replyToken,
message,
token
);
} catch (err) {
console.error(
'登録完了メッセージの返信に失敗: ' +
err
);
}
}
}
/**
* LINEグループ名を取得
*/
function getLineGroupName_(
groupId,
token
) {
const url =
'https://api.line.me/v2/bot/group/' +
encodeURIComponent(groupId) +
'/summary';
const response =
UrlFetchApp.fetch(
url,
{
method: 'get',
headers: {
Authorization:
'Bearer ' +
token
},
muteHttpExceptions: true
}
);
const status =
response.getResponseCode();
const body =
response.getContentText();
if (status !== 200) {
throw new Error(
'LINEグループ情報取得エラー HTTP ' +
status +
': ' +
body
);
}
const data =
JSON.parse(body);
return data.groupName || '';
}
/**
* LINEへ返信メッセージを送信
*/
function replyLineMessage_(
replyToken,
text,
token
) {
const response =
UrlFetchApp.fetch(
'https://api.line.me/v2/bot/message/reply',
{
method: 'post',
contentType:
'application/json',
headers: {
Authorization:
'Bearer ' +
token
},
payload:
JSON.stringify({
replyToken:
replyToken,
messages: [
{
type: 'text',
text: text
}
]
}),
muteHttpExceptions:
true
}
);
const status =
response.getResponseCode();
if (status !== 200) {
throw new Error(
'LINE返信エラー HTTP ' +
status +
': ' +
response.getContentText()
);
}
}
/**
* LINEグループへの送信テスト
*/
function testLineGroup() {
const settings =
getSettings_(true);
const groupName =
settings.lineGroupName
? '\nグループ: ' +
settings.lineGroupName
: '';
sendLinePush_(
'✅ Gmail → LINEグループ 転送テスト' +
groupName +
'\n\nGoogle Apps Scriptからの送信に成功しました。',
settings
);
}
/**
* 既存メールを処理済みにする
*
* 最初に1回だけ実行する
*/
function initializeForwarding() {
const settings =
getSettings_(true);
const props =
PropertiesService
.getScriptProperties();
cleanupProcessedIds_();
const query =
'from:' +
settings.targetSender +
' newer_than:' +
CONFIG.SEARCH_DAYS +
'd';
const threads =
GmailApp.search(
query,
0,
CONFIG.MAX_THREADS
);
const nowMs =
Date.now();
const now =
String(nowMs);
const messageCutoff =
nowMs -
CONFIG.SEARCH_DAYS *
24 *
60 *
60 *
1000;
const processed = {};
let count = 0;
threads.forEach(thread => {
thread
.getMessages()
.forEach(message => {
const from =
extractEmailAddress_(
message.getFrom()
);
if (
from !==
settings.targetSender
) {
return;
}
if (
message
.getDate()
.getTime() <
messageCutoff
) {
return;
}
processed[
CONFIG.PROCESSED_PREFIX +
message.getId()
] = now;
count++;
});
});
if (
Object.keys(processed)
.length > 0
) {
props.setProperties(
processed,
false
);
}
props.setProperty(
'INITIALIZED',
'true'
);
console.log(
'初期化完了: ' +
count +
' 件を処理済みにしました。'
);
}
/**
* Gmailを確認してLINEグループへ転送
*/
function checkGmailAndForwardToLine() {
const lock =
LockService.getScriptLock();
if (!lock.tryLock(5000)) {
console.log(
'別の処理が実行中のため終了します。'
);
return;
}
try {
const settings =
getSettings_(true);
const props =
PropertiesService
.getScriptProperties();
if (
props.getProperty(
'INITIALIZED'
) !== 'true'
) {
throw new Error(
'初期化されていません。先に initializeForwarding() を実行してください。'
);
}
const query =
'from:' +
settings.targetSender +
' newer_than:' +
CONFIG.SEARCH_DAYS +
'd';
const threads =
GmailApp.search(
query,
0,
CONFIG.MAX_THREADS
);
const messageCutoff =
Date.now() -
CONFIG.SEARCH_DAYS *
24 *
60 *
60 *
1000;
const messagesToSend = [];
threads.forEach(thread => {
thread
.getMessages()
.forEach(message => {
const from =
extractEmailAddress_(
message.getFrom()
);
if (
from !==
settings.targetSender
) {
return;
}
if (
message
.getDate()
.getTime() <
messageCutoff
) {
return;
}
const processedKey =
CONFIG.PROCESSED_PREFIX +
message.getId();
if (
props.getProperty(
processedKey
)
) {
return;
}
messagesToSend.push(
message
);
});
});
/*
* 古いメールから順番に送信
*/
messagesToSend.sort(
(a, b) =>
a.getDate().getTime() -
b.getDate().getTime()
);
let sentCount = 0;
messagesToSend.forEach(
message => {
const lineText =
createLineText_(
message
);
/*
* LINE送信に成功した後で
* 処理済みとして保存する
*/
sendLinePush_(
lineText,
settings
);
props.setProperty(
CONFIG.PROCESSED_PREFIX +
message.getId(),
String(Date.now())
);
sentCount++;
}
);
cleanupProcessedIds_();
console.log(
'処理完了: ' +
sentCount +
' 件をLINEグループへ送信しました。'
);
} finally {
lock.releaseLock();
}
}
/**
* 1分間隔のトリガーを作成
*/
function createMinuteTrigger() {
createTimeTrigger_(1);
}
/**
* 5分間隔のトリガーを作成
*/
function createFiveMinuteTrigger() {
createTimeTrigger_(5);
}
/**
* 自動実行トリガー作成
*/
function createTimeTrigger_(
minutes
) {
const functionName =
'checkGmailAndForwardToLine';
/*
* 同じ関数の既存トリガーを削除
*/
ScriptApp
.getProjectTriggers()
.forEach(trigger => {
if (
trigger
.getHandlerFunction() ===
functionName
) {
ScriptApp.deleteTrigger(
trigger
);
}
});
ScriptApp
.newTrigger(
functionName
)
.timeBased()
.everyMinutes(minutes)
.create();
console.log(
minutes +
'分間隔のトリガーを作成しました。'
);
}
/**
* Gmail自動転送を停止
*/
function deleteForwardingTrigger() {
const functionName =
'checkGmailAndForwardToLine';
ScriptApp
.getProjectTriggers()
.forEach(trigger => {
if (
trigger
.getHandlerFunction() ===
functionName
) {
ScriptApp.deleteTrigger(
trigger
);
}
});
console.log(
'自動転送トリガーを削除しました。'
);
}
/**
* 登録しているグループを解除
*/
function clearRegisteredGroup() {
const props =
PropertiesService
.getScriptProperties();
props.deleteProperty(
'LINE_GROUP_ID'
);
props.deleteProperty(
'LINE_GROUP_NAME'
);
props.deleteProperty(
'GROUP_REGISTRATION_EXPIRES_AT'
);
console.log(
'登録済みLINEグループ情報を削除しました。'
);
}
/**
* 現在登録しているグループを確認
*/
function showCurrentGroup() {
const props =
PropertiesService
.getScriptProperties();
console.log(
'LINE_GROUP_NAME: ' +
(
props.getProperty(
'LINE_GROUP_NAME'
) || '未登録'
) +
'\nLINE_GROUP_ID: ' +
(
props.getProperty(
'LINE_GROUP_ID'
) || '未登録'
)
);
}
/**
* LINEへ送る文章を作成
*/
function createLineText_(
message
) {
const subject =
message.getSubject() ||
'(件名なし)';
let body =
message.getPlainBody() ||
'(本文なし)';
/*
* 改行を整理
*/
body =
body
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(
/\n{3,}/g,
'\n\n'
)
.trim();
const text =
'📩 Gmail\n\n' +
'件名: ' +
subject +
'\n\n' +
'本文:\n' +
body;
return truncateForLine_(
text,
CONFIG.MAX_LINE_CHARS
);
}
/**
* LINEの文字数上限に合わせて切り詰める
*
* JavaScriptのString.lengthはUTF-16コード単位。
* LINE Messaging APIもUTF-16コード単位で
* テキスト文字数をカウントする。
*/
function truncateForLine_(
text,
maxChars
) {
if (
text.length <= maxChars
) {
return text;
}
const suffix =
'\n\n…(本文が長いため、ここで省略しました)';
let cutLength =
maxChars -
suffix.length;
let result =
text.slice(
0,
cutLength
);
/*
* サロゲートペアの途中で
* 切断しないようにする
*/
if (result.length > 0) {
const lastCode =
result.charCodeAt(
result.length - 1
);
if (
lastCode >= 0xD800 &&
lastCode <= 0xDBFF
) {
result =
result.slice(0, -1);
}
}
return result + suffix;
}
/**
* LINEグループへPush Messageを送信
*/
function sendLinePush_(
text,
settings
) {
const response =
UrlFetchApp.fetch(
'https://api.line.me/v2/bot/message/push',
{
method: 'post',
contentType:
'application/json',
headers: {
Authorization:
'Bearer ' +
settings
.lineChannelAccessToken
},
payload:
JSON.stringify({
to:
settings
.lineGroupId,
messages: [
{
type: 'text',
text: text
}
]
}),
muteHttpExceptions:
true
}
);
const status =
response.getResponseCode();
const body =
response.getContentText();
if (status !== 200) {
throw new Error(
'LINE送信エラー HTTP ' +
status +
': ' +
body
);
}
}
/**
* スクリプトプロパティを取得
*/
function getSettings_(
requireGroup
) {
const props =
PropertiesService
.getScriptProperties();
const targetSender =
(
props.getProperty(
'TARGET_SENDER'
) || ''
)
.trim()
.toLowerCase();
const lineChannelAccessToken =
(
props.getProperty(
'LINE_CHANNEL_ACCESS_TOKEN'
) || ''
).trim();
const lineGroupId =
(
props.getProperty(
'LINE_GROUP_ID'
) || ''
).trim();
const lineGroupName =
(
props.getProperty(
'LINE_GROUP_NAME'
) || ''
).trim();
if (!targetSender) {
throw new Error(
'TARGET_SENDER が設定されていません。'
);
}
if (!lineChannelAccessToken) {
throw new Error(
'LINE_CHANNEL_ACCESS_TOKEN が設定されていません。'
);
}
if (
requireGroup &&
!lineGroupId
) {
throw new Error(
'LINE_GROUP_ID が未登録です。先にLINEグループの登録を行ってください。'
);
}
return {
targetSender:
targetSender,
lineChannelAccessToken:
lineChannelAccessToken,
lineGroupId:
lineGroupId,
lineGroupName:
lineGroupName
};
}
/**
* Fromからメールアドレスのみ抽出
*
* Example Inc. <info@example.com>
* ↓
* info@example.com
*/
function extractEmailAddress_(
from
) {
const match =
from.match(
/<([^>]+)>/
);
if (match) {
return match[1]
.trim()
.toLowerCase();
}
return from
.trim()
.toLowerCase();
}
/**
* 古い処理済みメールIDを削除
*/
function cleanupProcessedIds_() {
const props =
PropertiesService
.getScriptProperties();
const all =
props.getProperties();
const cutoff =
Date.now() -
CONFIG
.PROCESSED_RETENTION_DAYS *
24 *
60 *
60 *
1000;
Object.keys(all)
.forEach(key => {
if (
!key.startsWith(
CONFIG.PROCESSED_PREFIX
)
) {
return;
}
const timestamp =
Number(all[key]);
if (
timestamp &&
timestamp < cutoff
) {
props.deleteProperty(
key
);
}
});
}
/**
* Webhookへのレスポンス
*/
function textResponse_(
text
) {
return ContentService
.createTextOutput(text)
.setMimeType(
ContentService.MimeType.TEXT
);
}
8.Apps ScriptをWebアプリとして公開する
LINEグループのIDを取得するため、最初だけLINEからWebhookを受信します。

Apps Script右上の、
デプロイ → 新しいデプロイ
を選択します。
種類は、
ウェブアプリ
です。
設定は、
次のユーザーとして実行
→ 自分
外部からアクセスできる設定については、
アクセスできるユーザー
→ 全員
を選択します。
LINEのサーバーからアクセスする必要があるため、Googleへのログインを要求しない公開状態が必要です。下記の画面が表示された場合は、プロジェクトに移動し権限を設定してください。

デプロイすると、
https://script.google.com/macros/s/xxxxxxxxxxxxxxxx/exec
というURLが発行されます。

この**/execで終わるURL**をコピーしておきます。
Apps ScriptのWebアプリは、HTTP POSTを受けるとdoPost(e)を実行し、POST本文はe.postData.contentsから取得できます。
9.LINEにWebhook URLを登録する
LINE Developers Consoleに戻ります。
Messaging APIの設定にある、
Webhook URL
へ先ほどのApps ScriptのURLを登録します。
https://script.google.com/macros/s/xxxxxxxxxxxxxxxx/exec

登録後、
検証(Verify)
を実行します。
問題なければ(ステータスは302になると思います)、
Webhookの利用
をONにします。
10.LINEグループを転送先として登録する
ここからが今回の仕組みのポイントです。
通常、LINEグループへPush Messageを送るには、そのグループのgroupIdが必要です。
LINEでは、公式アカウントが参加しているグループでメッセージが送信されるとWebhookが発生し、そのイベントのsource.groupIdからグループIDを取得できます。
今回のスクリプトでは、この処理を自動化しています。
Apps Scriptで、
enableGroupRegistration
を選択して実行します。
これにより、10分間だけグループ登録が可能になります。
例えばスクリプトプロパティに、
GROUP_REGISTRATION_CODE
M8q2Xk7R4p
と登録した場合、転送したいLINEグループで、
転送先登録 M8q2Xk7R4p
と送信します。
成功すると、LINE公式アカウントから、
✅ Gmail転送先として登録しました。
グループ: 家族
以後、対象メールの件名と本文をこのグループへ送信します。
と返信されます。

同時にApps Scriptのスクリプトプロパティへ、
LINE_GROUP_ID
Cxxxxxxxxxxxxxxxxxxxxxxxxxxxx
と、
LINE_GROUP_NAME
家族
が自動登録されます。
つまり、groupIdを自分で調べてコピーする必要はありません。
なお、スクリーンショットには「メッセージありがとうございます!・・・」と毎回表示されていますが、これは、Line DevelopersのMessaging API設定で「あいさつメッセージ」を「無効」に設定しておけば表示されなくなります。
11.LINEグループへの送信をテストする
Apps Scriptで、
testLineGroup
を実行します。

登録したLINEグループに、
✅ Gmail → LINEグループ 転送テスト
グループ: 家族
Google Apps Scriptからの送信に成功しました。
と表示されれば、LINE側の設定は完成です。
Messaging APIのPush Messageは、
POST /v2/bot/message/push
を使用し、toへグループIDを指定することで、公式アカウントが参加しているLINEグループへ送信できます。
12.過去メールを「転送済み」にする
次に非常に重要な初期化処理を行います。
Apps Scriptで、
initializeForwarding
を1回実行します。
この処理は、指定した送信元から過去7日間に届いているメールを、
「すでに処理済み」
として記録します。
これを実行しないと、設定直後に過去のメールまでLINEへ送られる可能性があります。
例えばすでに20件メールが届いていたとしても、
initializeForwarding
を実行しておけば、それらは転送されません。
以降に届いた新しいメールだけが対象になります。
13.1分ごとのGmail自動チェックを開始する
最後にApps Scriptで、
createMinuteTrigger
を実行します。
これで、
1分ごと
↓
Gmailを確認
↓
TARGET_SENDERからのメールか?
↓
まだLINEへ送っていないか?
↓
Subject取得
↓
本文取得
↓
LINEグループへPush
という処理が自動で繰り返されます。
Google Apps Scriptの時間主導型トリガーでは、everyMinutes()を使って1、5、10、15、30分間隔の実行を設定できます。
5分ごとで十分なら、
createFiveMinuteTrigger
を実行します。
14.実際にメールを送って確認する
例えば、
TARGET_SENDER
information@example.com
と設定している場合、そのアドレスからGmailへ、
件名:
LINE転送テスト
本文:
これはLINEグループへの
自動転送テストです。
というメールを送ります。
LINEグループに、
📩 Gmail
件名: LINE転送テスト
本文:
これはLINEグループへの
自動転送テストです。
と表示されれば完成です。
トリガーを待たずすぐ確認したい場合は、
checkGmailAndForwardToLine
をApps Scriptから直接実行しても構いません。
同じメールが何度も送られない仕組み
1分ごとにGmailを検索しているため、そのままでは同じメールを何度もLINEへ転送してしまいます。
そこで、このスクリプトではGmailのMessage IDを利用します。
LINEへの送信に成功すると、
PROCESSED_xxxxxxxxxxxxx
というプロパティを保存します。
次回のGmailチェックで同じMessage IDを発見すると、
すでに送信済み
↓
送信しない
となります。
また、メールスレッド単位ではなくメッセージ単位で管理しています。
そのため、
1通目
↓
返信
↓
さらに返信
という同一スレッド内のメールでも、新しく届いたメールだけをLINEへ送信できます。
処理済みIDは永遠に保存するのではなく、10日経過すると削除しています。
長いメールはどうなるのか
LINE Messaging APIのテキストメッセージには最大5,000文字という制限があります。
そのため今回のスクリプトでは余裕を持たせて、
MAX_LINE_CHARS: 4500
としています。
4500文字を超えた場合は、
…(本文が長いため、ここで省略しました)
と表示して途中で切ります。
なお、LINE Messaging APIでは文字数をUTF-16の符号単位でカウントします。絵文字などは見た目が1文字でも2文字として数えられる場合があります。今回のコードではJavaScript側もその仕様に合わせて切り詰め処理をしています。
LINEのWebhookは登録後にOFFでもよい
今回Webhookを利用する目的は、基本的に、
転送先となるLINEグループのgroupIdを取得すること
です。
一度グループIDがApps Scriptへ保存された後のメール転送は、
Gmail
↓
Google Apps Script
↓
保存済みLINE_GROUP_ID
↓
Messaging API Push Message
↓
LINEグループ
という流れになります。
そのため、LINEグループの登録が終わったら、LINE Developers ConsoleでWebhookをOFFにしても、GmailからLINEグループへのPush送信自体は可能です。
個人的には、グループを登録した後はWebhookをOFFにしておく運用が分かりやすいと思います。
転送するLINEグループを変更する
後から別のグループへ変更することもできます。
まずLINE Developers ConsoleでWebhookをONにします。
その後Apps Scriptで、
enableGroupRegistration
を実行します。
そして新しいLINEグループで、
転送先登録 M8q2Xk7R4p
のように登録コマンドを送ります。
新しいグループの、
LINE_GROUP_ID
と、
LINE_GROUP_NAME
で自動的に上書きされます。
自動転送を停止する
LINEへのGmail転送を一時的に停止したい場合は、
deleteForwardingTrigger
を実行します。
これでGmailを定期チェックするトリガーが削除されます。
再開するときは、
createMinuteTrigger
をもう一度実行するだけです。
セキュリティについて
今回の構成では、チャネルアクセストークンをソースコードへ直接書かず、Apps Scriptのスクリプトプロパティへ保存しています。
特に、
LINE_CHANNEL_ACCESS_TOKEN
は第三者へ公開しないよう注意が必要です。
アクセストークンが漏洩すると、そのMessaging APIチャネルを第三者に利用される危険性があります。LINEも、漏洩が疑われるアクセストークンについては取り消すよう案内しています。
また、Apps Scriptプロジェクト自体を他人と共有すると、プロジェクトの編集権限を持つ人から設定情報へアクセスされる可能性があるため、このスクリプトは基本的に自分だけで管理します。
Webhook署名検証について
ここは少し技術的ですが、重要な点です。
通常、LINEのWebhookを本格的なWebサービスで受信する場合は、LINEから送られる署名を検証し、本当にLINEから届いたWebhookなのか確認する設計にします。
一方、Google Apps Scriptの標準WebアプリでdoPost(e)に渡されるイベント情報には、POST本文などはありますが、任意のHTTPリクエストヘッダーを取得するための項目が公式ドキュメント上用意されていません。
そのため今回の簡易構成では、Webhookによるグループ登録について、
- ランダムな登録コードを要求する
- 登録可能なのは10分間だけ
- 登録完了後は受付を自動的に終了する
- 必要がなければWebhook自体をOFFにする
という対策を入れています。
GROUP_REGISTRATION_CODE
M8q2Xk7R4p
のように、推測しにくい文字列を設定してください。
企業システムなど、Webhookの署名検証を必須としたい用途では、Google Apps ScriptだけではなくCloud RunやCloud FunctionsなどをWebhookの受信口として利用する構成を検討した方がよいでしょう。
Gmailの標準転送機能を使わない理由
Gmailにはメールの自動転送機能がありますが、通常は「別のメールアドレス」への転送です。
今回行いたいのは、
メール
↓
Subjectと本文を取り出す
↓
LINE用メッセージに加工する
↓
LINEグループへ送信する
という処理なので、Google Apps ScriptでGmailを直接読み取る方法を採用しました。
この方法なら、将来的に、
差出人:
件名:
本文:
を追加したり、
「件名に【重要】が含まれるメールだけ送る」
といった条件追加も簡単です。
まとめ
今回は、Google Apps ScriptとLINE Messaging APIを使って、
特定の送信元からGmailへ届いたメールの件名と本文をLINEグループへ自動転送
する仕組みを作りました。
最終的な構成は次のとおりです。
指定した送信元
↓
Gmail
↓
Google Apps Script
↓
送信元をチェック
↓
件名+本文を取得
↓
重複送信をチェック
↓
LINE Messaging API
↓
指定したLINEグループ
一度設定してしまえば、普段は何も操作する必要はありません。
Gmailを1分間隔で確認し、新しい対象メールが届けば自動的にLINEグループへ流れます。
LINE Notifyが終了した現在、ちょっとしたメール通知をLINEへ連携したい場合には、Google Apps ScriptとMessaging APIの組み合わせは比較的手軽に構築できる方法だと思います。
また、今回の仕組みではLINEグループのgroupIdを自分で調べる必要もありません。
最初に、
転送先登録 <登録コード>
とグループで送信するだけで、自動的に転送先を登録できます。
Gmailでしか届かない重要な通知を家族やチームのLINEグループでも共有したい場合には、なかなか便利な仕組みではないでしょうか。