bbottema / simple-java-mail

Simple API, Complex Emails (Jakarta Mail smtp wrapper)
http://www.simplejavamail.org
Apache License 2.0
1.22k stars 266 forks source link

[Feature Request] Per-connection limit on the number of emails #548

Open mdesharnais opened 3 days ago

mdesharnais commented 3 days ago

Hi, I have a use case where I need to send thousand of emails and the SMTP provider has a rate limit of 5000 emails per SMTP connection.

Rate limits

When using SMTP to send emails through SendGrid, it's essential to be aware of the following rate limits:

  • You may send up to 5k messages per SMTP connection .
  • You may open up to 10k concurrent connections from a single server .

I initially assumed that the Mailer interface represented an SMTP connection and manually handled the limit in the following way.

final Iterator<String> iterator = ...
while (iterator.hasNext()) {
  try (Mailer mailer = ...) { // I was hoping to open an SMTP connection here
    for (int i = 0; i < MAX_EMAILS_PER_CONNECTION && iterator.hasNext(); i += 1) {
      final String email = iterator.next();
      ...
    }
  } // I was hoping to close the SMTP connection here
}

After some debugging, I realized that the Mailer interfaces opens a new SMTP connection for every message. My understanding is that the batch module does not support any form of per-SMTP-connection hard limit and, thus, cannot solve my problem.

Note that adding such feature to the batch module is one possible solution but not the only one. Another solution could be to add some abstraction for an SMTP connection, e.g., an SMTPConnection interface that implements the Closable interface, and let users such as myself manually handle the limits we need.

The need for a per-connection hard limit was raised in issue #348 in October 2021, but the issue was closed after the poster found out they were hitting some other, unrelated limit.

bbottema commented 1 day ago

This should be achievable with your example with the batch-module, as long as your have a core thread configured: withConnectionPoolCoreSize

# keep a single Transport connection alive for at least 60 minutes if not in use, and force all emails through it
simplejavamail.defaults.connectionpool.coresize=1 
simplejavamail.defaults.connectionpool.maxsize=1
simplejavamail.defaults.connectionpool.claimtimeout.millis=60000 (defaults to forever)
simplejavamail.defaults.connectionpool.expireafter.millis=60000 (defaults to 5000)

Also see here: https://www.simplejavamail.org/configuration.html#section-batch-and-clustering.

After that, you need to manually call shutdown on the internal 'cluster', wait for it and create a new Mailer instance.

Future<?> shutdown = mailer.shutdownConnectionPool();
shutdown.get();

mailer = MailerBuilder.build();
mdesharnais commented 1 day ago

Thanks for your response.

Do I understand correctly, that you mean I should do something like that?

Mailer mailer = ... // previous Mailer
final Iterator<String> iterator = ...
while (iterator.hasNext()) {
  // Force the shutdown of the connection pool and, thus, of the one and only SMTP connection.
  // This is in case messages were sent previously with the currently active connection.
  final Future<?> shutdown1 = mailer.shutdownConnectionPool();
  shutdown1.get();
  mailer = ... // Get a new mailer and, thus, a new SMTP connection.
  for (int i = 0; i < MAX_EMAILS_PER_CONNECTION && iterator.hasNext(); i += 1) {
    final String email = iterator.next();
    ...
  }

  // Force the shutdown of the connection pool and, thus, of the one and only SMTP connection.
  // This is in case messages get sent afterward with the currently active connection.
  final Future<?> shutdown2 = mailer.shutdownConnectionPool();
  shutdown2.get();
}

Assuming I understood correctly, this should allow me to use one connection for many emails while respecting the per-connection limit on the number of emails, but I see some caveats.

By changing the default from creating a new SMTP connection for each message to always reuse the same SMTP connection, I will now have protect all email-sending code snippets with shutdown to avoid prevent the per-connection limit from being reached by sending individual emails over time from different part of the code base. And if we forget to add protective shutdown code in some places, we could reach the limits at unexpected times and places.

Now I don't know if I expressed myself understandably, but I would prefer a solution that keeps the default behaviour safe, i.e., without requiring some protective code every time an email is sent. Because, most of the time, I don't care about the performance of sending individual emails, I would prefer the default to be simple and safe, i.e., not reusing SMTP connections, and to put more work in the very few places where performance matters and sharing an SMTP connection is relevant.

bbottema commented 1 day ago

Not entirely, by default batch module has defaults that refuses Transport connections, but they are deallocated (disconnected) after a timeout until a thread is needed again to end emails. The shutdown is only needed here to force such a deallocation, as this is a special requirement for your server.