我知道如何使用
GmailApp
获取谷歌电子表格格式的附件,但这种方法非常慢,当文件很多时不适合。
我正在尝试使用
GmailApi V1
和 GoogleDriveApi V3
从邮件中获取附件文件,但我遇到了一些问题:
// Объект конфигурации для управления параметрами поиска файлов Excel
const config = {
folderId: '1cZeymn1OUhew0xc3uO_MeS4aDTH3gxXp', // ID папки на Google Drive для поиска файлов Excel
query: 'is:unread has:attachment', // запрос для поиска непрочитанных сообщений с вложениями
mimeTypes: {
excel: [
MimeType.MICROSOFT_EXCEL, // стандартный MIME-тип для Excel файлов
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' // MIME-тип для современных Excel файлов (.xlsx)
],
googleSheets: MimeType.GOOGLE_SHEETS // MIME-тип для Google Таблиц
}
};
const saveGmailAttachmentsToDrive = () => {
// Включите Gmail API и Drive API в разделе Google Cloud Platform API Dashboard для вашего проекта
const response = Gmail.Users.Messages.list('me', { q: 'is:unread has:attachment ' });
// Проверяем наличие сообщений
if (response && response.messages && response.messages.length > 0) {
response.messages.forEach((messageWrapper) => {
const messageId = messageWrapper.id;
const message = Gmail.Users.Messages.get('me', messageId, { format: 'full' });
// Проверяем наличие вложений
if (message.payload && message.payload.parts) {
message.payload.parts.forEach((part) => {
// Ищем части с 'body.attachmentId' (что указывает на вложение)
if (part.body && part.body.attachmentId) {
const attachment = Gmail.Users.Messages.Attachments.get('me', messageId, part.body.attachmentId)
const content = attachment.data.map(code => String.fromCharCode(code)).join('');
console.log(content);
// Создаем Blob из данных вложения
const blob = Utilities.newBlob(content, part.mimeType, part.filename);
const fileMetdata = {
name: part.filename,
parents: [config.folderId]
};
const response = Drive.Files.create(fileMetdata, blob);
console.log(response);
}
});
}
});
}
当前脚本使用
forEach
方法检索附件。从 JSON 响应中检索到的附件数据如下:
[80,75,3,4,20,0,8,8,8,0,115,80,-107,88,0,0,0,0,0,0,0,0,0,0,0,0,24,0,0,0,120,108,47,100,114,97,119,105,110,103,115,47,100,114,97,119,105,110,103,49,46,120,109,108,-99,-48,93,110,-62,48,12,7,-16,19,-20,14,85,-34,105,90,24,19,67,20,94,-48,78,48,14,-32,37,110,27,-111,-113,-54,14,-93,-36,126,-47,74,54,105,123,1,30,109,-53,63,-7,-17,-51,110,116,-74,-8,68,98,19,124,35,-22,-78,18,]
虽然当我使用
API Explorer
获取附件数据时,我得到 base64
格式的数据
然后我只设法将文件保存为 xlsx 格式,但是当我打开它时出现错误,我尝试将其转换为谷歌电子表格,我也收到错误
this file format cannot be converted to google spreadsheet
。
此代码工作正常,但如果附件和电子邮件较多,则会花费很多时间。
/**
* Обработка вложений из Gmail и конвертация Excel файлов в Google Sheets.
* @return {object[]} Массив объектов с ID сконвертированных таблиц.
*/
const convertToGoogleSpreadsheet = () => {
// Получаем список всех непрочитанных сообщений с вложениями в Gmail
const threads = GmailApp.search(config.query);
const messages = threads.flatMap(thread => GmailApp.getMessagesForThreads([thread]));
const convertedSheetIds = []; // Инициализируем массив для хранения ID конвертированных таблиц
// Перебираем каждое сообщение для обработки вложений
messages.flat().forEach(message => {
const attachments = message.getAttachments();
// Перебираем вложения в каждом сообщении
attachments.forEach(attachment => {
// Проверяем MIME-тип вложения, чтобы убедиться, что он соответствует формату Excel
if (config.mimeTypes.excel.includes(attachment.getContentType())) {
try {
// Создание объекта для дальнейшей вставки файла на Google Drive
const resource = {
name: attachment.getName(),
mimeType: config.mimeTypes.googleSheets,
parents: [config.folderId],
};
// Конвертируем вложение в Google Sheets с помощью API Google Drive
const blob = attachment.getAs(config.mimeTypes.excel[0]);
const file = Drive.Files.create(resource, blob, { convert: true });
// Логируем информацию о сохраненном файле
console.log(`Файл ${attachment.getName()} сохранен с ID: ${file.id}`);
// Добавляем ID сконвертированной таблицы в массив
convertedSheetIds.push({ id: file.id });
} catch (error) {
// Логирование ошибок при конвертации и сохранении файла
logError('convertToGoogleSpreadsheet', error);
}
}
});
});
}
我为自己找到了以下解决方案:
UrlFetchApp.fetchAll
方法和multipart
下载。这是我当前的代码,运行非常快速且可靠:
/**
* Извлекает адрес электронной почты отправителя из сообщения.
* @param {string} messageId - Идентификатор сообщения Gmail.
* @return {string|null} Адрес электронной почты отправителя или null, если он не найден.
*/
const getSenderEmail = (message) => {
// Извлечение заголовка 'From' (отправитель письма)
const headers = message.payload.headers;
const fromHeader = headers.find(header => header.name.toLowerCase() === 'from');
// Если заголовок 'From' найден, возвращаем его значение, иначе - null
return fromHeader ? fromHeader.value.replace(/["<>]/g, '') : null;
}
/**
* Выполняет операцию обработки всех тредов с непрочитанными сообщениями, содержащими вложения
* соответствующего MIME типа, и возвращает информацию для пакетной загрузки.
*
* @return {Object} Объект с информацией о вложениях для пакетной загрузки.
*/
const processUnreadMessagesWithAttachments = async (scriptProperties) => {
let attachmentsInfo = [];
try {
const threadsResponse = await Gmail.Users.Threads.list('me', { q: config.query });
const threads = threadsResponse.threads || [];
if (threads) {
const threadIds = threads.map(thread => thread.id);
// Сохраняем идентификаторы тредов в свойствах скрипта
scriptProperties.setProperties({ 'unreadThreadIds': JSON.stringify(threadIds) });
for (const thread of threads) {
const threadAttachments = await processThreadAttachments(thread.id);
attachmentsInfo = attachmentsInfo.concat(threadAttachments);
}
}
return attachmentsInfo;
} catch (error) {
logError('processUnreadMessagesWithAttachments', error);
}
};
/**
* Обрабатывает вложения в заданном треде электронной почты и возвращает массив объектов с идентификаторами вложений и метаданными.
*
* @param {string} threadId - ID треда, вложения которого необходимо обработать.
* @return {Array<Object>} Массив объектов для каждого вложения с его ID и метаданными.
*/
const processThreadAttachments = async (threadId) => {
try {
const threadData = Gmail.Users.Threads.get('me', threadId);
return threadData.messages.flatMap(message => {
return (message.payload.parts || []).flatMap(part => {
if (part.body.attachmentId && config.mimeTypes.excel.includes(part.mimeType)) {
return {
attachmentId: part.body.attachmentId,
filename: part.filename,
mimeType: part.mimeType,
messageId: message.id,
threadId: threadId,
sender: getSenderEmail(message) // получаем отправителей
}
}
return [];
}).filter(Boolean) // Убираем undefined значения
});
} catch (error) {
logError('processThreadAttachments', error);
}
};
/**
* Инициирует процесс пакетной загрузки файлов.
* Эта функция является входной точкой для начала загрузки файлов на Google Drive на основе вложений
* в непрочитанных сообщениях Gmail, соответствующих определенным MIME-типам.
*/
const startBatchUpload = async () => {
// Получаем объект для работы со свойствами скрипта, где будем хранить данные между выполнениями.
const scriptProperties = PropertiesService.getScriptProperties();
// Получаем OAuth 2.0 токен для текущего пользователя, что позволит делать авторизованные запросы к Google API.
const accessToken = ScriptApp.getOAuthToken();
// Получаем список вложений, которые нужно загрузить. Эта функция предполагается быть реализованной
// и должна обрабатывать все непрочитанные сообщения в Gmail, извлекая нужные вложения.
const attachmentsInfo = await processUnreadMessagesWithAttachments(scriptProperties);
if (attachmentsInfo.length == 0) {
console.log(`Нет новых закзаов`);
retutn;
}
// Сохраняем необходимую информацию для процесса загрузки в свойства скрипта.
// Это позволяет удерживать состояние между выполнениями функции, особенно полезно при использовании триггеров.
scriptProperties.setProperties({
'attachmentsInfo': JSON.stringify(attachmentsInfo), // информация о вложениях, представленная в виде строки JSON
'accessToken': accessToken, // сохраняемый токен доступа
'currentIndex': '0' // начальный индекс текущего вложения, с которого начнется загрузка
});
// Запускаем процесс пакетной загрузки файлов, вызывая функцию, которая обрабатывает загрузку.
triggerBatchUpload();
};
/**
* Триггер вызывает пакетную загрузку файлов на Google Drive.
* Запускает обработку следующего пакета файлов или завершает процесс, если все файлы обработаны.
*/
const triggerBatchUpload = async () => {
// Получаем доступ к сохраненным свойствам скрипта
const scriptProperties = PropertiesService.getScriptProperties();
const attachmentsInfo = JSON.parse(scriptProperties.getProperty('attachmentsInfo'));
const accessToken = scriptProperties.getProperty('accessToken');
let currentIndex = parseInt(scriptProperties.getProperty('currentIndex'), 10);
// Проверяем, не была ли обработка уже завершена
if (currentIndex >= attachmentsInfo.length) {
deleteAllPropertiesExceptUnreadThreadIds(scriptProperties); // Удаляем все сохраненные свойства
deleteAllTriggers(); // Удаляем все триггеры, так как загрузка завершена
addTrigger('doOrders', 1 * 20 * 1000) // запускаем процесс оработки заказов
console.log('Все файлы были обработаны.');
return;
}
// формируем массив отправителей
const senderData = attachmentsInfo.slice(currentIndex, currentIndex + 3).map(info => ({ sender: info.sender }));
// Формируем массив запросов на получение данных вложений из Gmail
const batchInfo = attachmentsInfo.slice(currentIndex, currentIndex + 3);
const requests = batchInfo.map(info => ({
url: `https://www.googleapis.com/gmail/v1/users/me/messages/${info.messageId}/attachments/${info.attachmentId}`,
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json'
},
filename: info.filename,
mimeType: info.mimeType,
muteHttpExceptions: true
}));
// Подготовим массивы для хранения blob данных и метаданных файлов
let blobs = [];
let attachmentsData = [];
// Выполнение запросов и обработка полученных данных вложений
if (requests.length > 0) {
const responses = await UrlFetchApp.fetchAll(requests);
responses.forEach((response, index) => {
if (response.getResponseCode() === 200) {
// Если ответ успешный, обрабатываем данные вложения
const request = requests[index];
const attachmentData = JSON.parse(response.getContentText());
const blob = Utilities.base64DecodeWebSafe(attachmentData.data);
blobs.push(blob);
attachmentsData.push({
name: request.filename,
mimeType: 'application/vnd.google-apps.spreadsheet',
parents: [config.folderId]
});
} else {
console.error(`Ошибка получения данных вложения: ${response.getResponseCode()}`);
}
});
}
// Запустим асинхронную функцию загрузки файлов на Google Drive
batchUploadFiles({ attachmentsData, blobs, senderData });
// Обновляем индекс текущего выполнения и сохраняем его в свойствах скрипта
currentIndex += batchInfo.length;
scriptProperties.setProperty('currentIndex', currentIndex.toString());
// Если не все файлы обработаны, установим новый триггер для продолжения загрузки
if (currentIndex < attachmentsInfo.length) {
addTrigger('triggerBatchUpload', 1 * 60 * 1000); // Запустить следующий шаг через 30 секунд
} else {
deleteAllPropertiesExceptUnreadThreadIds(scriptProperties); // Процесс загрузки завершён, удаляем свойства
deleteAllTriggers(); // Удаляем все триггеры
addTrigger('doOrders', 1 * 20 * 1000) // запускаем процесс оработки заказов
console.log('Все файлы были обработаны.');
}
};
/**
* Функция, которая будет вызвана триггером и отметит ранее сохраненные треды, как прочитанные.
*/
const markThreadsAsRead = () => {
try {
const scriptProperties = PropertiesService.getScriptProperties();
const threadIdsString = scriptProperties.getProperty('unreadThreadIds');
if (!threadIdsString) {
return;
}
const threadIds = JSON.parse(threadIdsString);
threadIds.forEach(threadId => {
// Отмечаем каждое сообщение из треда, как прочитанное
Gmail.Users.Threads.modify({ removeLabelIds: ['UNREAD'] }, 'me', threadId);
});
// Удаляем идентификаторы тредов из свойств скрипта после их обработки
scriptProperties.deleteProperty('unreadThreadIds');
console.log('Unread threads are marked as read.');
} catch (error) {
logError('markThreadsAsRead', error);
}
};
/**
* Удаляет все свойства скрипта, кроме 'unreadThreadIds'.
*/
const deleteAllPropertiesExceptUnreadThreadIds = (scriptProperties) => {
const properties = scriptProperties.getProperties();
// Получаем массив ключей свойств и удаляем каждое свойство, не являющееся 'unreadThreadIds'
Object.keys(properties).forEach(key => {
if (key !== 'unreadThreadIds') {
scriptProperties.deleteProperty(key);
}
});
// Логирование успешного выполнения операции
console.log('Все свойства, кроме "unreadThreadIds", были успешно удалены.');
};
/**
* Выполняет пакетную загрузку файлов на Google Drive.
*/
async function batchUploadFiles(data) {
try {
// Получение доступного OAuth 2.0 токена для текущей сессии пользователя
const accessToken = ScriptApp.getOAuthToken();
// Получаем информацию о вложениях и созданные blob'ы
const { attachmentsData, blobs, senderData } = data;
const boundary = "xxxxxxxxxx"; // Уникальная граница нужна для разделения частей в multipart запросе
// Создаем массив запросов для каждого вложения
const requests = attachmentsData.map((metadata, index) => {
// Использование шаблонных литералов для формирования multipart тела запроса
let data = `--${boundary}\r\n`;
data += `Content-Disposition: form-data; name="metadata"\r\n`;
data += `Content-Type: application/json; charset=UTF-8\r\n\r\n`;
data += `${JSON.stringify(metadata)}\r\n`; // Добавление метаданных файла
data += `--${boundary}\r\n`;
data += `Content-Disposition: form-data; name="file"; filename="${metadata.name}"\r\n`;
data += `Content-Type: ${config.mimeTypes.excel[0]}\r\n\r\n`;
// TODO: Этот блок кода предполагает наличие массива байт в blobs, который нужно будет преобразовать в blob
let payload = [...Utilities.newBlob(data).getBytes(), ...blobs[index]];
payload = [...payload, ...Utilities.newBlob(`\r\n--${boundary}--`).getBytes()]; // Закрытие multipart тела
// Формируем объект запроса для fetchAll
return {
url: 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': `multipart/related; boundary=${boundary}`
},
payload: payload,
muteHttpExceptions: true
};
});
// Выполняем пакетные запросы для загрузки файлов
const responses = await UrlFetchApp.fetchAll(requests);
// Обработка ответов и логирование результатов
const log = responses.map((response, index) => {
if (response.getResponseCode() === 200) {
return {
'Файл': JSON.parse(response.getContentText()).name,
'Отправитель': senderData[index].sender,
'Статус': 'успешно загружен'
}
} else {
return {
'Ошибка загрузки файла': response.getContentText(),
'Отправитель': senderData[index].sender,
'Статус': 'неудачно'
}
}
});
console.log(JSON.stringify(log, null, 2));
// отправляем сообщения в телеграм
sendMessage(JSON.stringify(log, null, 2));
} catch (error) {
// Логирование и передача информации об ошибке
logError('batchUploadFiles', error)
}
}