first version able to send

This commit is contained in:
2026-06-11 00:06:44 +02:00
parent ce43f2658f
commit 3b06f3670e
12 changed files with 740 additions and 67 deletions

View File

@@ -136,6 +136,14 @@ class RecipientConfig(StrictModel):
class RecipientsConfig(StrictModel):
from_: RecipientConfig | None = Field(default=None, alias="from")
@field_validator("from_", mode="before")
@classmethod
def empty_from_object_means_unset(cls, value: Any) -> Any:
if isinstance(value, dict) and not any(value.values()):
return None
return value
allow_individual_from: bool = False
to: list[RecipientConfig] = Field(default_factory=list)
@@ -200,6 +208,17 @@ class ZipConfig(StrictModel):
method: ZipMethod = ZipMethod.AES
class AttachmentBasePathConfig(StrictModel):
id: str | None = None
name: str
path: str = "."
allow_individual: bool = False
# Legacy UI builds briefly wrote a source value. Keep accepting it so older
# drafts do not become invalid merely because the current UI no longer shows
# or edits that column.
source: str | None = None
class AttachmentConfig(StrictModel):
id: str | None = None
label: str | None = None
@@ -208,26 +227,45 @@ class AttachmentConfig(StrictModel):
include_subdirs: bool = False
required: bool = True
allow_multiple: bool = False
missing_behavior: Behavior = Behavior.ASK
ambiguous_behavior: Behavior = Behavior.ASK
# None means: inherit from validation_policy. Explicit values remain
# supported for backwards compatibility and per-rule overrides.
missing_behavior: Behavior | None = None
ambiguous_behavior: Behavior | None = None
zip: ZipConfig = Field(default_factory=ZipConfig)
class AttachmentsConfig(StrictModel):
base_path: str = "."
base_paths: list[AttachmentBasePathConfig] = Field(default_factory=list)
allow_individual: bool = False
send_without_attachments: bool = True
global_: list[AttachmentConfig] = Field(default_factory=list, alias="global")
missing_behavior: Behavior = Behavior.ASK
ambiguous_behavior: Behavior = Behavior.ASK
@property
def individual_base_path_values(self) -> set[str]:
return {base_path.path for base_path in self.base_paths if base_path.allow_individual}
class EntryConfig(StrictModel):
id: str | None = None
active: bool = True
# Compatibility fields written by older/current WebUI recipient rows.
# Address routing uses the explicit to/cc/bcc/reply_to/from fields below;
# these values are retained for round-tripping but are not used for sending.
name: str | None = None
email: str | None = None
from_: RecipientConfig | None = Field(default=None, alias="from")
@field_validator("from_", mode="before")
@classmethod
def empty_from_object_means_unset(cls, value: Any) -> Any:
if isinstance(value, dict) and not any(value.values()):
return None
return value
to: list[RecipientConfig] = Field(default_factory=list)
combine_to: bool = True
@@ -270,8 +308,12 @@ class EntriesConfig(StrictModel):
@model_validator(mode="after")
def inline_or_external(self) -> "EntriesConfig":
has_inline = self.inline is not None
has_external = self.source is not None or self.mapping is not None or self.defaults is not None
if has_inline and has_external:
has_external_source = self.source is not None or self.mapping is not None
# defaults are compatible with both inline and external entries. The
# WebUI stores the current per-entry combination defaults here even for
# inline campaigns, so treating defaults as an external-source marker
# made valid UI drafts fail backend validation.
if has_inline and has_external_source:
raise ValueError("entries must be either inline or source-based, not both")
if has_inline:
return self
@@ -292,6 +334,7 @@ class ValidationPolicy(StrictModel):
missing_required_attachment: Behavior = Behavior.ASK
missing_optional_attachment: Behavior = Behavior.WARN
ambiguous_attachment_match: Behavior = Behavior.ASK
ignore_empty_fields: bool = False
missing_email: MissingAddressBehavior = MissingAddressBehavior.BLOCK
template_error: MissingAddressBehavior = MissingAddressBehavior.BLOCK
inactive_entry: InactiveEntryBehavior = InactiveEntryBehavior.DROP