Skip to content

Commit 894cdb5

Browse files
committed
feat: chunk upload support in outbound.send_raw
1 parent eb90f62 commit 894cdb5

File tree

2 files changed

+73
-15
lines changed

2 files changed

+73
-15
lines changed

mail/api/outbound.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import os
12
from email.utils import parseaddr
23

34
import frappe
45
from frappe import _
56
from frappe.utils.file_manager import save_file
67
from werkzeug.datastructures.file_storage import FileStorage
8+
from werkzeug.utils import secure_filename
79

810
from mail.mail.doctype.mail_queue.mail_queue import MailQueue
11+
from mail.utils import get_messages_directory
912
from mail.utils.cache import get_account_for_user
1013
from mail.utils.rate_limiter import dynamic_rate_limit
1114
from mail.utils.user import has_role
@@ -98,25 +101,22 @@ def send_raw(
98101
raw_message: str | None = None,
99102
is_newsletter: bool = False,
100103
) -> str:
101-
"""Send Raw Mail."""
104+
"""Send raw email. Supports both single-shot and chunked upload."""
105+
106+
chunk_index = frappe.form_dict.get("chunk_index")
107+
total_chunks = frappe.form_dict.get("total_chunk_count")
108+
upload_session = frappe.form_dict.get("uuid")
109+
110+
if chunk_index is not None and total_chunks is not None and upload_session:
111+
return _handle_chunked_upload(
112+
from_, to, is_newsletter, int(chunk_index), int(total_chunks), str(upload_session)
113+
)
102114

103-
from_name, from_email = parseaddr(from_)
104115
raw_message = raw_message or get_message_from_files()
105116
if not raw_message:
106117
frappe.throw(_("The raw message is required."), frappe.MandatoryError)
107118

108-
doc = MailQueue._create(
109-
account=get_account(),
110-
from_name=from_name,
111-
from_email=from_email,
112-
recipients=format_recipients(to),
113-
via_api=True,
114-
newsletter=is_newsletter,
115-
raw_message=raw_message,
116-
delivery_mode="Batch" if is_newsletter else "Enqueue",
117-
)
118-
119-
return doc.name
119+
return _enqueue_mail(from_, to, raw_message, is_newsletter)
120120

121121

122122
def get_account() -> str:
@@ -131,7 +131,7 @@ def get_account() -> str:
131131

132132

133133
def get_message_from_files() -> str | None:
134-
"""Returns the message from the files"""
134+
"""Extracts message from uploaded file in single upload mode."""
135135

136136
files = frappe._dict(frappe.request.files)
137137

@@ -164,6 +164,35 @@ def format_reply_to(reply_to: str | list[str] | None) -> list[dict]:
164164
return _normalize_recipients(reply_to)
165165

166166

167+
def _handle_chunked_upload(
168+
from_: str, to: str | list[str], is_newsletter: bool, chunk_index: int, total_chunks: int, session_id: str
169+
) -> str:
170+
"""Handle chunked uploads for large emails."""
171+
172+
file = frappe.request.files.get("raw_message")
173+
if not file:
174+
frappe.throw(_("No file part named 'raw_message' found."))
175+
176+
upload_dir = get_messages_directory()
177+
filename = secure_filename(f"{session_id}.eml")
178+
temp_path = os.path.join(upload_dir, filename)
179+
offset = int(frappe.form_dict.get("chunk_byte_offset", 0))
180+
181+
with open(temp_path, "ab") as f:
182+
f.seek(offset)
183+
f.write(file.stream.read())
184+
185+
if chunk_index < total_chunks - 1:
186+
return f"Chunk {chunk_index + 1} of {total_chunks} received."
187+
188+
with open(temp_path, "rb") as f:
189+
raw_message = f.read().decode("utf-8")
190+
191+
os.remove(temp_path)
192+
193+
return _enqueue_mail(from_, to, raw_message, is_newsletter)
194+
195+
167196
def _normalize_recipients(
168197
recipients: str | list[str] | None, recipient_type: str | None = None
169198
) -> list[dict]:
@@ -181,3 +210,24 @@ def _normalize_recipients(
181210
result.append(recipient_dict)
182211

183212
return result
213+
214+
215+
def _enqueue_mail(from_: str, to: str | list[str], raw_message: str, is_newsletter: bool = False) -> str:
216+
"""Enqueue mail in MailQueue."""
217+
218+
from_name, from_email = parseaddr(from_)
219+
if not raw_message:
220+
frappe.throw(_("The raw message is required."), frappe.MandatoryError)
221+
222+
doc = MailQueue._create(
223+
account=get_account(),
224+
from_name=from_name,
225+
from_email=from_email,
226+
recipients=format_recipients(to),
227+
via_api=True,
228+
newsletter=is_newsletter,
229+
raw_message=raw_message,
230+
delivery_mode="Batch" if is_newsletter else "Enqueue",
231+
)
232+
233+
return doc.name

mail/utils/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,14 @@ def get_mail_app_path() -> str:
313313
return os.path.join(get_bench_path(), "apps/mail")
314314

315315

316+
def get_messages_directory() -> str:
317+
"""Returns the path to the messages directory for the current site."""
318+
319+
directory = os.path.join(get_bench_path(), "sites", frappe.local.site, "raw_messages")
320+
os.makedirs(directory, exist_ok=True)
321+
return directory
322+
323+
316324
def get_import_directory() -> str:
317325
"""Returns the path to the import directory for the current site."""
318326

0 commit comments

Comments
 (0)