Building Mail Guard Around Nextcloud's Outgoing Mail Pipeline
An outgoing system email already has a sender, a subject, and a layout. I wanted to add a personal recognition marker without modifying every feature that sends mail.
Mail Guard attaches to Nextcloud's central outgoing-message event. Its scope is the messages that pass through that event, not every possible SMTP path on the server.
Keep the listener small
The listener coordinates several separate services: policy lookup, message introspection, local recipient resolution, language selection, code decision, and body mutation.
That decomposition makes an important distinction visible. Deciding which marker is appropriate is a policy problem. Inserting it safely into a message is a formatting problem. Neither needs to be mixed with the mail transport.
The listener checks whether the feature is enabled and whether it can inspect the message before attempting mutation. When the required interfaces are unavailable, skipping is more honest than claiming that the email was protected.
One body cannot contain a different private marker for everyone
Multi-recipient mail is the difficult case. A single shared message body cannot simultaneously be a separate personalized message for every recipient.
The current decision service gathers the unique configured codes. With one unique code, it can use that code. With multiple different codes, policy either skips mutation or uses an explicit multi-recipient fallback.
That fallback is not equivalent to personal recognition. It is a deliberate limitation of the current implementation.
Sending one new message per recipient would be a larger delivery redesign. It introduces questions about duplicate sends, Bcc privacy, transport errors, and interaction with other listeners. I did not want a body-decoration feature to silently take ownership of mail dispatch.
Mutate both message representations
A message may contain a plain-text body and an HTML body. Updating only HTML leaves text-only readers with a different result.
The mutation layer therefore handles both where supported. It also needs idempotency: processing the same body twice should not keep appending security lines.
HTML output requires escaping marker values and controlled placement. A user-defined marker is still input; inserting it directly as markup would give a recognition feature an unnecessary injection surface.
Preserve delivery, expose the limitation
The listener catches mutation failures so the feature does not intentionally abort transport. Logs record limited diagnostic information rather than message bodies or personal codes.
This availability choice has a consequence: receiving an unmodified message does not, by itself, establish why the marker is missing. The integration may have been bypassed, disabled, or unable to process the message.
A familiar marker is also not a cryptographic signature. Someone who learns it may reproduce it. Mail Guard adds a recognition cue; it does not replace transport authentication, signing, or cautious handling of suspicious content.
The most useful engineering result is the explicit policy boundary: the app can explain when it inserted a marker, when it skipped, and which cases its central integration cannot cover.