Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.net.MimeMessageBuilder;
import org.apache.logging.log4j.core.net.SmtpManager;
import org.apache.logging.log4j.core.test.AvailablePortFinder;
Expand Down Expand Up @@ -177,4 +178,56 @@ void testDelivery() {
assertFalse(body2.contains("Error with exception"));
assertTrue(body2.contains("Error message #2"));
}

@Test
void testCustomHeaders() {
final String traceKey = getClass().getName() + ".traceId";
final String traceValue = "TraceValue1";
ThreadContext.put(traceKey, traceValue);
final int smtpPort = AvailablePortFinder.getNextAvailable();
final SmtpAppender appender = SmtpAppender.newBuilder()
.setName("TestHeaders")
.setTo("headers-to@example.com")
.setFrom("headers-from@example.com")
.setSubject("Headers Subject")
.setSmtpHost(HOST)
.setSmtpPort(smtpPort)
.setBufferSize(3)
.addHeader(Property.createProperty("X-Static", "fixed-value"))
.addHeader(Property.createProperty("X-Trace-Id", "%X{" + traceKey + "}"))
.addHeader(Property.createProperty("X-Tag", "first"))
.addHeader(Property.createProperty("X-Tag", "second"))
.addHeader(Property.createProperty("X-Message", "%m"))
.addHeader(Property.createProperty("X:Invalid", "ignored"))
.build();
assertNotNull(appender);
assertInstanceOf(SmtpManager.class, appender.getManager());
appender.start();

final LoggerContext context = LoggerContext.getContext();
final Logger root = context.getLogger("SMTPAppenderHeadersTest");
root.addAppender(appender);
root.setAdditive(false);
root.setLevel(Level.DEBUG);

final SimpleSmtpServer server = SimpleSmtpServer.start(smtpPort);
try {
root.error("safe\r\nX-Evil: injected");
} finally {
server.stop();
root.removeAppender(appender);
appender.stop();
ThreadContext.remove(traceKey);
}

assertEquals(1, server.getReceivedEmailSize());
final SmtpMessage email = server.getReceivedEmail().next();

assertEquals("fixed-value", email.getHeaderValue("X-Static"));
assertEquals(traceValue, email.getHeaderValue("X-Trace-Id"));
assertArrayEquals(new String[] {"first", "second"}, email.getHeaderValues("X-Tag"));
assertEquals("safe X-Evil: injected", email.getHeaderValue("X-Message"));
assertEquals(0, email.getHeaderValues("X-Evil").length);
assertEquals(0, email.getHeaderValues("X").length);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Arrays;
import javax.mail.MessagingException;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.SmtpAppender;
import org.apache.logging.log4j.core.async.RingBufferLogEvent;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.apache.logging.log4j.core.impl.MutableLogEvent;
import org.apache.logging.log4j.core.util.ClockFactory;
Expand Down Expand Up @@ -50,6 +53,62 @@ void testCreateManagerName() {
assertEquals("SMTP:to:cc::from::LOG4J2-3107:proto:smtp.log4j.com:4711:username::filter", managerName);
}

@Test
void testCreateManagerNameDistinguishesHeaders() {
assertThat(managerNameWithHeaders(Property.createProperty("X-Tag", "a")))
.isNotEqualTo(managerNameWithHeaders(Property.createProperty("X-Tag", "b")));
}

private static String managerNameWithHeaders(final Property... headers) {
return SmtpManager.createManagerName(
"to",
"cc",
null,
"from",
null,
"LOG4J2-3107",
"proto",
"smtp.log4j.com",
4711,
"username",
false,
"filter",
headers);
}

@Test
void testEncodeHeaderValueLeavesPlainAsciiAlone() throws MessagingException {
assertEquals("plain value", SmtpManager.encodeHeaderValue("X-Test", "plain value"));
}

@Test
void testEncodeHeaderValueNeutralizesControlCharacters() throws MessagingException {
assertEquals("safe X-Evil: injected", SmtpManager.encodeHeaderValue("X-Test", "safe\r\nX-Evil: injected"));
}

@Test
void testEncodeHeaderValueIsAsciiOnly() throws MessagingException {
final String encoded = SmtpManager.encodeHeaderValue("X-Test", "Jos\u00e9 \u20b9500");
assertThat(encoded.chars().allMatch(c -> c < 128)).isTrue();
}

@Test
void testEncodeHeaderValueRespectsLineLengthLimit() throws MessagingException {
final char[] chars = new char[5_000];
Arrays.fill(chars, 'x');
assertLineLengthLimit(SmtpManager.encodeHeaderValue("X-Test", new String(chars)));
Arrays.fill(chars, '\u00e9');
assertLineLengthLimit(SmtpManager.encodeHeaderValue("X-Test", new String(chars)));
}

private static void assertLineLengthLimit(final String encoded) {
int used = "X-Test".length() + 2;
for (final String line : encoded.split("\r\n", -1)) {
assertThat(used + line.length()).isLessThanOrEqualTo(998);
used = 0;
}
}

private void testAdd(final LogEvent event) {
final SmtpAppender appender = SmtpAppender.newBuilder()
.setName("smtp")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package org.apache.logging.log4j.core.appender;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ServiceLoader;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Core;
Expand Down Expand Up @@ -138,6 +141,9 @@ public static class Builder extends AbstractAppender.Builder<Builder>
@PluginElement("SSL")
private SslConfiguration sslConfiguration;

@PluginElement("Headers")
private Property[] headers;

/**
* Comma-separated list of recipient email addresses.
*/
Expand Down Expand Up @@ -251,6 +257,33 @@ public Builder setSslConfiguration(final SslConfiguration sslConfiguration) {
return this;
}

/**
* Specifies custom headers to add to every message. Header values are {@link PatternLayout} patterns,
* evaluated against the event that triggers the message.
*
* @since 2.27.0
*/
public Builder setHeaders(final Property[] headers) {
this.headers = headers;
return this;
}

/**
* Adds a single custom header. The header value is a {@link PatternLayout} pattern, evaluated against the
* event that triggers the message.
*
* @since 2.27.0
*/
public Builder addHeader(final Property header) {
if (header != null) {
final Property[] oldHeaders = headers != null ? headers : Property.EMPTY_ARRAY;
final Property[] newHeaders = Arrays.copyOf(oldHeaders, oldHeaders.length + 1);
newHeaders[oldHeaders.length] = header;
headers = newHeaders;
}
return this;
}

/**
* Specifies the layout used for the email message body. By default, this uses the
* {@linkplain HtmlLayout#createDefaultLayout() default HTML layout}.
Expand Down Expand Up @@ -284,6 +317,14 @@ public SmtpAppender build() {
.setConfiguration(getConfiguration())
.setPattern(subject)
.build();
final Property[] headerArray = filterValidHeaders(headers);
final Serializer[] headerSerializers = new Serializer[headerArray.length];
for (int i = 0; i < headerArray.length; i++) {
headerSerializers[i] = PatternLayout.newSerializerBuilder()
.setConfiguration(getConfiguration())
.setPattern(headerArray[i].getValue())
.build();
}
final FactoryData data = new FactoryData(
to,
cc,
Expand All @@ -300,7 +341,9 @@ public SmtpAppender build() {
smtpDebug,
bufferSize,
sslConfiguration,
getFilter().toString());
getFilter().toString(),
headerArray,
headerSerializers);
final MailManagerFactory factory = ServiceLoaderUtil.safeStream(
MailManagerFactory.class,
ServiceLoader.load(
Expand All @@ -317,6 +360,37 @@ MailManagerFactory.class, getClass().getClassLoader()),
return new SmtpAppender(
getName(), getFilter(), getLayout(), smtpManager, isIgnoreExceptions(), getPropertyArray());
}

private Property[] filterValidHeaders(final Property[] headers) {
if (headers == null || headers.length == 0) {
return Property.EMPTY_ARRAY;
}
final List<Property> validHeaders = new ArrayList<>(headers.length);
for (final Property header : headers) {
if (isValidHeaderName(header.getName())) {
validHeaders.add(header);
} else {
LOGGER.error(
"SmtpAppender '{}' ignores the header with the invalid name '{}'.",
getName(),
header.getName());
}
}
return validHeaders.toArray(Property.EMPTY_ARRAY);
}

private static boolean isValidHeaderName(final String name) {
if (Strings.isEmpty(name)) {
return false;
}
for (int i = 0; i < name.length(); i++) {
final char c = name.charAt(i);
if (c < '!' || c > '~' || c == ':') {
return false;
}
}
return true;
}
}

/**
Expand Down
Loading
Loading