diff --git a/config.json b/config.json
index d2d5cb7..4a26dac 100644
--- a/config.json
+++ b/config.json
@@ -1,6 +1,6 @@
{
"id": "events",
- "version": "1.6.1-aio.14",
+ "version": "1.6.1-aio.15",
"name": "Events",
"repo": "https://git.atitlan.io/aiolabs/events",
"short_description": "Sell and register event tickets",
diff --git a/models.py b/models.py
index 9a24822..be5fb69 100644
--- a/models.py
+++ b/models.py
@@ -38,6 +38,10 @@ class EventExtra(BaseModel):
# signs — so organizers get a human sender without touching mail auth.
organizer_name: str = ""
reply_to_email: EmailStr | None = None
+ # BCC every ticket email to the reply-to address (explicit or the
+ # owner's account email) so the organizer keeps a copy — LNbits submits
+ # over SMTP, nothing lands in a Sent folder otherwise.
+ copy_to_organizer: bool = False
@validator("reply_to_email", pre=True)
def blank_reply_to_is_none(cls, v):
diff --git a/services.py b/services.py
index 2d2644b..7969b16 100644
--- a/services.py
+++ b/services.py
@@ -255,6 +255,13 @@ async def _deliver_ticket_notifications(
if result.email.attempted:
try:
assert ticket.email
+ bcc = (
+ [reply_to]
+ if event.extra.copy_to_organizer
+ and reply_to
+ and reply_to.lower() != ticket.email.lower()
+ else []
+ )
card = render_ticket_card(
ticket,
event,
@@ -271,6 +278,7 @@ async def _deliver_ticket_notifications(
],
reply_to=reply_to,
sender_name=organizer_sender_name(event),
+ bcc=bcc,
)
ticket.extra.email_notification_sent = True
result.email.sent = True
@@ -304,6 +312,7 @@ async def _send_ticket_email_notification(
attachments: list[tuple[str, bytes]] | None = None,
reply_to: str | None = None,
sender_name: str | None = None,
+ bcc: list[str] | None = None,
) -> None:
"""Multipart (text + HTML) ticket email through the instance SMTP
settings. Core's `send_email_notification` is plain-text only, which is
@@ -320,6 +329,10 @@ async def _send_ticket_email_notification(
for address in to_emails:
if not is_valid_email_address(address):
raise ValueError(f"Invalid email address: {address}")
+ # BCC recipients ride only on the SMTP envelope, never in a header.
+ envelope_recipients = list(to_emails) + [
+ b for b in (bcc or []) if is_valid_email_address(b)
+ ]
msg = build_ticket_email(
from_email,
@@ -339,7 +352,7 @@ async def _send_ticket_email_notification(
username,
settings.lnbits_email_notifications_password,
from_email,
- to_emails,
+ envelope_recipients,
msg.as_string(),
)
diff --git a/static/js/index.js b/static/js/index.js
index b882497..b5c4d1f 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -133,6 +133,19 @@ window.PageEvents = {
{name: 'event', align: 'left', label: 'Event', field: 'event'},
{name: 'name', align: 'left', label: 'Name', field: 'name'},
{name: 'email', align: 'left', label: 'Email', field: 'email'},
+ {
+ name: 'email_sent',
+ align: 'left',
+ label: 'Email sent',
+ field: row =>
+ !row.email
+ ? 'no email'
+ : row.extra?.email_notification_sent
+ ? '\u2713 sent'
+ : row.paid
+ ? 'not sent'
+ : 'unpaid'
+ },
{
name: 'registered',
align: 'left',
@@ -207,6 +220,7 @@ window.PageEvents = {
payment_methods: ['lightning'],
promo_codes: [],
organizer_name: '',
+ copy_to_organizer: false,
reply_to_email: '',
notification_subject: '',
notification_body: ''
@@ -585,6 +599,7 @@ window.PageEvents = {
nostr_notifications: false,
promo_codes: [],
organizer_name: '',
+ copy_to_organizer: false,
reply_to_email: '',
notification_subject: '',
notification_body: ''
@@ -607,6 +622,7 @@ window.PageEvents = {
nostr_notifications: false,
promo_codes: [],
organizer_name: '',
+ copy_to_organizer: false,
reply_to_email: '',
notification_subject: '',
notification_body: ''
diff --git a/static/js/index.vue b/static/js/index.vue
index 19b2227..a8d99ab 100644
--- a/static/js/index.vue
+++ b/static/js/index.vue
@@ -804,6 +804,14 @@
label="Nostr notifications"
left-label
>
+