如何使用 Python - Gmail API 将大文件附加到电子邮件

How to attach large files to an email using Python - Gmail API

我正在尝试发送一封电子邮件,其中的附件(最好是多个附件)大于 10 MB 且小于总大小限制 25 MB。我提到 10 MB 的原因是因为它似乎是当附加文件的正常方式停止工作并且你得到 Error 10053.

时的下限

我在文档中读到,执行此操作的最佳方法是使用 resumable upload 方法,但我无法使其正常工作,也无法找到任何方法Python 中的好例子。大多数关于此的 SO 问题只是 link 返回到没有 Python 示例的文档,或者他们的代码导致了其他错误。

我正在 Python 中寻找解释,因为我想确保我理解正确。

我看过的问题:

代码:

import base64
import json
import os
from email import utils, encoders
from email.message import EmailMessage
from email.mime import application, multipart, text, base, image, audio
import mimetypes

from apiclient import errors
from googleapiclient import discovery, http
from google.oauth2 import service_account

def send_email(email_subject, email_body, email_sender='my_service_account@gmail.com', email_to='', email_cc='', email_bcc='', files=None):

    # Getting credentials
    with open(os.environ.get('SERVICE_KEY_PASSWORD')) as f:
        service_account_info = json.loads(f.read())

    # Define which scopes we're trying to access
    SCOPES = ['https://www.googleapis.com/auth/gmail.send']

    # Setting up credentials using the gmail api
    credentials = service_account.Credentials.from_service_account_info(service_account_info, scopes=SCOPES)

    # This allows us to assign an alias account to the message so that the messages aren't coming from 'ServiceDriod-8328balh blah blah'
    delegated_credentials = credentials.with_subject(email_sender)

    # 'Building' the service instance using the credentials we've passed
    service = discovery.build(serviceName='gmail', version='v1', credentials=delegated_credentials)

    # Building out the email 
    message = multipart.MIMEMultipart()
    message['to'] = email_to
    message['from'] = email_sender
    message['date'] = utils.formatdate(localtime=True)
    message['subject'] = email_subject
    message['cc'] = email_cc
    message['bcc'] = email_bcc
    message.attach(text.MIMEText(email_body, 'html'))


    for f in files or []:
        mimetype, encoding = mimetypes.guess_type(f)

        # If the extension is not recognized it will return: (None, None)
        # If it's an .mp3, it will return: (audio/mp3, None) (None is for the encoding)
        # For an unrecognized extension we set mimetype to 'application/octet-stream' so it won't return None again. 
        if mimetype is None or encoding is not None:
            mimetype = 'application/octet-stream'
        main_type, sub_type = mimetype.split('/', 1)

        # Creating the attachement:
        # This part is used to tell how the file should be read and stored (r, or rb, etc.)
        if main_type == 'text':
            print('text')
            with open(f, 'rb') as outfile:
                attachement = text.MIMEText(outfile.read(), _subtype=sub_type)
        elif main_type == 'image':
            print('image')
            with open(f, 'rb') as outfile:
                attachement = image.MIMEImage(outfile.read(), _subtype=sub_type)
        elif main_type == 'audio':
            print('audio')
            with open(f, 'rb') as outfile:
                attachement = audio.MIMEAudio(outfile.read(), _subtype=sub_type)          
        elif main_type == 'application' and sub_type == 'pdf':   
            with open(f, 'rb') as outfile:
                attachement = application.MIMEApplication(outfile.read(), _subtype=sub_type)
        else:                              
            attachement = base.MIMEBase(main_type, sub_type)
            with open(f, 'rb') as outfile:
                attachement.set_payload(outfile.read())

        encoders.encode_base64(attachement)
        attachement.add_header('Content-Disposition', 'attachment', filename=os.path.basename(f))
        message.attach(attachement)



    media_body = http.MediaFileUpload(files[0], chunksize=500, resumable=True)
    print('Uploading large file...')
    body = {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}


    message = (service.users().messages().send(userId='me', body=body, media_body=media_body).execute())

注意: 现在,我在 MediaFileUpload 中使用 files[0] 因为我只使用一个文件进行测试,我只想附加一个文件现在直到它起作用。

错误:

Exception has occurred: ResumableUploadError
<HttpError 400 "Bad Request">
  File "C:\Users\CON01599\AppData\Local\Continuum\anaconda3\Lib\site-packages\googleapiclient\http.py", line 927, in next_chunk
    raise ResumableUploadError(resp, content)
  File "C:\Users\CON01599\AppData\Local\Continuum\anaconda3\Lib\site-packages\googleapiclient\_helpers.py", line 130, in positional_wrapper
    return wrapped(*args, **kwargs)
  File "C:\Users\CON01599\AppData\Local\Continuum\anaconda3\Lib\site-packages\googleapiclient\http.py", line 822, in execute
    _, body = self.next_chunk(http=http, num_retries=num_retries)
  File "C:\Users\CON01599\AppData\Local\Continuum\anaconda3\Lib\site-packages\googleapiclient\_helpers.py", line 130, in positional_wrapper
    return wrapped(*args, **kwargs)
  File "C:\Users\CON01599\Documents\GitHub\pipelines\components\email\send_email.py", line 105, in send_email
    message = (service.users().messages().send(userId='me', body=body, media_body=media_body).execute())

答案:

import base64
import io
import json
import os
from email import utils, encoders
from email.message import EmailMessage
from email.mime import application, multipart, text, base, image, audio
import mimetypes

from apiclient import errors
from googleapiclient import discovery, http
from google.oauth2 import service_account


def get_environment_variables():
    """ Retrieves the environment variables and returns them in
        a dictionary object.
    """
    env_var_dict = {
        'to': os.environ.get('TO'),
        'subject': os.environ.get('SUBJECT'),
        'body': os.environ.get('BODY'),
        'file': os.environ.get('FILE')
    }

    return env_var_dict


def send_email(email_subject, email_body, email_sender='my_service_account@gmail.com', email_to='', email_cc='', email_bcc='', files=None):

    # Pulling in the string value of the service key from the parameter
    with open(os.environ.get('SERVICE_KEY_PASSWORD')) as f:
        service_account_info = json.loads(f.read())

    # Define which scopes we're trying to access
    SCOPES = ['https://www.googleapis.com/auth/gmail.send']

    # Setting up credentials using the gmail api
    credentials = service_account.Credentials.from_service_account_info(service_account_info, scopes=SCOPES)
    # This allows us to assign an alias account to the message so that the messages aren't coming from 'ServiceDriod-8328balh blah blah'
    delegated_credentials = credentials.with_subject(email_sender)
    # 'Building' the service instance using the credentials we've passed
    service = discovery.build(serviceName='gmail', version='v1', credentials=delegated_credentials)

    # Building out the email 
    message = multipart.MIMEMultipart()
    message['to'] = email_to
    message['from'] = email_sender
    message['date'] = utils.formatdate(localtime=True)
    message['subject'] = email_subject
    message['cc'] = email_cc
    message['bcc'] = email_bcc
    message.attach(text.MIMEText(email_body, 'html'))


    for f in files or []:
        f = f.strip(' ')
        mimetype, encoding = mimetypes.guess_type(f)

        # If the extension is not recognized it will return: (None, None)
        # If it's an .mp3, it will return: (audio/mp3, None) (None is for the encoding)
        # For an unrecognized extension we set mimetype to 'application/octet-stream' so it won't return None again. 
        if mimetype is None or encoding is not None:
            mimetype = 'application/octet-stream'
        main_type, sub_type = mimetype.split('/', 1)

        # Creating the attachement:
        # This part is used to tell how the file should be read and stored (r, or rb, etc.)
        if main_type == 'text':
            print('text')
            with open(f, 'rb') as outfile:
                attachement = text.MIMEText(outfile.read(), _subtype=sub_type)
        elif main_type == 'image':
            print('image')
            with open(f, 'rb') as outfile:
                attachement = image.MIMEImage(outfile.read(), _subtype=sub_type)
        elif main_type == 'audio':
            print('audio')
            with open(f, 'rb') as outfile:
                attachement = audio.MIMEAudio(outfile.read(), _subtype=sub_type)          
        elif main_type == 'application' and sub_type == 'pdf':   
            with open(f, 'rb') as outfile:
                attachement = application.MIMEApplication(outfile.read(), _subtype=sub_type)
        else:                              
            attachement = base.MIMEBase(main_type, sub_type)
            with open(f, 'rb') as outfile:
                attachement.set_payload(outfile.read())

        encoders.encode_base64(attachement)
        attachement.add_header('Content-Disposition', 'attachment', filename=os.path.basename(f))
        message.attach(attachement)

    media_body = http.MediaIoBaseUpload(io.BytesIO(message.as_bytes()), mimetype='message/rfc822', resumable=True)
    body_metadata = {} # no thread, no labels in this example

    try:
        print('Uploading file...')
        response = service.users().messages().send(userId='me', body=body_metadata, media_body=media_body).execute()
        print(response)
    except errors.HttpError as error:
        print('An error occurred when sending the email:\n{}'.format(error))


if __name__ == '__main__':

    env_var_dict = get_environment_variables()
    print("Sending email...")
    send_email(email_subject=env_var_dict['subject'], 
            email_body=env_var_dict['body'], 
            email_to=env_var_dict['to'],
            files=env_var_dict['file'].split(','))

    print("Email sent!")

您提到附件大于 10Mb,但没有提到小于 25Mb:gmail 有附件不能大于 25Mb 的限制,所以如果是这种情况,只需无法完成此操作,因为它超出了 gmail 的限制。

可以找到解释here

你能确认你的附件没有太大吗?

您遇到的问题是您的 MediaUpload 是单个附件。

您需要上传整个 RFC822 消息作为可续传文件MediaUpload,而不是上传单个附件MediaUpload

换句话说:

import ...
...
from io import BytesIO
from googleapiclient.http import MediaIoBaseUpload

SCOPES = [ 'scopes' ]

creds = get_credentials_somehow()
gmail = get_authed_service_somehow()

msg = create_rfc822_message(headers, email_body)
to_attach = get_attachment_paths_from_dir('../reports/tps/memos/2019/04')
add_attachments(msg, to_attach)

media = MediaIoBaseUpload(BytesIO(msg.as_bytes()), mimetype='message/rfc822', resumable=True)
body_metadata = {} # no thread, no labels in this example
resp = gmail.users().messages().send(userId='me', body=body_metadata, media_body=media).execute()
print(resp)
# { "id": "some new id", "threadId": "some new thread id", "labelIds": ["SENT"]}

我根据您提供的代码拼凑了这个,查看 this GitHub issue and Google's Inbox-to-Gmail email importer, specificially this bit

当发送对现有邮件的回复时,您几乎肯定会有某种元数据,您应该提供这些元数据以帮助 Gmail 跟踪您的新回复和原始对话。即,您将传递信息性元数据,而不是空的 body 参数,例如

body_metadata = { 'labelIds': [
                    "your label id here",
                    "another label id" ],
                  'threadId': "some thread id you took from the message you're replying to"
                }

其他好的参考文献: