如何使用 Gmail Api 和 Gdrive Api V3 将 xlsx 附件从 Gmail 上传到 Gdrive

问题描述 投票:0回答:1

我知道如何使用

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 Таблиц
  }
};

1。样品1

    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

2。样品2

此代码工作正常,但如果附件和电子邮件较多,则会花费很多时间。

/**
 * Обработка вложений из 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);
        }
      }
    });
  });
}
google-apps-script google-drive-api gmail-api
1个回答
0
投票

我为自己找到了以下解决方案:

  1. 我使用了函数的异步执行。
  2. 我使用了
    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)
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.