So, we came up with two possible approaches to solve this issue:
1. Make CyclicBuffer smarter, replacing .removeAll() with .removeTo(T item). This would allow SMTPManager to only remove events up to the triggering event, leaving anything appended after in the buffer. However, this has a couple issues:
a. Empty or nonsensical emails (not containing the target event) could still occur, if the CyclicBuffer is too small, and more events are written to the buffer between the .isFiltered call and the .append call. This is actually an issue with the current implementation as well.
b. Log4jLogEvent would need to (at a minimum) implement a .equals method, so we can actually find the target event in the CyclicBuffer. I'm not sure if .equals was omitted purposefully.
b. The .removeTo code would be a bit more complex than the current .removeAll implementation.
2. Don't append non-filtered items in the .isFiltered method; instead, wait and append them to the buffer just before the current .removeAll call. This would ensure that at least the triggering event is reported. However, this also has an issue: Currently, CyclicBuffer claims to be thread-safe, as all mutating methods are synchronized. As .add would be called just before .removeAll, the calling code in SMTPManager would need to instead hold a synchronization lock (similar to my previous bad patch for this issue). Alternatively, .removeAll() could become an atomic .removeAllAndInclude(T item), preserving the thread-safety of CyclicBuffer, at the cost of some reusability of the class (the method doesn't seem to make sense for general use cases).
I've done a proof of concept with #2, which only required changing a few lines in a couple files. I did not yet make an atomic .removeAllAndInclude, but could if this alternative is desired.