mock server, file and folder management
This commit is contained in:
@@ -8,6 +8,12 @@ import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.mailer.campaign.models import ImapConfig, TransportSecurity
|
||||
from app.mailer.dev.mock_mailbox import (
|
||||
MOCK_IMAP_FOLDERS,
|
||||
consume_fail_next_imap,
|
||||
is_mock_imap_host,
|
||||
record_imap_append,
|
||||
)
|
||||
|
||||
|
||||
class ImapConfigurationError(ValueError):
|
||||
@@ -210,6 +216,14 @@ def test_imap_login(*, imap_config: ImapConfig) -> ImapLoginTestResult:
|
||||
"""
|
||||
|
||||
host, port = _require_imap_config(imap_config)
|
||||
if is_mock_imap_host(imap_config.host):
|
||||
return ImapLoginTestResult(
|
||||
host=host,
|
||||
port=port,
|
||||
security=imap_config.security.value,
|
||||
authenticated=bool(imap_config.username and imap_config.password),
|
||||
)
|
||||
|
||||
client = _open_imap(imap_config)
|
||||
try:
|
||||
return ImapLoginTestResult(
|
||||
@@ -229,6 +243,16 @@ def list_imap_folders(*, imap_config: ImapConfig) -> ImapFolderListResult:
|
||||
"""Return folders visible through IMAP LIST and the best sent-folder guess."""
|
||||
|
||||
host, port = _require_imap_config(imap_config)
|
||||
if is_mock_imap_host(imap_config.host):
|
||||
folders = [ImapMailboxInfo(name=str(item["name"]), flags=list(item.get("flags") or [])) for item in MOCK_IMAP_FOLDERS]
|
||||
return ImapFolderListResult(
|
||||
host=host,
|
||||
port=port,
|
||||
security=imap_config.security.value,
|
||||
folders=folders,
|
||||
detected_sent_folder="Sent",
|
||||
)
|
||||
|
||||
client = _open_imap(imap_config)
|
||||
try:
|
||||
typ, data = client.list()
|
||||
@@ -272,6 +296,20 @@ def append_message_to_sent(
|
||||
"""
|
||||
|
||||
host, port = _require_imap_config(imap_config)
|
||||
if is_mock_imap_host(imap_config.host):
|
||||
if consume_fail_next_imap():
|
||||
raise ImapAppendError("Mock IMAP configured to fail the next append", temporary=False)
|
||||
target_folder = folder or (imap_config.sent_folder if imap_config.sent_folder and imap_config.sent_folder != "auto" else "Sent")
|
||||
record = record_imap_append(message_bytes, folder=target_folder, imap_host=imap_config.host)
|
||||
return ImapAppendResult(
|
||||
host=host,
|
||||
port=port,
|
||||
security=imap_config.security.value,
|
||||
folder=target_folder,
|
||||
bytes_appended=len(message_bytes),
|
||||
response=f"mock append stored as {record.id}",
|
||||
)
|
||||
|
||||
client: imaplib.IMAP4 | None = None
|
||||
try:
|
||||
client = _open_imap(imap_config)
|
||||
|
||||
@@ -30,6 +30,7 @@ from app.mailer.persistence.campaigns import _write_campaign_snapshot
|
||||
from app.mailer.sending.rate_limit import wait_for_rate_limit
|
||||
from app.mailer.sending.smtp import SmtpConfigurationError, SmtpSendError, send_email_message
|
||||
from app.mailer.sending.imap import ImapAppendError, ImapConfigurationError, append_message_to_sent
|
||||
from app.storage.services import mark_job_attachment_uses_sent
|
||||
|
||||
|
||||
class QueueingError(RuntimeError):
|
||||
@@ -591,6 +592,7 @@ def send_campaign_job(session: Session, *, job_id: str, dry_run: bool = False, u
|
||||
else:
|
||||
job.imap_status = JobImapStatus.NOT_REQUESTED.value
|
||||
job.last_error = None
|
||||
mark_job_attachment_uses_sent(session, job)
|
||||
session.add(attempt)
|
||||
session.add(job)
|
||||
_update_campaign_after_job(session, job.campaign_id, job.campaign_version_id)
|
||||
@@ -702,6 +704,7 @@ def append_sent_for_job(session: Session, *, job_id: str, dry_run: bool = False)
|
||||
attempt.folder = result.folder
|
||||
job.imap_status = JobImapStatus.APPENDED.value
|
||||
job.last_error = None
|
||||
mark_job_attachment_uses_sent(session, job)
|
||||
session.add(attempt)
|
||||
session.add(job)
|
||||
session.commit()
|
||||
|
||||
@@ -8,6 +8,12 @@ from email.message import EmailMessage
|
||||
from email.utils import formataddr
|
||||
|
||||
from app.mailer.campaign.models import SmtpConfig, TransportSecurity
|
||||
from app.mailer.dev.mock_mailbox import (
|
||||
consume_fail_next_smtp,
|
||||
get_failures,
|
||||
is_mock_smtp_host,
|
||||
record_smtp_delivery,
|
||||
)
|
||||
|
||||
|
||||
class SmtpConfigurationError(ValueError):
|
||||
@@ -98,6 +104,15 @@ def test_smtp_login(*, smtp_config: SmtpConfig) -> SmtpLoginTestResult:
|
||||
"""
|
||||
|
||||
host, port = _require_smtp_config(smtp_config)
|
||||
if is_mock_smtp_host(smtp_config.host):
|
||||
host, port = _require_smtp_config(smtp_config)
|
||||
return SmtpLoginTestResult(
|
||||
host=host,
|
||||
port=port,
|
||||
security=smtp_config.security.value,
|
||||
authenticated=bool(smtp_config.username and smtp_config.password),
|
||||
)
|
||||
|
||||
smtp = _open_smtp(smtp_config)
|
||||
try:
|
||||
return SmtpLoginTestResult(
|
||||
@@ -165,6 +180,37 @@ def send_email_message(
|
||||
if not envelope_recipients:
|
||||
raise SmtpConfigurationError("at least one SMTP envelope recipient is required")
|
||||
|
||||
if is_mock_smtp_host(smtp_config.host):
|
||||
if consume_fail_next_smtp():
|
||||
raise SmtpSendError("Mock SMTP configured to fail the next send")
|
||||
failures = get_failures()
|
||||
reject_text = str(failures.get("smtp_reject_recipients_containing") or "").strip().lower()
|
||||
refused: dict[str, tuple[int, bytes]] = {}
|
||||
accepted = list(envelope_recipients)
|
||||
if reject_text:
|
||||
refused = {
|
||||
recipient: (550, b"mock recipient rejected")
|
||||
for recipient in envelope_recipients
|
||||
if reject_text in recipient.lower()
|
||||
}
|
||||
accepted = [recipient for recipient in envelope_recipients if recipient not in refused]
|
||||
if not accepted:
|
||||
raise SmtpSendError(f"all mock SMTP recipients were refused: {_decode_refused(refused)}")
|
||||
record_smtp_delivery(
|
||||
message,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=accepted,
|
||||
smtp_host=smtp_config.host,
|
||||
)
|
||||
return SmtpSendResult(
|
||||
host=host,
|
||||
port=port,
|
||||
security=smtp_config.security.value,
|
||||
envelope_from=envelope_from,
|
||||
envelope_recipients=list(envelope_recipients),
|
||||
refused_recipients=_decode_refused(refused),
|
||||
)
|
||||
|
||||
try:
|
||||
with _open_smtp(smtp_config) as smtp:
|
||||
refused = smtp.send_message(
|
||||
|
||||
Reference in New Issue
Block a user