Множество различных вложений с помощью Gmail API в Django
Как в Django учесть отправку нескольких вложений (или без вложений) (через request.FILES
) с помощью Gmail API, чтобы сохранить старый идентификатор сообщения (т.е. «FBf...MiD») для последующего получения ответа в той же теме/thread_ID? Я перехожу с SMTP (который будет полностью упразднен Google) на Django's email.send(), который, как оказалось, значительно отличается в том, как он обрабатывает типы файлов.
Я специально пытаюсь выяснить, как прикрепить несколько файлов разных типов к API Gmail. (не знаю, должен ли я сериализовать EmailMultiAlternatives и как это сделать)
view.py
class MakeQuoteWithItems(…, CreateView):
def post(self, request, *args, **kwargs):
# init a Django Model
quote = QuoteClass(request)
# with a generated PDF, and send the associated email
quote.make_and_email_quote()
Текущий способ работы (SMTP)
from django.core.mail import EmailMultiAlternatives
def emailContactsTheOptionsQuote(quote, attachments=None):
from_email = …
subject = …
email_context = …
html_content = render_to_string("email/tempalte.html", email_context, quote.request)
to_emails = … (string, [] or whatever format needed)
…
#
# Current working way sending through SMTP
#
# Django settings somehow gets the email out
# EMAIL_USE_TLS = False
# EMAIL_HOST = os.getenv("EMAIL_PROVIDER_SMTP_NOTIFIER_HOST")
# EMAIL_HOST_USER = os.getenv("EMAIL_PROVIDER_SMTP_NOTIFIER_EMAIL")
# EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PROVIDER_SMTP_NOTIFIER_PASSWORD")
# EMAIL_PORT = 587
#
mail = EmailMultiAlternatives(
subject,
strip_tags(html_content),
to=to_emails,
bcc=bcc_emails,
from_email=from_email,
)
mail.attach_alternative(html_content, "text/html")
mail.attach(
quote.pdf_canvas._filename, open(quote.canvas._filename, "rb").read(), "application/pdf"
)
if quote.attachments:
for attachment in quote.attachments:
mail.attach(attachment._name, attachment.read(), attachment.content_type)
mail.send(fail_silently=False)
# But can't get a hold of the mail going out to get it's ID to reply to later
Предложенный способ (Gmail API)
def emailContactsTheOptionsQuote(quote, attachments=None):
from_email = …
subject = …
# Instead -
gmail_email = create_and_send_gmail_message(
from_email,
to_emails,
subject,
html_content,
text_content,
options_quote.request,
attachmentFiles=email_attachments,
)
# Some Django Model to query to get the email.message_id and email.thread_id
GmailEmailLog.objects.create(
gmail_email=gmail_email,
message_id=gmail_email.message_id,
# Thread ids are the same for the first message in a thread, but remain
# the same for all subsequent messages sent within the thread
thread_id=gmail_email.thread_id,
quote_id=quote.id
…
)
return gmail_email
helper_functions.py
def create_and_send_gmail_message(
sender,
to,
subject,
msgHtml,
msgPlain,
request,
attachmentFiles=None,
cc=None,
bcc=None,
):
if attachmentFiles:
message_body = create_body_message_with_attachments(sender,to,subject,msgHtml,msgPlain,attachmentFiles,cc,bcc,)
else:
# This is not an issue
message_body = create_messag_body_html(sender,to,subject,msgHtml,msgPlain,cc,bcc,)
result = SendMessageInternal(message_body, request)
return result
def SendMessageInternal(incoming_message_body, request):
credentials = get_credentials(request)
service = discovery.build("gmail", "v1", credentials=credentials)
user_id = settings.EMAIL_GMAIL_USERID
try:
msg = (
service.users()
.messages()
.send(
userId=user_id,
body=incoming_message_body,
)
.execute()
)
print(f"Message Id: {msg['id']}")
return msg
except errors.HttpError as error:
print(f"An error occurred: {error}")
return f"{error=}"
return "OK"
Вот где происходит магия/головная боль и возникают проблемы. Я не уверен, почему мне кажется, что я заново изобретаю колесо, и не могу найти никаких пакетов со стороны сервера, которые облегчают эту задачу. Я могу заставить «вложения» «прикрепляться» к письму и отображаться в пользовательском интерфейсе, но файлы не доступны для клика, а иногда пустые, учитывая их тип.
Заметки (которые помогают дифференцировать другие вопросы СЦ)
- Не используем SMTP, так как он уже начал закатываться/разрушаться, и пытаемся использовать настройку аккаунта google "менее безопасные приложения"
- Используем EmailMultiAlternatives от
django.core.mail
для составления и отправки, которыйmail.send(fail_silently=True)
не возвращает сообщение, или не дает ID на сервере gmail, так что поиск точного email не является функциональным/детерминированным, учитывая сходства - В Django, когда загружаемые файлы проходят через
request.FILES
, они имеют типInMemoryUploadedFile
отdjango.core.files.uploadedfile
¿с потенциалом_io.BufferedReader
быть разными MIME-типами? , но внутреннее вложение сгенерированного PDF имеет тип <<<11>>>. Предположительно, это и является причиной моей головной боли - использую Python 3.11.7 в dev и Python 3.12.3 в prod, и знаю, что в 3. 13, что
mimetypes.guess_type()
будет устаревшим согласно this - Отправляя EmailMultiAlternatives напрямую в Gmail API
send().execute()
, я получаю ошибкуObject of type EmailMultiAlternatives is not JSON serializable
- The MIME information from the received email is:
.
--===============2268240127970561189==
Content-Type: image/png
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Screenshot 2025-03-12 at 1.54.01PM.png"
--===============2268240127970561189==
Content-Type: image/png
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Screenshot 2025-03-12 at 4.44.11PM.png"
--===============2268240127970561189==
Content-Type: application/octet-stream
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Q-DFD-12345-031525-0XEH-1153.pdf"
--===============2268240127970561189==--
Заранее спасибо :)
Я не знаю, получил ли я именно то, что вам нужно, но да, вы можете отправить несколько типов файлов в одном сообщении gmail ...
У меня есть некоторый код, который я использую для подготовки таких сообщений:
Я использую динамические настройки почты (не статические), но вы можете не обращать на это внимания.
вот как я использую эту программу для обработки электронной почты:
body = render_to_string(TEMPLATE_PATH, CONTEXT_VARIABLES)
recipients = ...
subject = ...
message_id = EmailWrapper.send_mail(
subject=subject,
body=body,
recipients=recipients,
attachments=attachments,
)
import base64
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
import mimetypes
def create_message_with_attachments(sender, to, subject, message_text, files):
message = MIMEMultipart()
message['to'] = to
message['from'] = sender
message['subject'] = subject
msg = MIMEText(message_text)
message.attach(msg)
# Attach files
for file in files:
file.seek(0) # Ensure you're starting at the beginning of the file
file_content = file.read()
mime_type, encoding = mimetypes.guess_type(file.name)
main_type, sub_type = (mime_type or 'application/octet-stream').split('/', 1)
part = MIMEBase(main_type, sub_type)
part.set_payload(file_content)
encoders.encode_base64(part)
part.add_header('Content-Disposition', f'attachment; filename="{file.name}"')
message.attach(part)
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
return {'raw': raw_message}
def send_message(service, user_id, message):
try:
sent_message = (service.users().messages().send(userId=user_id, body=message)
.execute())
print(f'Message Id: {sent_message["id"]}')
return sent_message
except Exception as error:
print(f'An error occurred: {error}')
return None
# Assuming you have the Gmail API setup and credentials
creds = Credentials.from_authorized_user_file('path_to_credentials.json')
service = build('gmail', 'v1', credentials=creds)
message = create_message_with_attachments(
sender="your-email@example.com",
to="recipient@example.com",
subject="Your Subject",
message_text="Email body goes here.",
files=request.FILES.values()
)
send_message(service, "me", message)
Что касается первой проблемы, вложения размером 0 Кб, я полагаю, что ошибка здесь:
elif isinstance(attachment, InMemoryUploadedFile):
content_type = attachment.content_type or content_type
file_data = attachment.read()
В файле InMemoryUploadedFile нет метода.read()
, поэтому просто откройте его, как в блоке "if".
Вам не следует переходить EmailMultiAlternatives
к почтовому API Google, способ взаимодействия с ним кажется правильным.
service.users()
.messages()
.send(
userId=user_id,
body=incoming_message_body,
)
.execute()
с incoming_massage_body
равно:
{"raw": base64.urlsafe_b64encode(message.as_bytes()).decode()}
так, как кажется. Это должно работать до тех пор, пока message
является экземпляром MIMEMultipart
(который является). В любом случае, вы должны быть в состоянии создать допустимый экземпляр MIMEMultipart
, получая значения из экземпляра EmailMultiAlternatives
.
Какую ошибку вы получаете из-за этого блока?
except errors.HttpError as error:
print(f"An error occurred: {error}")
return f"{error=}"
Я попытался найти простейшее рабочее решение, требующее наименьшего количества нового кода. Для этой цели я адаптировал ваш предыдущий код, в котором вы использовали SMTP
и mail.send(...)
. Я взял вашу функцию emailTheQuote
за основу и немного модифицировал ее для работы с GmailAPI
. Рабочая версия будет выглядеть следующим образом:
def emailTheQuote(quote, attachments=None):
from_email = ...
subject = ...
email_context = ...
html_content = render_to_string(
"email/tempalte.html",
email_context,
quote.request,
)
to_emails = ... # (string, [] or whatever format needed)
# Your other code
mail = EmailMultiAlternatives(
subject,
strip_tags(html_content),
to=to_emails,
bcc=bcc_emails,
from_email=from_email,
)
mail.attach_alternative(html_content, "text/html")
mail.attach(
filename=quote.pdf_canvas._filename,
content=open(quote.canvas._filename, "rb").read(),
mimetype="application/pdf"
)
if quote.attachments:
for attachment in quote.attachments:
mail.attach(
# Use public attribute `name` instead of `_name`
filename=attachment.name,
content=attachment.read(),
mimetype=attachment.content_type,
)
message_bytes = mail.message().as_bytes(linesep='\r\n')
raw_message = base64.urlsafe_b64encode(message_bytes)
gmail_response = SendMessageInternal(
incoming_message_body={'raw': raw_message.decode()},
request=quote.request,
)
print(gmail_response)
# Now save some data from `gmail_response` to DB
Как вы можете видеть, здесь появилось всего несколько новых строк кода:
message_bytes = mail.message().as_bytes(linesep='\r\n')
raw_message = base64.urlsafe_b64encode(message_bytes)
gmail_response = SendMessageInternal(
incoming_message_body={'raw': raw_message.decode()},
request=quote.request,
)
По сути, когда вы создаете экземпляр EmailMultiAlternatives
, вы используете удобную оболочку, которая инкапсулирует всю низкоуровневую логику. При вызове метода EmailMultiAlternatives(...).send
в конечном итоге будет вызван метод _send
, который в конечном итоге дает нам представление о том, какие методы будут вызваны при фактической отправке электронного письма. Все, что нам нужно сделать, это использовать аналогичную логику, вызвать EmailMultiAlternatives(...).message()
преобразовать сообщение в набор байтов и закодировать байты с помощью base64.urlsafe_b64encode
(это описано в документации).
"Необработанный": "Строка", # Целое сообщение электронной почты в формате RFC 2822 и в кодировке base64url. Возвращается в ответах
messages.get
иdrafts.get
при указании параметраformat=RAW
.
Затем делегируйте отправку сообщения клиенту Gmail
. Фактическое описание метода message
можно найти здесь, вот небольшая часть его описания из документации:
<время работы/>
message()
создает объектdjango.core.mail.SafeMIMEText
(подкласс класса PythonMIMEText
) или объектdjango.core.mail.SafeMIMEMultipart
, содержащий сообщение для отправки.
Теперь он должен работать корректно, как и раньше, при использовании SMTP
. Но поскольку вы не предоставили некоторые из своих функций, я написал свой собственный код, чтобы протестировать это. Если кто-то хочет протестировать приведенный ниже код, но никогда раньше не сталкивался с Oauth
и GmailAPI
, вот мой ответ он немного отличается, но в нем много общего полезных ссылок, которые могут дать вам хорошее представление о правильном пути и общее представление о том, как это работает.
{#test_email.html#}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Hello World!</h1>
<p><b>Lorem Ipsum</b> is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
when an unknown printer took a galley of type and scrambled it to make a type
specimen book. <i>It has survived not only</i> five centuries, but also the leap into
electronic typesetting, remaining essentially unchanged. It was popularised
in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
and more recently with desktop publishing software like Aldus PageMaker
including versions of Lorem Ipsum.
</p>
</body>
</html>
{#test_template.html#}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<main>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">SEND FILES</button>
</form>
</main>
</div>
</body>
</html>
Все максимально просто - одна view
функция, которая принимает файлы из формы (не забудьте добавить ее в urlpatterns
). Также есть функция create_and_send_email_using_gmail_api
, которая почти в точности копирует поведение вашей функции emailTheQuote
. И два простых шаблона html
. Я также поместил два файла (на том же уровне, где находится views.py
): файл с google credentials
и простой pdf
для тестирования.
Это было протестировано на файлах разных типов и размеров и дало правильные результаты. Вот пример сообщения с вложениями внутри, которое я получил.
<время работы/>Вот несколько полезных ссылок:
- Документация клиент для работы с
GmailAPI
. - Пример из документации по отправке электронных писем (пример есть в
python
). - Руководство из документации "Начало работы" с
python
. - Документация по API для метода
users.messages.send
(вы можете проверить отправку непосредственно со страницы).
И, наконец, я хотел бы добавить этот фрагмент :
Если вы пытаетесь отправить ответ и хотите, чтобы сообщение попало в ветку рассылки, убедитесь, что:
- Заголовки
Subject
совпадают с- Заголовки
References
иIn-Reply-To
соответствуют стандарту RFC 2822.
Я думаю, это также может быть полезно для вас, если вы планируете использовать threadId
для ответа на сообщение. Напоминаю, что дополнительные заголовки можно передавать следующим образом:
EmailMultiAlternatives(headers={'References': ...})
Это отправляет электронные письма через Gmail API в Django, захватывая вложения из request.FILES.getlist()
и вводя их с помощью MIME_HANDLERS
dict; Я рекомендую использовать object вместо conditions, когда есть несколько ветвей.
Он возвращает идентификатор сообщения для потоковых ответов. В этом решении вам нужно будет передать csrf_token
непосредственно в creds
аргумент.
Gmail API в Django, заменяющий устаревший подход SMTP. Он обрабатывает запрос.ФАЙЛЫ-как InMemoryUploadedFile, сгенерированные PDF-файлы -как _io.BufferedReader и сохраняет идентификатор сообщения и идентификатор потока для последующей обработки в потоковом режиме
Правильные MIME-типы используют content_type из InMemoryUploadedFile
Совместимость с Gmail API - Создайте MimeMultipart-сообщение и закодируйте его в base64url
Отслеживание потоков - фиксируйте id и ThreadId в ответе api
import base64
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
import mimetypes
def create_message(sender, to, subject, body, files):
message = MIMEMultipart()
message['to'] = to
message['from'] = sender
message['subject'] = subject
# Add the body of the email
message.attach(MIMEText(body, 'plain'))
# Add files as attachments
for file in files:
file.seek(0) # Reset the file pointer to the start
file_content = file.read()
mime_type, _ = mimetypes.guess_type(file.name)
main_type, sub_type = (mime_type or 'application/octet-stream').split('/', 1)
part = MIMEBase(main_type, sub_type)
part.set_payload(file_content)
encoders.encode_base64(part)
# Add headers and attachment to the message
part.add_header('Content-Disposition', f'attachment; filename="{file.name}"')
message.attach(part)
# Convert message to base64 format
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
return {'raw': raw_message}
def send_message(service, user_id, message):
try:
sent_message = service.users().messages().send(userId=user_id, body=message).execute()
print(f'Message Id: {sent_message["id"]}')
return sent_message
except Exception as error:
print(f'An error occurred: {error}')
return None
# Assuming the Gmail API service is initialized
creds = Credentials.from_authorized_user_file('path_to_credentials.json')
service = build('gmail', 'v1', credentials=creds)
# Example usage to send an email with an attachment
files = ['path_to_file1', 'path_to_file2'] # Replace these with your actual file objects
message = create_message(
sender="your-email@example.com",
to="recipient@example.com",
subject="Email with Attachments",
body="This is the body of the email.",
files=files
)
send_message(service, "me", message)