logging-log4j2 icon indicating copy to clipboard operation
logging-log4j2 copied to clipboard

Add Support for Custom Headers in SMTP Appender

Open ppkarwasz opened this issue 7 months ago • 11 comments

Currently, the SMTP Appender does not support adding custom headers to outgoing email messages. In contrast, the HTTP Appender allows users to define custom headers using <Property> elements. Introducing similar support for the SMTP Appender would enhance feature parity across appenders and improve user experience and flexibility.

Proposed Solution

Both the HTTP and SMTP Appenders extend AbstractFilterable and already support <Property> configuration elements. In the HTTP Appender, these <Property> elements are interpreted as HTTP headers. Similarly, the SMTP Appender could treat <Property> entries as custom email headers.

Example:

<Appenders>
  <SMTP name="Email" ...>
    <Property name="X-Custom-ID" value="12345"/>
    <Property name="Reply-To" value="[email protected]"/>
  </SMTP>
</Appenders>

This change would preserve existing behavior while enabling greater customization for advanced email use cases.

ppkarwasz avatar Jun 01 '25 06:06 ppkarwasz

Hello @ppkarwasz ,

I'd like to contribute to this enhancement.

Is this issue currently open for contribution? If so, could you please guide me on where to start in the codebase (such as which classes or modules are involved)? Also, are there any recommended steps for setting up the project locally, including running and debugging it? I'm particularly interested in any tips for working with a large codebase like this, as it's my first time contributing to a project of this scale.

Thank you.

sidhantmourya avatar Jul 13 '25 18:07 sidhantmourya

Hi @sidhantmourya,

Thanks for volunteering! We really appreciate contributions — all issues are open for community help, especially since we receive more requests than we can handle on our own.

For this specific issue, here are a few pointers to get you started:

  • In Log4j Core, each appender is typically split into two components: the Appender (which mostly handles configuration) and an associated AbstractManager.
  • The HttpAppender, for example, accepts additional headers via a Property[] array.
  • These headers are processed in HttpURLConnectionManager, where they are applied to each request.
  • A similar approach is needed for SmtpAppender, particularly in its interaction with MailManager.
  • One caveat: MailManager is an abstract class with two concrete implementations — one targeting Java EE and another for Jakarta EE (located in the log4j-jakarta-smtp module).

For help with building and debugging the codebase, please see our BUILDING.md guide.

Let me know if you run into any questions!

ppkarwasz avatar Jul 13 '25 18:07 ppkarwasz

@ppkarwasz I'm adding custom header support to the SMTP appender similar to the HTTP appender, but need guidance on handling the javax.mail vs jakarta.mail MimeMessage types - what's the recommended way to implement applyHeaders() to work with both implementations? Should I use instanceof checks, create separate methods, or is there a better pattern?

my solution is something like this in the Abstract Class curently.

protected void applyHeaders(final Object message, final StrSubstitutor substitutor)
            throws MessagingException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method setHeader = message.getClass().getMethod(
                "setHeader",
                String.class,
                String.class
        );
        for (Property header : headers)
        {
            setHeader.invoke(message, header.getName(), substitutor.replace(header.getValue()));

        }
    }

And then handle the exceptions in the implementation classes.

sidhantmourya avatar Jul 16 '25 15:07 sidhantmourya

Hi @sidhantmourya,

Thanks for digging into this!

I must admit that when I introduced the Jakarta version of the SMTP appender, I only added a minimal abstraction for MailManager. In hindsight, I could have refactored more shared logic into the abstract base class. One reason I held back was that SmtpManager exposes Java EE Mail types in its public and protected methods, which we can’t remove without breaking backward compatibility.

That said, if you'd like to take on abstracting MimeMessage, feel free — it would be a useful improvement. Alternatively, you're also welcome to duplicate the logic across both MailManager implementations. The Java EE version will be removed in Log4j 3.x anyway, so code duplication is a short-term compromise.

A few quick notes on the code you shared:

setHeader.invoke(message, header.getName(), substitutor.replace(header.getValue()));

When calling MimeMessage.setHeader(), make sure the value is correctly encoded and safe. Specifically:

  1. The value must be ASCII-only, per RFC 5322, section 2.2.
  2. It must not contain newlines, to avoid header injection vulnerabilities.
  3. It must respect the 998-character line length limit, per RFC 5322, section 2.1.1. For example, I’ve seen overly long headers cause message rejection on a Debian server running Exim with default settings.

To handle these constraints, you can use the encode and fold methods from MimeUtility.

Finally, although HttpAppender only supports property substitution, I’d personally prefer we allow full PatternLayout expressions here — just like the "Subject" field is implemented in SmtpManager.

ppkarwasz avatar Jul 16 '25 19:07 ppkarwasz

Hi @ppkarwasz, After reading through the documentation, I've updated the header handling to properly encode and fold values per RFC 5322 requirements. The current implementation uses MimeUtility to ensure ASCII-only content with proper line length limits.

for (Property header : headers)
        {
            String value = MimeUtility.fold(78, MimeUtility.encodeText(substitutor.replace(header.getValue())));
            setHeader.invoke(message, header.getName(), value);

        }

I'm now looking at how to implement PatternLayout support for headers similar to how SubjectSerializer works.

PatternLayout.java
final Serializer subjectSerializer = PatternLayout.newSerializerBuilder()
                    .setConfiguration(getConfiguration())
                    .setPattern(subject)
                    .build();

The issue is that while there's only one subject per email, there can be multiple headers each with their own potential patterns. Looking at the SMTPAppender test case in , I see subjects can contain patterns like "%X{key}", but I'm unsure how to properly extend this to multiple headers.

@Test
    void testDelivery() {
        final String subjectKey = getClass().getName();
        final String subjectValue = "SubjectValue1";
        ThreadContext.put(subjectKey, subjectValue);
        final int smtpPort = AvailablePortFinder.getNextAvailable();
        final SmtpAppender appender = SmtpAppender.newBuilder()
                .setName("Test")
                .setTo("[email protected]")
                .setCc("[email protected]")
                .setBcc("[email protected]")
                .setFrom("[email protected]")
                .setReplyTo("[email protected]")
                .setSubject("Subject Pattern %X{" + subjectKey + "} %maxLen{%m}{10}")
                .setSmtpHost(HOST)
                .setSmtpPort(smtpPort)
                .setBufferSize(3)
                .build();
        assertNotNull(appender);
        assertInstanceOf(SmtpManager.class, appender.getManager());
        appender.start();
...
}

I went through the documentation and found this https://logging.apache.org/log4j/2.x/manual/pattern-layout.html#plugin-attr-header. Are there any existing examples in the codebase of similar multi-pattern handling that I could follow?

sidhantmourya avatar Jul 17 '25 16:07 sidhantmourya

Hi @sidhantmourya,

I'm now looking at how to implement PatternLayout support for headers similar to how SubjectSerializer works.
https://github.com/apache/logging-log4j2/blob/8ec5703670fc24d8883db39df8be244c34c8e0bd/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/SmtpAppender.java#L283-L286
The issue is that while there's only one subject per email, there can be multiple headers each with their own potential patterns.

You could approach it like this:

String[] headerNames = headers.stream()
    .map(Property::getName)
    .toArray(String[]::new);

Serializer[] headerSerializers = headers.stream()
    .map(header -> PatternLayout.newSerializerBuilder()
        .setConfiguration(getConfiguration())
        .setPattern(header.getValue())
        .build())
    .toArray(Serializer[]::new);

This would give you a pair of arrays — one for names and one for serializers — that you can zip together at runtime when generating headers. Of course you can zip them in any other way you like, I wouldn't just use Map<String, Serializer>, since it prevents users from configuring repeated headers.

To expose a new headers property in the builder, you just need to annotate it like this:

@PluginElement("Headers")
private List<Property> headers = new ArrayList<>();

Note: the argument to @PluginElement is ignored, but it's required for compatibility reasons.

You might also want to add a setter method and an addHeader(Property header) method to simplify usage in tests and manual configurations.

ppkarwasz avatar Jul 17 '25 17:07 ppkarwasz

hi @ppkarwasz - sorry for the delay, I was out of town. I tried implementing PatternLayout support for headers similar to how SubjectSerializer works, but i couldn't understand properly. So i would like to review the code and better understand the requirement. The original requirement that is to Add Support for Custom Headers in SMTP Appender is working and i had test it through test class, although while trying to run mvn clean install command i started facing issues, the log4jcore build is failing with this error.

[ERROR] Failed to execute goal biz.aQute.bnd:bnd-baseline-maven-plugin:7.1.0:baseline (check-api-compat) on project log4j-core: An error occurred while calculating the baseline: Baseline problems detected. See the report in D:\Codes\logging-log4j2\log4j-core\target\baseline\log4j-core-2.26.0-SNAPSHOT.txt.

I wanted to commit my changes for a review.

sidhantmourya avatar Aug 04 '25 17:08 sidhantmourya

The build failure you're seeing is caused by the BND Baseline Plugin, which we use to explicitly opt in to any changes in the public API. This helps us catch unintended API breaks.

In this case, the API changes are expected as part of your implementation. To resolve the failure, you'll need to increment the version of the affected package(s) according to our guidelines for fixing API compatibility check failures.

Let me know if you need help identifying the affected packages or version updates.

ppkarwasz avatar Aug 04 '25 19:08 ppkarwasz

@sidhantmourya Can we get a PR started for this? It's perfectly fine to label it [WIP] and have the code publicly available for inspection. It might make it easier to discuss in this issue. It also means I can try the code, etc. that you are working on.

ChristopherSchultz avatar Sep 22 '25 12:09 ChristopherSchultz

hi @ChristopherSchultz, I've made the changes and would like to get the PR started although I am not sure how good of a design it is. As i mentioned earlier, i wanted to commit my changes for review but was unable to do so.

sidhantmourya avatar Sep 26 '25 16:09 sidhantmourya

Hi @sidhantmourya,

Don't worry about the design: we don't have a uniform design in Log4j. You can submit a draft PR and I will guide on what is missing.

ppkarwasz avatar Sep 26 '25 17:09 ppkarwasz