2626import java .util .ArrayList ;
2727import java .util .Arrays ;
2828import java .util .HashSet ;
29+ import java .util .Iterator ;
2930import java .util .List ;
31+ import java .util .Map ;
3032import java .util .Optional ;
3133import java .util .Set ;
3234import java .util .function .Predicate ;
@@ -56,9 +58,72 @@ public class EmailService extends NotificationService<Account> {
5658 (key ) -> Setting .simpleString (key , Property .Dynamic , Property .NodeScope )
5759 );
5860
61+ private static final List <String > ALLOW_ALL_DEFAULT = List .of ("*" );
62+
5963 private static final Setting <List <String >> SETTING_DOMAIN_ALLOWLIST = Setting .stringListSetting (
6064 "xpack.notification.email.account.domain_allowlist" ,
61- List .of ("*" ),
65+ ALLOW_ALL_DEFAULT ,
66+ new Setting .Validator <>() {
67+ @ Override
68+ public void validate (List <String > value ) {
69+ // Ignored
70+ }
71+
72+ @ Override
73+ @ SuppressWarnings ("unchecked" )
74+ public void validate (List <String > value , Map <Setting <?>, Object > settings ) {
75+ List <String > recipientAllowPatterns = (List <String >) settings .get (SETTING_RECIPIENT_ALLOW_PATTERNS );
76+ if (value .equals (ALLOW_ALL_DEFAULT ) == false && recipientAllowPatterns .equals (ALLOW_ALL_DEFAULT ) == false ) {
77+ throw new IllegalArgumentException (
78+ "Cannot set both ["
79+ + SETTING_RECIPIENT_ALLOW_PATTERNS .getKey ()
80+ + "] and ["
81+ + SETTING_DOMAIN_ALLOWLIST .getKey ()
82+ + "] to a non [\" *\" ] value at the same time."
83+ );
84+ }
85+ }
86+
87+ @ Override
88+ public Iterator <Setting <?>> settings () {
89+ List <Setting <?>> settingRecipientAllowPatterns = List .of (SETTING_RECIPIENT_ALLOW_PATTERNS );
90+ return settingRecipientAllowPatterns .iterator ();
91+ }
92+ },
93+ Property .Dynamic ,
94+ Property .NodeScope
95+ );
96+
97+ private static final Setting <List <String >> SETTING_RECIPIENT_ALLOW_PATTERNS = Setting .stringListSetting (
98+ "xpack.notification.email.recipient_allowlist" ,
99+ ALLOW_ALL_DEFAULT ,
100+ new Setting .Validator <>() {
101+ @ Override
102+ public void validate (List <String > value ) {
103+ // Ignored
104+ }
105+
106+ @ Override
107+ @ SuppressWarnings ("unchecked" )
108+ public void validate (List <String > value , Map <Setting <?>, Object > settings ) {
109+ List <String > domainAllowList = (List <String >) settings .get (SETTING_DOMAIN_ALLOWLIST );
110+ if (value .equals (ALLOW_ALL_DEFAULT ) == false && domainAllowList .equals (ALLOW_ALL_DEFAULT ) == false ) {
111+ throw new IllegalArgumentException (
112+ "Connect set both ["
113+ + SETTING_RECIPIENT_ALLOW_PATTERNS .getKey ()
114+ + "] and ["
115+ + SETTING_DOMAIN_ALLOWLIST .getKey ()
116+ + "] to a non [\" *\" ] value at the same time."
117+ );
118+ }
119+ }
120+
121+ @ Override
122+ public Iterator <Setting <?>> settings () {
123+ List <Setting <?>> settingDomainAllowlist = List .of (SETTING_DOMAIN_ALLOWLIST );
124+ return settingDomainAllowlist .iterator ();
125+ }
126+ },
62127 Property .Dynamic ,
63128 Property .NodeScope
64129 );
@@ -167,6 +232,7 @@ public class EmailService extends NotificationService<Account> {
167232 private final CryptoService cryptoService ;
168233 private final SSLService sslService ;
169234 private volatile Set <String > allowedDomains ;
235+ private volatile Set <String > allowedRecipientPatterns ;
170236
171237 @ SuppressWarnings ("this-escape" )
172238 public EmailService (Settings settings , @ Nullable CryptoService cryptoService , SSLService sslService , ClusterSettings clusterSettings ) {
@@ -192,7 +258,9 @@ public EmailService(Settings settings, @Nullable CryptoService cryptoService, SS
192258 clusterSettings .addAffixUpdateConsumer (SETTING_SMTP_SEND_PARTIAL , (s , o ) -> {}, (s , o ) -> {});
193259 clusterSettings .addAffixUpdateConsumer (SETTING_SMTP_WAIT_ON_QUIT , (s , o ) -> {}, (s , o ) -> {});
194260 this .allowedDomains = new HashSet <>(SETTING_DOMAIN_ALLOWLIST .get (settings ));
261+ this .allowedRecipientPatterns = new HashSet <>(SETTING_RECIPIENT_ALLOW_PATTERNS .get (settings ));
195262 clusterSettings .addSettingsUpdateConsumer (SETTING_DOMAIN_ALLOWLIST , this ::updateAllowedDomains );
263+ clusterSettings .addSettingsUpdateConsumer (SETTING_RECIPIENT_ALLOW_PATTERNS , this ::updateAllowedRecipientPatterns );
196264 // do an initial load
197265 reload (settings );
198266 }
@@ -201,6 +269,10 @@ void updateAllowedDomains(List<String> newDomains) {
201269 this .allowedDomains = new HashSet <>(newDomains );
202270 }
203271
272+ void updateAllowedRecipientPatterns (List <String > newPatterns ) {
273+ this .allowedRecipientPatterns = new HashSet <>(newPatterns );
274+ }
275+
204276 @ Override
205277 protected Account createAccount (String name , Settings accountSettings ) {
206278 Account .Config config = new Account .Config (name , accountSettings , getSmtpSslSocketFactory (), logger );
@@ -228,46 +300,77 @@ public EmailSent send(Email email, Authentication auth, Profile profile, String
228300 "failed to send email with subject ["
229301 + email .subject ()
230302 + "] and recipient domains "
231- + getRecipientDomains (email )
303+ + getRecipients (email , true )
232304 + ", one or more recipients is not specified in the domain allow list setting ["
233305 + SETTING_DOMAIN_ALLOWLIST .getKey ()
234306 + "]."
235307 );
236308 }
309+ if (recipientAddressInAllowList (email , this .allowedRecipientPatterns ) == false ) {
310+ throw new IllegalArgumentException (
311+ "failed to send email with subject ["
312+ + email .subject ()
313+ + "] and recipients "
314+ + getRecipients (email , false )
315+ + ", one or more recipients is not specified in the domain allow list setting ["
316+ + SETTING_RECIPIENT_ALLOW_PATTERNS .getKey ()
317+ + "]."
318+ );
319+ }
237320 return send (email , auth , profile , account );
238321 }
239322
240323 // Visible for testing
241- static Set <String > getRecipientDomains (Email email ) {
242- return Stream .concat (
324+ static Set <String > getRecipients (Email email , boolean domainsOnly ) {
325+ var stream = Stream .concat (
243326 Optional .ofNullable (email .to ()).map (addrs -> Arrays .stream (addrs .toArray ())).orElse (Stream .empty ()),
244327 Stream .concat (
245328 Optional .ofNullable (email .cc ()).map (addrs -> Arrays .stream (addrs .toArray ())).orElse (Stream .empty ()),
246329 Optional .ofNullable (email .bcc ()).map (addrs -> Arrays .stream (addrs .toArray ())).orElse (Stream .empty ())
247330 )
248- )
249- .map (InternetAddress ::getAddress )
250- // Pull out only the domain of the email address, so [email protected] -> bar.com 251- .map (emailAddress -> emailAddress .substring (emailAddress .lastIndexOf ('@' ) + 1 ))
252- .collect (Collectors .toSet ());
331+ ).map (InternetAddress ::getAddress );
332+
333+ if (domainsOnly ) {
334+ // Pull out only the domain of the email address, so [email protected] becomes bar.com 335+ stream = stream .map (emailAddress -> emailAddress .substring (emailAddress .lastIndexOf ('@' ) + 1 ));
336+ }
337+
338+ return stream .collect (Collectors .toSet ());
253339 }
254340
255341 // Visible for testing
256342 static boolean recipientDomainsInAllowList (Email email , Set <String > allowedDomainSet ) {
257- if (allowedDomainSet .size () == 0 ) {
343+ if (allowedDomainSet .isEmpty () ) {
258344 // Nothing is allowed
259345 return false ;
260346 }
261347 if (allowedDomainSet .contains ("*" )) {
262348 // Don't bother checking, because there is a wildcard all
263349 return true ;
264350 }
265- final Set <String > domains = getRecipientDomains (email );
351+ final Set <String > domains = getRecipients (email , true );
266352 final Predicate <String > matchesAnyAllowedDomain = domain -> allowedDomainSet .stream ()
267353 .anyMatch (allowedDomain -> Regex .simpleMatch (allowedDomain , domain , true ));
268354 return domains .stream ().allMatch (matchesAnyAllowedDomain );
269355 }
270356
357+ // Visible for testing
358+ static boolean recipientAddressInAllowList (Email email , Set <String > allowedRecipientPatterns ) {
359+ if (allowedRecipientPatterns .isEmpty ()) {
360+ // Nothing is allowed
361+ return false ;
362+ }
363+ if (allowedRecipientPatterns .contains ("*" )) {
364+ // Don't bother checking, because there is a wildcard all
365+ return true ;
366+ }
367+
368+ final Set <String > recipients = getRecipients (email , false );
369+ final Predicate <String > matchesAnyAllowedRecipient = recipient -> allowedRecipientPatterns .stream ()
370+ .anyMatch (pattern -> Regex .simpleMatch (pattern , recipient , true ));
371+ return recipients .stream ().allMatch (matchesAnyAllowedRecipient );
372+ }
373+
271374 private static EmailSent send (Email email , Authentication auth , Profile profile , Account account ) throws MessagingException {
272375 assert account != null ;
273376 try {
@@ -304,6 +407,7 @@ private static List<Setting<?>> getDynamicSettings() {
304407 return Arrays .asList (
305408 SETTING_DEFAULT_ACCOUNT ,
306409 SETTING_DOMAIN_ALLOWLIST ,
410+ SETTING_RECIPIENT_ALLOW_PATTERNS ,
307411 SETTING_PROFILE ,
308412 SETTING_EMAIL_DEFAULTS ,
309413 SETTING_SMTP_AUTH ,
0 commit comments