Class EMailSender

java.lang.Object
de.gustavblass.commons.email.EMailSender
All Implemented Interfaces:
de.gustavblass.commons.Copyable<EMailSender>

public class EMailSender extends Object implements de.gustavblass.commons.Copyable<EMailSender>
Helper tool to send e-mails via an SMTP server. The e-mails can be encrypted with PGP in PGP/MIME format.
See Also:
  • Field Details

    • LOG

      private static final org.apache.logging.log4j.Logger LOG
    • PRODUCT_NAME

      public static final String PRODUCT_NAME
      The branded name of this library (instead of the generic name “EMailSender”).
      See Also:
    • VERSION

      @NonNull public static final @NonNull com.fasterxml.jackson.core.Version VERSION
      The current version of this library.
    • DEFAULT_USER_AGENT

      @NonNull private static final @NonNull String DEFAULT_USER_AGENT
      The default user-agent string sent with the e-mail.
    • bucket

      @NonNull private final @NonNull io.github.bucket4j.Bucket bucket
      Rate-limits all requests to the e-mail server to prevent spamming it and potentially getting banned.
    • MAXIMUM_PORT_NUMBER

      public static final int MAXIMUM_PORT_NUMBER
      Port numbers are 16-bit unsigned integers, thus ranging from 0 to 65535.
    • SMTP_PORT

      public static final int SMTP_PORT
      The default port of the SMTP server that will be used to send the e-mails.
      See Also:
    • IMAP_PORT

      public static final int IMAP_PORT
      The default port of the IMAP server that will be used to store the sent e-mails in the sent-folder of the user's inbox.
      See Also:
    • BOUNDARY_PATTERN

      private static final Pattern BOUNDARY_PATTERN
      The regular expression pattern to extract the boundary from the Content-Type header of a MIME message.
      See Also:
    • SENT_FOLDER_NAMES

      private static final String[] SENT_FOLDER_NAMES
      Common names for the sent-folder in various IMAP servers.
    • smtpHost

      @NonNull private @NonNull String smtpHost
      The domain name of the SMTP server that will be used to send the e-mails.
    • smtpPort

      private int smtpPort
      The port of the SMTP server that will be used to send the e-mails.
    • imapHost

      @NonNull private @NonNull String imapHost
      The domain name of the IMAP server that will be used to store the sent e-mails in the sent-folder of the user's inbox.
    • imapPort

      private int imapPort
      The port of the IMAP server that will be used to store the sent e-mails in the sent-folder of the user's inbox.
    • email

      @NonNull private final @NonNull jakarta.mail.internet.InternetAddress email
      The user's e-mail address at the e-mail server. Will be used as the sender of the e-mail.
    • password

      private final char @NonNull [] password
      The password for the e-mail server (both smtpHost and imapHost).
    • secretKey

      private byte @Nullable [] secretKey
      The secret (a.k.a. private) OpenPGP key to sign the e-mails with (regardless of encryption).
    • keyPassword

      private byte @Nullable [] keyPassword
      The passphrase to decrypt the secretKey. Null if the secret key is not passphrase-protected.
    • userAgent

      @Nullable private @Nullable String userAgent
      The user-agent string to send with the e-mail.
    • properties

      @NonNull private @NonNull Properties properties
      The configuration for sending the e-mails.
    • session

      @NonNull private @NonNull jakarta.mail.Session session
      The e-mail session that is used to send the e-mails.
  • Constructor Details

    • EMailSender

      public EMailSender(@NonNull @NonNull String smtpHost, @NonNull @NonNull String imapHost, @NonNull @NonNull jakarta.mail.internet.InternetAddress email, char @NonNull [] password) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Constructs a new EMailSender with the given SMTP credentials for the given e-mail server.
      Parameters:
      smtpHost - The domain name of the SMTP server. The default port will be used.
      imapHost - The domain name of the IMAP server. The default port will be used.
      email - The user's e-mail address at the given e-mail server. Will be used as the sender of the e-mail.
      password - The user's password for the e-mail server.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given SMTP or IMAP host is not a valid domain name.
    • EMailSender

      public EMailSender(@NonNull @NonNull String smtpHost, Integer smtpPort, @NonNull @NonNull String imapHost, int imapPort, @NonNull @NonNull jakarta.mail.internet.InternetAddress email, char @NonNull [] password) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Constructs a new EMailSender with the given SMTP credentials for the given e-mail server.
      Parameters:
      smtpHost - The domain name of the SMTP server.
      smtpPort - The port number of the SMTP server. Must be between 0 and 65535.
      imapHost - The domain name of the IMAP server.
      imapPort - The port number of the IMAP server. Must be between 0 and 65535.
      email - The user's e-mail address at the given e-mail server. Will be used as the sender of the e-mail.
      password - The user's password for the e-mail server.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given SMTP or IMAP host is not a valid domain name or if the given port numbers are not between 0 and 65535.
  • Method Details

    • setSmtpHost

      public void setSmtpHost(@NonNull @NonNull String host) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Updates the smtpHost with the given domain name.
      Parameters:
      host - The new domain name of the SMTP server.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given host is not a valid domain name.
    • setImapHost

      public void setImapHost(@NonNull @NonNull String host) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Updates the imapHost with the given domain name.
      Parameters:
      host - The new domain name of the IMAP server.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given host is not a valid domain name.
    • setSmtpPort

      public void setSmtpPort(int port) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Updates the smtpPort with the given port number.
      Parameters:
      port - The new port number of the SMTP server. Must be between 0 and 65535.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given port number is invalid.
    • setImapPort

      public void setImapPort(int port) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Updates the imapPort with the given port number.
      Parameters:
      port - The new port number of the IMAP server. Must be between 0 and 65535.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given port number is invalid.
    • setUserAgent

      public void setUserAgent(@NonNull @NonNull String userAgent) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Updates the userAgent.
      Parameters:
      userAgent - The new user-agent string to send with the e-mail. Must not be blank.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given user-agent string is blank.
    • resetUserAgent

      public void resetUserAgent()
      Deletes the userAgent.
    • initialiseProperties

      private void initialiseProperties()
      Initialises the properties with the default settings for sending e-mails via the SMTP server.
    • updateSmtpConnection

      public void updateSmtpConnection(@NonNull @NonNull SmtpConnectionSecurity connection)
      Updates the properties to use the given SmtpConnectionSecurity for sending e-mails.
      Parameters:
      connection - Whether to use SSL or STARTTLS for the connection to the SMTP server.
    • enableSigning

      public void enableSigning(byte @NonNull [] secretKey) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Specifies that the encrypted message should be signed with the given secret key.
      Parameters:
      secretKey - The secret (a.k.a. private) key to sign the message with. Must not be empty or require a passphrase.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given secrets key is empty.
    • enableSigning

      public void enableSigning(byte @NonNull [] secretKey, byte @NonNull [] passphrase) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Specifies that the encrypted message should be signed with the given secret key.
      Parameters:
      secretKey - The secret (a.k.a. private) key to sign the message with. Must not be empty.
      passphrase - The passphrase to decrypt the secret key.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given secret key is empty or if the passphrase is empty.
    • setSecretKey

      public void setSecretKey(byte @NonNull [] secretKey) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Updates the secretKey used to sign the message and deletes the keyPassword.
      Parameters:
      secretKey - The new secret (a.k.a. private) key to sign the message with. Must not be empty or require a passphrase.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given secret key is empty.
    • setSecretKey

      public void setSecretKey(byte @NonNull [] secretKey, byte @NonNull [] passphrase) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Updates the secretKey used to sign the message and sets the keyPassword.
      Parameters:
      secretKey - The new secret (a.k.a. private) key to sign the message with. Must not be empty.
      passphrase - The passphrase to decrypt the secret key.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the given secret key is empty or if the passphrase is empty.
    • send

      @NonNull public @NonNull EMailSender.SentEmail send(@NonNull @NonNull DraftEmail eMail) throws InterruptedException, de.gustavblass.commons.exceptions.IllegalArgumentException, IOException
      Sends an e-mail from the sender using the password specified in the constructor to log in at the SMTP server.
      Parameters:
      eMail - The e-mail to send.
      Returns:

      The entire e-mail as it was sent, ready to be written to an .eml file.

      • unencryptedEmail: A copy of the e-mail before encryption or (if sent unencrypted) the exact e-mail as it was sent. Empty if fallBackToUnencrypted is false.
      • encryptedEmail: If the e-mail was sent encrypted, the e-mail as it was sent. Otherwise, empty.

      One or both may be empty if the e-mail could not be written to an OutputStream. If empty, this does not imply that the e-mail was not sent; if the e-mail could not be sent, an exception will be thrown instead.

      Throws:
      IOException - If the e-mail could not be sent.
      de.gustavblass.commons.exceptions.IllegalArgumentException - If the e-mail could not be constructed from the given parameters (should not happen).
      InterruptedException - If the thread was interrupted while sending the e-mail.
    • sendEncryptedEmail

      @NonNull private @NonNull EMailSender.SentEmail sendEncryptedEmail(@NonNull @NonNull DraftEmail eMail) throws InterruptedException, IOException, jakarta.mail.MessagingException
      Throws:
      InterruptedException
      IOException
      jakarta.mail.MessagingException
    • encryptMessage

      private void encryptMessage(@NonNull @NonNull DraftEmail eMail, @NonNull @NonNull jakarta.mail.internet.MimeMessage encryptedEmail) throws jakarta.mail.MessagingException, IOException, de.gustavblass.commons.exceptions.IllegalArgumentException
      Throws:
      jakarta.mail.MessagingException
      IOException
      de.gustavblass.commons.exceptions.IllegalArgumentException
    • saveEmailToSentFolder

      private void saveEmailToSentFolder(@NonNull @NonNull jakarta.mail.internet.MimeMessage email) throws IOException, de.gustavblass.commons.exceptions.NotFoundException
      Tries to store the given e-mail in the sent-folder of the user's e-mail account, using the IMAP protocol.
      Parameters:
      email - The message to store in the sent-folder.
      Throws:
      IOException - If the connection to the IMAP server failed.
      de.gustavblass.commons.exceptions.NotFoundException - If the sent-folder could not be found.
      Implementation Note:
      Since the sent-folder is not standardised, several names are tried to find the correct folder. This method throws NotFoundException instead of FolderNotFoundException because the constructors of the latter require a Folder.
    • constructEmail

      @NonNull private @NonNull jakarta.mail.internet.MimeMessage constructEmail(@NonNull @NonNull DraftEmail draft) throws jakarta.mail.MessagingException, IOException, de.gustavblass.commons.exceptions.IllegalStateException
      Constructs a new MimeMessage from the specified DraftEmail
      Parameters:
      draft - The draft from which to construct the e-mail.
      Returns:
      The constructed e-mail.
      Throws:
      de.gustavblass.commons.exceptions.IllegalStateException - If the e-mail is from a read-only folder.
      jakarta.mail.MessagingException - If the e-mail is from a read-only folder.
      IOException - If the files attached could not be read.
    • constructMultipart

      @NonNull private @NonNull jakarta.mail.internet.MimeMultipart constructMultipart(String message, Iterable<File> attachments) throws jakarta.mail.MessagingException, IOException
      Constructs a MIME multipart from the given e-mail message and files attached.
      Parameters:
      message - The actual content of the e-mail.
      attachments - Any number of files to send along with the message.
      Returns:
      The e-mail message as a MIME multipart.
      Throws:
      jakarta.mail.MessagingException - No context specified by the underlying JavaMail API. Sorry.
      IOException - If the files attached could not be read.
    • writeEmail

      @NonNull private static @NonNull Optional<ByteArrayOutputStream> writeEmail(@NonNull @NonNull jakarta.mail.Message email)
      Converts the given e-mail to its binary representation (in MIME format).
      Parameters:
      email - The e-mail to convert.
      Returns:
      The e-mail as a binary representation in MIME format.
    • encryptMessage

      @NonNull private @NonNull File encryptMessage(@NonNull @NonNull jakarta.mail.internet.MimeMultipart message, @NonNull @NonNull Set<String> pgpPublicKeys, @NonNull @NonNull String boundary) throws de.gustavblass.commons.exceptions.IllegalArgumentException
      Encrypts the given MIME message for the given PGP public keys, following the PGP/MIME standard.
      Parameters:
      message - The already MIME-formatted message to encrypt. May contain text and several attachments of any file type.
      pgpPublicKeys - Any number of PGP public keys to encrypt the message with. It does not matter if the keys are of RSA or ECC type, as long as they support encryption.
      boundary - The boundary used in the MIME message provided. If there is no boundary present, this parameter should be an empty String (if so, no boundary will be specified). Will be added to the Content-Type header of the MIME message.
      Returns:
      The encrypted message as a File with the name encrypted.asc in the system's temporary directory.
      Throws:
      de.gustavblass.commons.exceptions.IllegalArgumentException -

      If:

      • no PGP public keys were provided
      • If the message could not be written out
      • The message could not be written to the encrypted.asc PGP/MIME file.
      Implementation Note:

      The boundary is needed due to limitations of JavaMail: It does not natively support PGP/MIME encryption and – by default – places a boundary at the top of the MIME message, which does not match the PGP/MIME format. Therefore, we can't encrypt entire MimeMessages but only their MimeMultipart parts and have to construct the beginning of the MimeMultipart ourselves.

      This is done by specifying the Content-Type header at the very top, as required for PGP/MIME, including the specified boundary.

      Incorrect (JavaMail default)
      
           --=-SiYyK6Oi9/3KVlFTZwJf
           Content-Type: multipart/mixed; boundary="=-SiYyK6Oi9/3KVlFTZwJf"
      
           Content-Type: text/plain
           Content-Transfer-Encoding: 7bit
      
           To whom it may concern,
           I am writing that you should not read this e-mail.
      
           --=-SiYyK6Oi9/3KVlFTZwJf
           Content-Disposition: attachment; filename="attachment.txt"
           Content-Type: text/plain; name="attachment.txt"; charset="UTF-8"
           Content-Transfer-Encoding: base64
      
           4oCcVWx0aW1hdGVseSwgYXJndWluZyB0aGF0IHlvdSBkb24ndCBjYXJlIGFib3V0IHRoZSByaWdo
           dCB0byBwcml2YWN5IGJlY2F1c2UgeW91IGhhdmUgbm90aGluZyB0byBoaWRlIGlzIG5vCmRpZmZl
           cmVudCB0aGFuIHNheWluZyB5b3UgZG9uJ3QgY2FyZSBhYm91dCBmcmVlIHNwZWVjaCBiZWNhdXNl
           IHlvdSBoYXZlIG5vdGhpbmcgdG8gc2F5LuKAnQoK4oCTIEVkd2FyZCBTbm93ZGVu
      
           --=-SiYyK6Oi9/3KVlFTZwJf--
       
      Correct (manually constructed)
      
           Content-Type: multipart/mixed; boundary="=-SiYyK6Oi9/3KVlFTZwJf"
      
           --=-SiYyK6Oi9/3KVlFTZwJf
           Content-Type: text/plain
           Content-Transfer-Encoding: 7bit
      
           To whom it may concern,
           I am writing that you should not read this e-mail.
      
           --=-SiYyK6Oi9/3KVlFTZwJf
           Content-Disposition: attachment; filename="attachment.txt"
           Content-Type: text/plain; name="attachment.txt"; charset="UTF-8"
           Content-Transfer-Encoding: base64
      
           4oCcVWx0aW1hdGVseSwgYXJndWluZyB0aGF0IHlvdSBkb24ndCBjYXJlIGFib3V0IHRoZSByaWdo
           dCB0byBwcml2YWN5IGJlY2F1c2UgeW91IGhhdmUgbm90aGluZyB0byBoaWRlIGlzIG5vCmRpZmZl
           cmVudCB0aGFuIHNheWluZyB5b3UgZG9uJ3QgY2FyZSBhYm91dCBmcmVlIHNwZWVjaCBiZWNhdXNl
           IHlvdSBoYXZlIG5vdGhpbmcgdG8gc2F5LuKAnQoK4oCTIEVkd2FyZCBTbm93ZGVu
      
           --=-SiYyK6Oi9/3KVlFTZwJf--
       
    • toString

      @NonNull public @NonNull String toString()
      Overrides:
      toString in class Object
    • copy

      @NonNull public @NonNull EMailSender copy() throws IllegalStateException
      Specified by:
      copy in interface de.gustavblass.commons.Copyable<EMailSender>
      Throws:
      IllegalStateException
    • equals

      public boolean equals(@Nullable @Nullable Object object)
      Compares this EMailSender with the given Object.
      Overrides:
      equals in class Object
      Parameters:
      object - The Object to compare this EMailSender with.
      Returns:
      • True if the given object is an EMailSender whose fields' values match their equivalents in this EMailSender, especially (but not exclusively) if the given EMailSender is the exact same reference as this EMailSender.
      • False if the given object is not a EMailSender or if any of the fields' values differ from their equivalents in this EMailSender.
    • parseBoundary

      @NonNull private static @NonNull Optional<String> parseBoundary(@NonNull @NonNull String contentTypeHeader)
      Parses the boundary from the given Content-Type header of a MIME message.
      Parameters:
      contentTypeHeader - The value of a MIME message's Content-Type header.
      Returns:
      The boundary specified in the given Content-Type header, if present. Empty if no boundary was found.
      Implementation Note:
      This method is necessary because JavaMail does not support PGP/MIME encryption natively and does not provide a way to manually specify a boundary for the MIME message. Therefore, the boundary must be extracted from the Content-Type header of the JavaMail-generated MIME message.