メールの一括管理システム
Gmail, iCloud, outlookを普段から使っていて見ないといけないのですが、iPhoneで見るならこれだけで3アプリ入れないとダメだしPCがないと見れないっていうのもなかなか不便ってことで一括でどのメールが来てもdiscordに通知して見れるようにするシステムを作って見ました
注意
- GASの定期実行でメールを確認するのでリアルタイムにメールを通知できるわけではありません
- 返信するなどの実装も可能ではありますが、この記事では解説していません(AIによる返信メールの自動生成なども同様です)
システム概要
本システムは以下の流れで動作します。
-
メール転送とラベル付与
iCloud/Gmail/outlookメールを共通のGmailに転送し、転送されたメールには自動的に特定のラベルが付与されます。 -
メール情報の取得と管理
Google Apps Scriptを利用して、各ラベルに該当するメールをスプレッドシートに記録します。 -
Discordへの自動通知
新規のメールが記録された際に、DiscordのWebhookを介してメール情報を通知します。
個人的にDiscordをよく使っているのでdiscordにしていますが、slackなどでも同様のことができると思います
前準備
共通のGoogleアカウントの作成
各種メールを一括で扱うために、専用のGoogleアカウントを作成してください。
https://www.google.com/intl/ja/account/about/
***.mailcatcher@gmail.com
こんな感じで作りました
なんでもいいです
スプレッドシートの準備
先ほど作ったgoogleアカウントでログインして、スプレッドシートを作成しておいてください
-
「mail list」シート:
Gmailから取得したメール情報(ラベル、送信元、件名、本文、受信日時など)を記録するシート。
A | B | C | D | E | F | G | H | I | J | K | |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | - | - | - | - | - | - | - | - | - | - | |
2 | label | webhook | thread id | message id | 件名 | 送信元 | 受信日時 | 本文 | message cnt |
こんな感じです
分かりづらw 分かりやすい書き方あれば教えてください
-
「label info」シート:
各ラベルに対応する情報を管理するシート。
必要なカラムは以下の通りです。- B列: Gmailラベル名(後でやります)
- C列: 転送元メールアドレス
- D列: Discord Webhook URL
discord webhookの取得方法はサボらしてもらいます
参考: https://zenn.dev/lambta/articles/5edbda4ccb1ec6
それぞれのチャンネルを作ってwebhookを発行しましょう
メールの転送・ラベリング
次はメールの転送をします
これは、各種(iCloud, Gmail, outlookなど)メールサービスのメールを先ほど作った***.mailcatcher@gmail.com
ここに転送することでメールを一括管理しようというものです
転送のやり方
ラベリング
ラベルの作成
ラベルの自動付与
自動付与の方はこの記事ではFromに登録することになっていますが、転送しているので、toの方に登録する必要があります
GAS
前準備のところで作成したスプレッドシートにGASを書いていきます
「拡張機能」→「Apps Script」
const ss = SpreadsheetApp.openById('**スプシのID**');
const mailListSheet = ss.getSheetByName('mail list');
const labelInfoSheet = ss.getSheetByName('label info');
const maxThreadCount = 100; // 1回で取得するスレッド数
function main() {
const newData = appendMailLog(labelInfos = getLabelInfos());
const unsentData = getUnsentMessages();
unsentData.forEach(data => {
const info = { label: data.label, email: data.email, webhook: data.webhook };
const message = {
subject: data.subject,
from: data.from,
date: data.date,
body: data.body,
sheetRow: data.sheetRow
};
if (sendDiscordNotificationWithRetry(info, message)) {
mailListSheet.getRange(message.sheetRow, 12).setValue("send success");
}
});
}
/**
* label infoシートのB~D列の情報をオブジェクト形式で返す関数
*/
function getLabelInfos() {
const lastRow = labelInfoSheet.getLastRow();
if (lastRow < 2) return [];
const dataRange = labelInfoSheet.getRange(2, 2, lastRow - 1, 3);
const values = dataRange.getValues();
return values.map(row => ({
label: row[0],
email: row[1],
webhook: row[2]
}));
}
/**
* 「mail list」シートに記録済みのメッセージIDを取得する関数
* ※ メッセージIDはシートのF列に記録されている前提
*/
function getExistingMessageIds() {
const lastRow = mailListSheet.getLastRow();
if (lastRow < 3) return [];
const idRange = mailListSheet.getRange(3, 6, lastRow - 2, 1);
const idValues = idRange.getValues();
return idValues.flat();
}
/**
* 各ラベルのメールスレッドから重複しないメール情報をオブジェクト形式で収集し、
* 「mail list」シートに追記する関数
* なお、各オブジェクトにはシート上の行番号 (sheetRow) も付与します。
*/
function appendMailLog(labelInfos) {
const result = [];
const existingMessageIds = getExistingMessageIds();
labelInfos.forEach(info => {
const query = 'label:' + info.label;
const threads = GmailApp.search(query, 0, maxThreadCount);
threads.forEach(thread => {
const threadId = thread.getId();
const messages = thread.getMessages();
const messageCount = messages.length;
messages.forEach(message => {
const messageId = message.getId();
if (existingMessageIds.indexOf(messageId) !== -1) return;
const maxBodyLength = 1000;
let bodyContent = message.getPlainBody();
if (bodyContent.length > maxBodyLength) {
bodyContent = bodyContent.substring(0, maxBodyLength) + '...';
}
const data = {
label: info.label,
email: info.email,
webhook: info.webhook,
threadId: threadId,
messageId: messageId,
subject: message.getSubject(),
from: message.getFrom(),
date: message.getDate(),
body: bodyContent,
messageCount: messageCount
};
result.push(data);
});
});
});
if (result.length > 0) {
const startRow = mailListSheet.getLastRow() + 1;
const rows = result.map((data, idx) => {
data.sheetRow = startRow + idx;
return [
data.label,
data.email,
data.webhook,
data.threadId,
data.messageId,
data.subject,
data.from,
data.date,
data.body,
data.messageCount,
"false"
];
});
mailListSheet.getRange(startRow, 2, rows.length, rows[0].length).setValues(rows);
}
return result;
}
/**
* 「mail list」シートから、L列(12列目)が空欄の未送信メール情報を取得する関数
* 返すオブジェクトには、シート上の行番号 (sheetRow) も含む
*/
function getUnsentMessages() {
const lastRow = mailListSheet.getLastRow();
if (lastRow < 3) return [];
const dataRange = mailListSheet.getRange(3, 2, lastRow - 2, 10);
const values = dataRange.getValues();
const unsent = [];
values.forEach((row, index) => {
const sheetRow = index + 3;
const flag = mailListSheet.getRange(sheetRow, 12).getValue();
if (flag !== "send success") {
unsent.push({
label: row[0],
email: row[1],
webhook: row[2],
threadId: row[3],
messageId: row[4],
subject: row[5],
from: row[6],
date: row[7],
body: row[8],
messageCount: row[9],
sheetRow: sheetRow
});
}
});
return unsent;
}
/**
* DiscordのWebhook URLに対して、メール情報を通知する関数(再試行付き)
*
* @param {Object} info - ラベル情報オブジェクト(例: { label, email, webhook })
* @param {Object} message - メール情報オブジェクト(例: { subject, from, date, body, sheetRow })
* @return {boolean} 送信成功ならtrue、失敗ならfalse
*/
function sendDiscordNotificationWithRetry(info, message) {
const formattedDate = Utilities.formatDate(message.date, Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss");
let content = "===" + message.subject + "========================================\n";
content += "転送元メール: " + info.email + "\n";
content += "送信元: " + message.from + "\n";
content += "受信日時: " + formattedDate + "\n";
content += "```\n";
content += message.body + "\n";
content += "```\n";
const maxDiscordLength = 1000;
if (content.length > maxDiscordLength) {
content = content.substring(0, maxDiscordLength - 3) + '...';
}
const payload = { content: content };
const options = {
method: "post",
contentType: "application/json",
payload: JSON.stringify(payload)
};
const maxAttempts = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
UrlFetchApp.fetch(info.webhook, options);
return true;
} catch (e) {
// 429エラー(レートリミット)なら待機して再試行
if (e.message.indexOf("429") !== -1) {
const retryAfter = 1000;
Utilities.sleep(retryAfter);
} else {
console.error("sendDiscordNotification error:", e);
return false;
}
}
}
return false;
}
終わり