html 的电子邮件附件在某些设备上不可见

Email attachments with html not visible on some devices

我想发送一封 html 电子邮件,其中包含内容 ID 附件(图片直接在邮件中)和内容处理附件,其中包含一个 xls 文件。

根据我使用的客户端邮件,xls 文件不可见(例如 iPhone、iPad 或三星)。

我想我没有在邮件的正确部分包含 xls 文件,但是当我尝试在 MIMEMultipart('alternative') 之前包含它时,它不起作用。

有人知道以下代码中问题的根源吗?

import base64
import httplib2

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import mimetypes
from email.mime.image import MIMEImage
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.application import MIMEApplication

from apiclient.discovery import build
from oauth2client.client import flow_from_clientsecrets
from oauth2client.file import Storage
from oauth2client.tools import run

html = """\
<html>
  <head></head>
  <body>
    <p>Hi!<br>
       XXX <br><br>
        Hereunder a short overview of market evolution :<br>
        <img src="cid:image1" height="600" width="600">
    </p>
  </body>
</html>
"""

to='test@gmail.com';
subject='XXX';
message_main = html;
attachment_paths = [('test.png'), '<image1>'),
                    ('2910e0f.xls', False)]


# Check https://developers.google.com/gmail/api/auth/scopes for all available scopes
OAUTH_SCOPE = 'https://www.googleapis.com/auth/gmail.compose'

# Location of the gmail.storage
STORAGE = Storage('gmail.storage')

# Start the OAuth flow to retrieve credentials
flow = flow_from_clientsecrets(CLIENT_SECRET_FILE, scope=OAUTH_SCOPE)
http = httplib2.Http()

# Try to retrieve credentials from storage or run the flow to generate them
credentials = STORAGE.get()
if credentials is None or credentials.invalid:
  credentials = run(flow, STORAGE, http=http)

# Authorize the httplib2.Http object with our credentials
http = credentials.authorize(http)

## Save credentials to storage
# STORAGE.put(credentials)
# Build the Gmail service from discovery
gmail_service = build('gmail', 'v1', http=http)

message = MIMEMultipart('related') # 'alternative', 'multipart', 'mixed', related
message['to'] = to
message['from'] = "YYY@gmail.com"
message['subject'] = subject
message.preamble = 'This is a multi-part message in MIME format.'

# Encapsulate the plain and HTML versions of the message body in an
# 'alternative' part, so message agents can decide which they want to     display.
msgAlternative = MIMEMultipart('alternative')
message.attach(msgAlternative)

msg1 = MIMEText(message_main, 'plain')
msg2 = MIMEText(message_main, 'html')
msgAlternative.attach(msg1)
msgAlternative.attach(msg2)

for filename, link in attachment_paths:
    print '----'
    print filename
    print link
    content_type, encoding = mimetypes.guess_type(filename)

    if content_type is None or encoding is not None:
        content_type = 'application/octet-stream'
    main_type, sub_type = content_type.split('/', 1)

    if main_type == 'text':
        fp = open(filename, 'rb')
        msg = MIMEText(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'image':
        fp = open(filename, 'rb')
        msg = MIMEImage(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'audio':
        fp = open(filename, 'rb')
        msg = MIMEAudio(fp.read(), _subtype=sub_type)
        fp.close()
    else:
        fp = open(filename, 'rb')
        msg = MIMEBase(main_type, sub_type)
        msg.set_payload(fp.read())
        fp.close()

    if not link:
        # msg.add_header('Content-Disposition', 'attachment', filename=filename)
        # message.attach(msg)
        # pdf = open(filename, 'rb').read()
        # msgPdf = MIMEApplication(pdf, 'pdf') # pdf for exemple
        # msgPdf.add_header('Content-ID', '<pdf1>') # if no cid, client like MAil.app (only one?) don't show the attachment
        msg.add_header('Content-Disposition', 'attachment', filename=filename)
        # msg.add_header('Content-Disposition', 'inline', filename=filename)
    else:
        # Define the image's ID as referenced above
        msg.add_header('Content-ID', link)
        msg.add_header('Content-Disposition', 'attachment', filename=filename)

    message.attach(msg)

body = {'raw': base64.urlsafe_b64encode(message.as_string())}

# body = {'raw': base64.b64encode(message.as_string())}

# send it
try:
  message = (gmail_service.users().messages().send(userId="me", body=body).execute())
  print('Message Id: %s' % message['id'])
  print(message)
except Exception as error:
  print('An error occurred: %s' % error)

解决方案包括将初始多部分容器更改为混合容器并分两步附加文件:

  • 在简单附件的 MIMEMultipart('alternative') 声明之前
  • MIMEMultipart('alternative') 附件声明之后,与 html 正文相关(主要针对消息内的图片)

您可以在下面找到正确的代码:

import base64
import httplib2

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import mimetypes
from email.mime.image import MIMEImage
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.application import MIMEApplication

from apiclient.discovery import build
from oauth2client.client import flow_from_clientsecrets
from oauth2client.file import Storage
from oauth2client.tools import run

html = """\
<html>
  <head></head>
  <body>
    <p>Hi!<br>
       XXX <br><br>
        Hereunder a short overview of market evolution :<br>
        <img src="cid:image1" height="600" width="600">
    </p>
  </body>
</html>
"""

to='test@gmail.com';
subject='XXX';
message_main = html;
attachment_paths_html = [('test.png'), '<image1>')]
attachment_paths_mixed = [('2910e0f.xls', '2910e0f.xls')]

# Check https://developers.google.com/gmail/api/auth/scopes for all available scopes
OAUTH_SCOPE = 'https://www.googleapis.com/auth/gmail.compose'

# Location of the gmail.storage
STORAGE = Storage('gmail.storage')

# Start the OAuth flow to retrieve credentials
flow = flow_from_clientsecrets(CLIENT_SECRET_FILE, scope=OAUTH_SCOPE)
http = httplib2.Http()

# Try to retrieve credentials from storage or run the flow to generate them
credentials = STORAGE.get()
if credentials is None or credentials.invalid:
  credentials = run(flow, STORAGE, http=http)

# Authorize the httplib2.Http object with our credentials
http = credentials.authorize(http)

## Save credentials to storage
# STORAGE.put(credentials)
# Build the Gmail service from discovery
gmail_service = build('gmail', 'v1', http=http)

message = MIMEMultipart('mixed')
message['to'] = to
message['from'] = "YYY@gmail.com"
message['subject'] = subject
message.preamble = 'This is a multi-part message in MIME format.'


for filename, name in attachment_paths_mixed:
    print '----'
    print filename
    print name
    content_type, encoding = mimetypes.guess_type(filename)

    if content_type is None or encoding is not None:
        content_type = 'application/octet-stream'
    main_type, sub_type = content_type.split('/', 1)

    if main_type == 'text':
        fp = open(filename, 'rb')
        msg = MIMEText(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'image':
        fp = open(filename, 'rb')
        msg = MIMEImage(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'audio':
        fp = open(filename, 'rb')
        msg = MIMEAudio(fp.read(), _subtype=sub_type)
        fp.close()
    else:
        fp = open(filename, 'rb')
        msg = MIMEBase(main_type, sub_type)
        msg.set_payload(fp.read())
        fp.close()

        msg.add_header('Content-Disposition', 'attachment', filename=name)
        message.attach(msg)

# Encapsulate the plain and HTML versions of the message body in an
# 'alternative' part, so message agents can decide which they want to     display.
msgAlternative = MIMEMultipart('alternative')
message.attach(msgAlternative)

msg1 = MIMEText(message_main, 'plain')
msg2 = MIMEText(message_main, 'html')
msgAlternative.attach(msg1)
msgAlternative.attach(msg2)

for filename, link in attachment_paths_html:
    print '----'
    print filename
    print link
    content_type, encoding = mimetypes.guess_type(filename)

    if content_type is None or encoding is not None:
        content_type = 'application/octet-stream'
    main_type, sub_type = content_type.split('/', 1)

    if main_type == 'text':
        fp = open(filename, 'rb')
        msg = MIMEText(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'image':
        fp = open(filename, 'rb')
        msg = MIMEImage(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'audio':
        fp = open(filename, 'rb')
        msg = MIMEAudio(fp.read(), _subtype=sub_type)
        fp.close()
    else:
        fp = open(filename, 'rb')
        msg = MIMEBase(main_type, sub_type)
        msg.set_payload(fp.read())
        fp.close()

        # Define the image's ID as referenced above
        msg.add_header('Content-ID', link)
        message.attach(msg)

body = {'raw': base64.urlsafe_b64encode(message.as_string())}

# body = {'raw': base64.b64encode(message.as_string())}

# send it
try:
  message = (gmail_service.users().messages().send(userId="me", body=body).execute())
  print('Message Id: %s' % message['id'])
  print(message)
except Exception as error:
  print('An error occurred: %s' % error)