Gmail添付ファイルの重複保存を実行前に発見|GASでDrive保存計画をdry-run監査
ツールおじ|GoogleアカウントをAIエージェント化
Gmailの添付ファイルをGoogleドライブへ整理するとき、同じ名前のファイルを何度も保存したり、対象メールを広く取りすぎたりすると、あとから整理し直す手間が増えます。この記事では、ファイルを実際には保存せず、対象件数・容量・保存先直下の同名候補だけを先に確認する無料GASを紹介します。
コードは常にdry-runで動きます。メールの件名、送信者、本文、添付ファイル名、メッセージIDはログへ出さず、件数と匿名の位置番号だけで保存計画を確認します。
先に知っておきたいこと
- できること: Gmail検索に一致したスレッドから添付ファイルを数え、保存先フォルダ直下の同名候補、名前なし、上限超過を確認できます。
- できないこと: 添付ファイルの保存、上書き、削除、メールの既読化、ラベル変更、転送、送信は行いません。
- 安全設計: DriveのcreateFile、GmailのsendEmail、moveToTrashなどを使いません。
- ログ範囲: スレッド・メッセージ・添付の位置番号、分類、合計件数、合計容量だけを記録します。
- 制約: 保存先フォルダ直下だけを調べます。サブフォルダやファイル内容の一致までは確認しません。
この例は学習・検証用です。実データで試す前に、専用のテストフォルダと少数のサンプルメールを用意し、権限画面と検索条件を確認してください。
準備するもの
- 添付ファイル整理用のGoogleドライブフォルダを1つ作ります。
- フォルダURLのfolders/より後ろにある文字列をfolderIdとして控えます。
- Gmail検索欄でhas:attachment newer_than:7dを試し、対象が広すぎないことを確認します。
- 初回はテスト用ラベルを付けたメールに限定し、label:任意のラベル名 has:attachmentのように絞ることを推奨します。
folderIdやGmail検索条件は、記事や公開コードへ貼り付けないでください。自分のApps Script内だけで設定します。
コピーして使える最小コード
function auditGmailAttachmentSavePlanFree() {
const config = {
gmailQuery: 'has:attachment newer_than:7d',
folderId: 'ここを保存先フォルダIDに置き換える',
maxThreads: 50,
maxAttachments: 200,
dryRun: true,
writeSummary: false,
summarySheetName: 'AuditSummary'
};
validateAttachmentAuditConfig_(config);
const destination = DriveApp.getFolderById(config.folderId);
const existingNames = collectDirectChildNames_(destination, config.maxAttachments * 5);
const threads = GmailApp.search(config.gmailQuery, 0, config.maxThreads + 1);
if (threads.length > config.maxThreads) {
throw new Error(
'検索結果が上限の' + config.maxThreads + 'スレッドを超えています。' +
'Gmail検索条件を狭めてください。'
);
}
const issues = [];
let attachmentCount = 0;
let totalBytes = 0;
let duplicateNameCount = 0;
let emptyNameCount = 0;
threads.forEach(function(thread, threadIndex) {
const messages = thread.getMessages();
messages.forEach(function(message, messageIndex) {
const attachments = message.getAttachments({
includeInlineImages: false,
includeAttachments: true
});
attachments.forEach(function(attachment, attachmentIndex) {
attachmentCount += 1;
if (attachmentCount > config.maxAttachments) {
throw new Error(
'添付ファイルが上限の' + config.maxAttachments + '件を超えています。' +
'Gmail検索条件を狭めてください。'
);
}
const name = String(attachment.getName() || '').trim();
const bytes = attachment.getBytes().length;
totalBytes += bytes;
if (!name) {
emptyNameCount += 1;
issues.push({
thread: threadIndex + 1,
message: messageIndex + 1,
attachment: attachmentIndex + 1,
code: 'ATTACHMENT_NAME_EMPTY'
});
return;
}
const normalizedName = normalizeAttachmentName_(name);
if (existingNames[normalizedName]) {
duplicateNameCount += 1;
issues.push({
thread: threadIndex + 1,
message: messageIndex + 1,
attachment: attachmentIndex + 1,
code: 'SAME_NAME_EXISTS_IN_FOLDER'
});
}
});
});
});
const summary = {
dryRun: config.dryRun,
checkedThreads: threads.length,
attachmentCount: attachmentCount,
totalBytes: totalBytes,
duplicateNameCount: duplicateNameCount,
emptyNameCount: emptyNameCount,
issueCount: issues.length
};
Logger.log('dry-run: ' + summary.dryRun);
Logger.log('確認スレッド数: ' + summary.checkedThreads);
Logger.log('添付ファイル数: ' + summary.attachmentCount);
Logger.log('合計容量: ' + formatBytesForAttachmentAudit_(summary.totalBytes));
Logger.log('保存先直下の同名候補: ' + summary.duplicateNameCount);
Logger.log('名前なし添付: ' + summary.emptyNameCount);
issues.forEach(function(issue) {
Logger.log(
'スレッド位置' + issue.thread +
' / メッセージ位置' + issue.message +
' / 添付位置' + issue.attachment +
': ' + issue.code
);
});
Logger.log('件名・送信者・本文・添付ファイル名・メッセージIDはログへ出していません。');
if (config.writeSummary === true) {
writeAttachmentAuditSummary_(config, summary);
}
return summary;
}
function validateAttachmentAuditConfig_(config) {
if (config.dryRun !== true) {
throw new Error('安全のためdryRunはtrue固定です。');
}
if (!config.gmailQuery || !String(config.gmailQuery).trim()) {
throw new Error('gmailQueryを入力してください。');
}
if (!config.folderId || config.folderId.indexOf('ここを') !== -1) {
throw new Error('folderIdを保存先フォルダIDへ置き換えてください。');
}
if (!Number.isInteger(config.maxThreads) || config.maxThreads < 1 || config.maxThreads > 100) {
throw new Error('maxThreadsは1〜100の整数にしてください。');
}
if (!Number.isInteger(config.maxAttachments) || config.maxAttachments < 1 || config.maxAttachments > 500) {
throw new Error('maxAttachmentsは1〜500の整数にしてください。');
}
}
function collectDirectChildNames_(folder, maxFiles) {
const names = {};
const files = folder.getFiles();
let checked = 0;
while (files.hasNext()) {
checked += 1;
if (checked > maxFiles) {
throw new Error(
'保存先直下のファイル数が確認上限の' + maxFiles + '件を超えています。' +
'テスト用フォルダか、より小さい保存先で確認してください。'
);
}
const name = String(files.next().getName() || '').trim();
if (name) {
names[normalizeAttachmentName_(name)] = true;
}
}
return names;
}
function normalizeAttachmentName_(name) {
return String(name)
.normalize('NFKC')
.trim()
.replace(/\s+/g, ' ')
.toLowerCase();
}
function formatBytesForAttachmentAudit_(bytes) {
if (bytes < 1024) {
return bytes + ' B';
}
if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(1) + ' KB';
}
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
}
function writeAttachmentAuditSummary_(config, summary) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let sheet = spreadsheet.getSheetByName(config.summarySheetName);
if (!sheet) {
sheet = spreadsheet.insertSheet(config.summarySheetName);
}
sheet.clearContents();
sheet.getRange(1, 1, 7, 2).setValues([
['確認日時', new Date()],
['dry-run', summary.dryRun],
['確認スレッド数', summary.checkedThreads],
['添付ファイル数', summary.attachmentCount],
['合計容量bytes', summary.totalBytes],
['保存先直下の同名候補', summary.duplicateNameCount],
['名前なし添付', summary.emptyNameCount]
]);
}実行手順
- 空のスプレッドシートをテスト用に作り、拡張機能からApps Scriptを開きます。
- コードを貼り付け、folderIdをテスト用フォルダのIDへ置き換えます。
- Gmail検索欄でgmailQueryと同じ条件を試し、対象件数と内容を手で確認します。
- maxThreadsを最初は5程度へ下げ、auditGmailAttachmentSavePlanFreeを実行します。
- 初回の権限画面で、Gmail、Drive、スプレッドシートへのアクセス範囲を読みます。
- 実行ログで、添付件数、合計容量、同名候補、匿名の位置番号を確認します。
- 結果が想定どおりでも、この無料コードは保存処理へ切り替えず、保存ルールを先に決めます。
このコードはGmailとDriveを読み取りますが、メールやファイルを変更しません。writeSummaryも初期値はfalseです。件数だけを別シートへ残したい場合に限り、テスト環境でtrueへ変更してください。
エラー分類の読み方
- ATTACHMENT_NAME_EMPTY: 添付ファイル名を取得できませんでした。自動保存の対象にせず、手動確認します。
- SAME_NAME_EXISTS_IN_FOLDER: 保存先フォルダ直下に正規化後の同名ファイルがあります。内容が同じとは限りません。
同名候補が出たときは、上書きや削除を自動化せず、受信日を付ける、送信元ごとにフォルダを分ける、同じ内容なら保存しないなどの運用ルールを決めてください。
失敗しやすい点と制限
Gmail検索が広すぎる
has:attachmentだけでは対象が多くなりやすいため、newer_than、label、fromなどを組み合わせ、Gmail検索画面で先に結果を確認してください。コード内の上限を増やして無理に全件処理するより、期間を分けるほうが安全です。
同名は内容一致ではない
この無料例は、保存先直下のファイル名だけを比較します。同じ名前でも内容が異なる場合があり、逆に名前が違っても同じ内容の場合があります。内容ハッシュ、版管理、サブフォルダ横断は対象外です。
サブフォルダは確認しない
collectDirectChildNames_は指定フォルダ直下だけを見ます。月別や送信元別のサブフォルダを使う場合は、保存ルールに合わせて確認範囲を設計してください。
添付ファイルをメモリへ読み込む
容量集計のためgetBytesを使います。大きな添付や大量メールでは実行時間・メモリ上限に達することがあります。maxThreadsとmaxAttachmentsを小さく保ち、少量から試してください。
権限を確認する
Apps Scriptの権限画面では、対象アカウントとアクセス範囲を確認してください。共有アカウントや業務メールへ適用する場合は、組織のルールと管理者方針も確認が必要です。
自動保存はしない
このコードにはcreateFileがありません。同名時の処理、保存期間、共有設定、個人情報を含む添付の扱いを決めずに、自動保存へ進まないでください。
公開前チェックリスト
- 専用のテストフォルダを使った
- Gmail検索欄で対象を先に確認した
- maxThreadsとmaxAttachmentsを小さく設定した
- dryRunがtrueになっている
- writeSummaryがfalseになっている
- 件名、送信者、本文、添付ファイル名、メッセージIDをログへ出していない
- 保存、上書き、削除、送信、転送、ラベル変更の処理がない
- 同名候補を内容一致と決めつけていない
- 権限画面と組織の運用ルールを確認した
まとめ
Gmail添付ファイルの整理では、保存処理を先に作るより、対象メールの範囲、添付件数、容量、同名時の扱いをdry-runで確認することが大切です。この無料コードなら、メールやDriveを変更せずに、保存前の確認材料を集められます。
Drive内のファイルを一覧化し、整理対象や更新状況を確認したい場合は、次の有料Tipsも確認できます。
DriveFileList Pro|Google Driveのファイル一覧化GASテンプレを見る

GasおじのTips商品一覧を見る

※本記事は学習・検証用の例です。実データへ適用する前に、少量のテスト環境で確認してください。Google公式・Google公認の商品ではありません。
