From 2f8edfb6bef3a65693dfe16476e42be161e6e7a7 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Sun, 22 Mar 2026 10:56:51 +0100 Subject: [PATCH] Add per-email sender name for compliance page emails When an email has a sender name set (the organization name), the mailer composes the From header as "OrgName via Probo" instead of the default global sender name. This gives compliance page recipients clearer context about which organization is contacting them. Signed-off-by: Bryan Frimin --- pkg/coredata/email.go | 14 ++++++++--- pkg/coredata/migrations/20260322T120000Z.sql | 1 + pkg/esign/completion_certificate_worker.go | 13 +++++++--- pkg/iam/auth_service.go | 9 ++++++- pkg/mailer/mailer.go | 7 +++++- pkg/mailman/service.go | 26 ++++++++++++++++++-- pkg/probo/document_service.go | 4 ++- pkg/probo/trust_center_access_service.go | 4 ++- pkg/trust/trust_center_access_service.go | 8 ++++-- 9 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 pkg/coredata/migrations/20260322T120000Z.sql diff --git a/pkg/coredata/email.go b/pkg/coredata/email.go index 2e8be2594..560fb7224 100644 --- a/pkg/coredata/email.go +++ b/pkg/coredata/email.go @@ -31,6 +31,7 @@ type ( ID gid.GID `db:"id"` RecipientEmail string `db:"recipient_email"` RecipientName string `db:"recipient_name"` + SenderName *string `db:"sender_name"` ReplyTo *mail.Addr `db:"reply_to"` UnsubscribeURL *string `db:"unsubscribe_url"` MailingListUpdateID *gid.GID `db:"mailing_list_update_id"` @@ -51,6 +52,7 @@ type ( Emails []*Email EmailOptions struct { + SenderName *string ReplyTo *mail.Addr UnsubscribeURL *string MailingListUpdateID *gid.GID @@ -91,6 +93,7 @@ func NewEmail( } if opts != nil { + e.SenderName = opts.SenderName e.ReplyTo = opts.ReplyTo e.UnsubscribeURL = opts.UnsubscribeURL e.MailingListUpdateID = opts.MailingListUpdateID @@ -108,7 +111,9 @@ INSERT INTO emails ( id, recipient_email, recipient_name, - reply_to, unsubscribe_url, + sender_name, + reply_to, + unsubscribe_url, mailing_list_update_id, subject, text_body, @@ -123,6 +128,7 @@ VALUES ( @id, @recipient_email, @recipient_name, + @sender_name, @reply_to, @unsubscribe_url, @mailing_list_update_id, @@ -141,6 +147,7 @@ VALUES ( "id": e.ID, "recipient_email": e.RecipientEmail, "recipient_name": e.RecipientName, + "sender_name": e.SenderName, "reply_to": e.ReplyTo, "unsubscribe_url": e.UnsubscribeURL, "mailing_list_update_id": e.MailingListUpdateID, @@ -172,6 +179,7 @@ func (emails Emails) BulkInsert( e.ID, e.RecipientEmail, e.RecipientName, + e.SenderName, e.ReplyTo, e.UnsubscribeURL, e.MailingListUpdateID, @@ -186,7 +194,7 @@ func (emails Emails) BulkInsert( _, err := conn.CopyFrom( ctx, pgx.Identifier{"emails"}, - []string{"id", "recipient_email", "recipient_name", "reply_to", "unsubscribe_url", "mailing_list_update_id", "subject", "text_body", "html_body", "created_at", "updated_at"}, + []string{"id", "recipient_email", "recipient_name", "sender_name", "reply_to", "unsubscribe_url", "mailing_list_update_id", "subject", "text_body", "html_body", "created_at", "updated_at"}, pgx.CopyFromRows(rows), ) return err @@ -198,7 +206,7 @@ func (e *Email) LoadNextPendingForUpdateSkipLocked( ) error { q := ` SELECT - id, recipient_email, recipient_name, reply_to, unsubscribe_url, mailing_list_update_id, subject, text_body, html_body, + id, recipient_email, recipient_name, sender_name, reply_to, unsubscribe_url, mailing_list_update_id, subject, text_body, html_body, status, processing_started_at, attempt_count, max_attempts, last_attempted_at, last_error, created_at, updated_at, sent_at FROM emails diff --git a/pkg/coredata/migrations/20260322T120000Z.sql b/pkg/coredata/migrations/20260322T120000Z.sql new file mode 100644 index 000000000..6cf9538f5 --- /dev/null +++ b/pkg/coredata/migrations/20260322T120000Z.sql @@ -0,0 +1 @@ +ALTER TABLE emails ADD COLUMN sender_name text; diff --git a/pkg/esign/completion_certificate_worker.go b/pkg/esign/completion_certificate_worker.go index ab6f76d5b..44322e595 100644 --- a/pkg/esign/completion_certificate_worker.go +++ b/pkg/esign/completion_certificate_worker.go @@ -238,8 +238,9 @@ func (w *CompletionCertificateWorker) generateCertificate( scope coredata.Scoper, ) (*coredata.Email, coredata.EmailAttachments, error) { var ( - events = coredata.ElectronicSignatureEvents{} - signedFile = coredata.File{} + events = coredata.ElectronicSignatureEvents{} + signedFile = coredata.File{} + organization = coredata.Organization{} ) if err := w.pg.WithConn( @@ -253,6 +254,10 @@ func (w *CompletionCertificateWorker) generateCertificate( return fmt.Errorf("cannot load signed file: %w", err) } + if err := organization.LoadByID(ctx, conn, scope, signature.OrganizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + return nil }, ); err != nil { @@ -322,7 +327,9 @@ func (w *CompletionCertificateWorker) generateCertificate( subject, textBody, htmlBody, - nil, + &coredata.EmailOptions{ + SenderName: new(organization.Name), + }, ) attachments := coredata.EmailAttachments{ diff --git a/pkg/iam/auth_service.go b/pkg/iam/auth_service.go index 1c0cf5e35..7152d2bc7 100644 --- a/pkg/iam/auth_service.go +++ b/pkg/iam/auth_service.go @@ -583,13 +583,20 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques return fmt.Errorf("cannot render magic link email: %w", err) } + var emailOpts *coredata.EmailOptions + if req.CompliancePageID != nil { + emailOpts = &coredata.EmailOptions{ + SenderName: new(organization.Name), + } + } + magicLinkEmail := coredata.NewEmail( fullName, req.Email, subject, textBody, htmlBody, - nil, + emailOpts, ) if err := magicLinkEmail.Insert(ctx, tx); err != nil { diff --git a/pkg/mailer/mailer.go b/pkg/mailer/mailer.go index ad08a3587..53fd9b33d 100644 --- a/pkg/mailer/mailer.go +++ b/pkg/mailer/mailer.go @@ -202,9 +202,14 @@ func (w *SendingWorker) sendAndCommit( return fmt.Errorf("cannot load email attachments: %w", err) } + fromName := w.senderName + if email.SenderName != nil { + fromName = *email.SenderName + " via " + w.senderName + } + mail := enmime.Builder(). Subject(email.Subject). - From(w.senderName, w.senderEmail). + From(fromName, w.senderEmail). To(email.RecipientName, email.RecipientEmail). Text([]byte(email.TextBody)) diff --git a/pkg/mailman/service.go b/pkg/mailman/service.go index 6d358a89b..641cff934 100644 --- a/pkg/mailman/service.go +++ b/pkg/mailman/service.go @@ -708,6 +708,7 @@ func (s *Service) CreateUpdateEmails( textBody, htmlBody, &coredata.EmailOptions{ + SenderName: new(orgName), ReplyTo: replyTo, UnsubscribeURL: &unsubscribeURL, MailingListUpdateID: &mailingListUpdateID, @@ -752,7 +753,18 @@ func (s *Service) buildConfirmationMail( return nil, fmt.Errorf("cannot render subscription confirmation email: %w", err) } - return coredata.NewEmail(fullName, email, subject, textBody, htmlBody, &coredata.EmailOptions{ReplyTo: replyTo, UnsubscribeURL: &unsubscribeURL}), nil + return coredata.NewEmail( + fullName, + email, + subject, + textBody, + htmlBody, + &coredata.EmailOptions{ + SenderName: new(orgName), + ReplyTo: replyTo, + UnsubscribeURL: &unsubscribeURL, + }, + ), nil } func (s *Service) buildUnsubscriptionMail( @@ -772,7 +784,17 @@ func (s *Service) buildUnsubscriptionMail( return nil, fmt.Errorf("cannot render unsubscription email: %w", err) } - return coredata.NewEmail(fullName, email, subject, textBody, htmlBody, &coredata.EmailOptions{ReplyTo: replyTo}), nil + return coredata.NewEmail( + fullName, + email, + subject, + textBody, + htmlBody, + &coredata.EmailOptions{ + SenderName: new(orgName), + ReplyTo: replyTo, + }, + ), nil } func (s *Service) buildUnsubscribeURL(mailingListID gid.GID, email mail.Addr) (string, error) { diff --git a/pkg/probo/document_service.go b/pkg/probo/document_service.go index c6c1d663b..10df4658a 100644 --- a/pkg/probo/document_service.go +++ b/pkg/probo/document_service.go @@ -693,7 +693,9 @@ func (s *DocumentService) SendSigningNotifications( subject, textBody, htmlBody, - nil, + &coredata.EmailOptions{ + SenderName: new(organization.Name), + }, ) if err := email.Insert(ctx, tx); err != nil { diff --git a/pkg/probo/trust_center_access_service.go b/pkg/probo/trust_center_access_service.go index f779985b2..1ed7ba4c6 100644 --- a/pkg/probo/trust_center_access_service.go +++ b/pkg/probo/trust_center_access_service.go @@ -366,7 +366,9 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Con subject, textBody, htmlBody, - nil, + &coredata.EmailOptions{ + SenderName: new(organization.Name), + }, ) if err := accessEmail.Insert(ctx, tx); err != nil { diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index 7ed38f284..3e573f89e 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -450,7 +450,9 @@ func (s *TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Co subject, textBody, htmlBody, - nil, + &coredata.EmailOptions{ + SenderName: new(organization.Name), + }, ) if err := accessEmail.Insert(ctx, tx); err != nil { @@ -585,7 +587,9 @@ func (s *TrustCenterAccessService) sendDocumentAccessRejectedEmail( subject, textBody, htmlBody, - nil, + &coredata.EmailOptions{ + SenderName: new(organization.Name), + }, ) if err := accessEmail.Insert(ctx, tx); err != nil {