Send emails with Spring by using Java annotations

前端 未结 5 487
野趣味
野趣味 2021-02-02 14:37

How could I send an email with Spring 4 (and Spring Boot) by using a pure annotation-based approach (according to the Java Configurations

5条回答
  •  长情又很酷
    2021-02-02 15:02

    With Spring-Boot it was close to trivial, with one adjustment needed for smtp.office365.com mail server - which is what this company is using.

    Before making this adjustment the authentication to the Office365 SMTP server kept failing and we'd get an error along the lines of:

    org.springframework.mail.MailSendException: Failed messages: com.sun.mail.smtp.SMTPSendFailedException: 530 5.7.57 SMTP; Client was not authenticated to send anonymous mail during MAIL FROM
        at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:474)
        at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:307)
        at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:296)
    

    This was even though we were setting a username and password that were being correctly picked up by Spring's MailProperties class.

    Turns out Office365 needs TLS authentication enabled and Spring's current JavaMail implementation doesn't have a simple way to do that with properties. The solution is to create our own javax.mail.Session instance and register it with Spring.

    The whole picture follows.

    In the pom.xml file, add:

        
            org.springframework.boot
            spring-boot-starter-mail
            ${spring-boot.version}
        
    

    Where spring-boot.version is defined elsewhere (in a parent pom in this case) and in this case has a value of 1.3.1.RELEASE

    Make sure to include the package and/or the auto-configuration class if you itemize those in your main application - if you use the default @SpringBootApplication this isn't needed, but in my case I added MailSenderAutoConfiguration.class to the @Import annotation's list of classes.

    I created a simple EmailSender class:

    @SuppressWarnings("SpringJavaAutowiringInspection")
    @Slf4j
    @Service
    @ConditionalOnClass(JavaMailSender.class)
    public class EmailSender {
    
        @Autowired
        private EventBus mmEventBus;
    
        @Autowired
        private JavaMailSender mailSender;
    
        @Autowired
        private MessageConfig messageConfig;
    
        @Subscribe
        public void sendEmail(EmailMessageEvent eme) {
            log.info("{}", eme);
    
            SimpleMailMessage msg = new SimpleMailMessage(messageConfig.getMessageTemplate());
            msg.setSubject(eme.getSubject());
            msg.setText(eme.getMessage());
    
            try {
                mailSender.send(msg);
            } catch (MailException ex) {
                log.error("Error sending mail message: " + eme.toString(), ex);
            }
        }
    
        @PostConstruct
        public void start() throws MessagingException {
            mmEventBus.register(this);
        }
    
    }
    

    Where the @Slf4j is a lombok annotation, and the @Subscribe is for Guava's EventBus, which is how this app lets the EmailSender know there's a message to send. I use a trivial EmailMessageEvent POJO that has a subject and message text - good enough for this app's purposes.

    The MessageConfig class just makes it easy to set up message defaults along with the rest of the application's configuration, and it has the one piece of special sauce that was needed to make this work with an Office365 SMTP server, a custom javax.mail.Session instance registered as a Spring Bean:

    @Data
    @Component
    @ConfigurationProperties(prefix = "spring.message", ignoreUnknownFields = false)
    @Slf4j
    public class MessageConfig {
    
        @SuppressWarnings("SpringJavaAutowiringInspection")
        @Autowired
        private MailProperties mailProperties;
    
        private String from;
        private String subject;
        private String[] recipients;
    
        private SimpleMailMessage messageTemplate;
    
        public void setRecipients(String... r) {
            this.recipients = r;
        }
    
        @PostConstruct
        public void createTemplate() {
            messageTemplate = new SimpleMailMessage();
            messageTemplate.setFrom(from);
            messageTemplate.setSubject(subject);
            messageTemplate.setTo(recipients);
    
            log.debug("Email Message Template defaults: {}", messageTemplate);
        }
    
        @Bean
        public SimpleMailMessage getMessageTemplate() {
            return messageTemplate;
        }
    
        @Bean
        public Session getSession() {
            log.debug("Creating javax.mail.Session with TLS enabled.");
            // We could be more flexible and have auth based on whether there's a username and starttls based on a property.
            Properties p = new Properties();
            p.setProperty("mail.smtp.auth", "true");
            p.setProperty("mail.smtp.starttls.enable", "true");
            p.setProperty("mail.smtp.host", mailProperties.getHost());
            p.setProperty("mail.smtp.port", mailProperties.getPort().toString());
            return Session.getDefaultInstance(p, new Authenticator() {
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(mailProperties.getUsername(), mailProperties.getPassword());
                }
            });
        }
    
    }
    

    The @Data is again a Lombok annotation - automatagically adds mutator and accessor methods, toString(), equals() & hashCode(), etc.

    The limitation of Spring's JavaMailSender (JavaMailSenderImpl) is that its Session is not directly configurable, in particular there's no way to enable TLS authentication via a property. However, if there's a javax.mail.Session Bean registered in the context, that will be injected (conditionally autowired) into the MailSenderAutoConfiguration and then used to construct the JavaMailSenderImpl instance.

    So we register just such a Bean via the getSession() method. For good measure we make the Session we construct here the default one for the JVM - change it to call getInstance() if you don't want that behavior.

    After adding this Session @Bean everything worked.

提交回复
热议问题