Les mails, malgré la montée en puissance de la communication instantanée via les réseaux sociaux, restent un outil indisocciable des applications web. En mars 2013, Galen Gruman a publié un article expliquant pourquoi les applications sociales ne remplaceront jamais un e-mail.
C'est d'ailleurs une des raisons pour lesquelles on va aborder l'aspect d'envoi des e-mails dans Spring dans un article séparé. On début on verra quels sont les moyens de gestion de la communication mail dans ce framework. Ensuite on présentera l'implémentation de l'un de ces moyens avec l'exemple de l'envoi d'un mail suite à l'ouverture du compte.
Envoyer des mails dans Spring
Un package entier, org.springframework.mail, est consacré à l'envoi des mails grâce au Spring. L'interface principale utilisée pour l'envoi des mails est MailSender. Elle permet de construire un système d'expédition complexe grâce à la gestion à des niveaux différents. Les exceptions lancées concernent aussi bien les erreurs de traitement des messages que celles liées à l'authentification.
L'implémentation de cette interface qu'on utilisera dans notre application, org.springframework.mail.javamail.JavaMailSenderImpl, supporte les deux classes gérant types MIME des e-mails (MimeMessage du JavaMail et SimpleMailMessage du Spring).
La configuration du bean qui s'appelle mainSender se fait dans un fichier de contexte, comme suit :
<beans:bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl"> <beans:property name="host" value="smtp.gmail.com" /> <beans:property name="port" value="587" /> <beans:property name="username" value="bartkonieczny@gmail.com" /> <beans:property name="password" value="********" /> <beans:property name="javaMailProperties"> <beans:props> <beans:prop key="mail.smtp.auth">true</beans:prop> <beans:prop key="mail.smtp.starttls.enable">true</beans:prop> </beans:props> </beans:property> </beans:bean>
On ne remarque rien de mystérieux dans cette configuration. Juste les données d'accès et la configuration des propriétés liées à l'authentification demandée par Gmail y sont présentes. Cependant, ce n'est pas le bean qui nous servira à expédier les mails réellement. Il s'agit juste d'une configuration qu'on va injecter dans un autre bean qui se chargera de cette tâche.
Envoyer les mails avec Spring et Velocity
L'outil qu'on emploiera pour expédier les mails, va se baser sur Velocity. Il est un système des templates qui permet de gérer facilement la transmissions des variables vers la partie des vues. En plus, dans les templates Velocity on peut utiliser les variables selon la manière très simple, en les précédant par le signe du dollar ($). Regardons donc comment le bean avec notre outil de mail est configuré :
<beans:bean id="mailerTool" name="mailerTool" class="library.tools.MailerTool"> <beans:property name="mailSender" ref="mailSender" /> <beans:property name="velocityEngine" ref="velocityEngine"/> <beans:property name="sender" value="bartkonieczny@gmail.com" /> <beans:property name="localeResolver" ref="localeResolver" /> </beans:bean>
Le mailerTool possède 4 propriétés :
- mailerSender : le bean défini dans le paragraphe précédent
- velocityEngine : le moteur du Velocity utilisé pour générer les messages des mails
- sender : une chaîne de caractères avec le mail de l'expéditeur des mails
- localeResolver : le bean qui nous permettra de définir la langue de l'utilisateur, donc de lui fournir le message compréhensible
Notre mailerTool va se baser sur les traductions placées dans les fichiers de configuration qui contiendra les relations entre les codes et les templates à utiliser. Comme le démontre cet exemple :
; register template body_register_fr = register_fr.vtl body_register_en = register_en.vtl body_register_pl = register_pl.vtl title_register_fr = ${vars.login}, vous venez de vous enregistrer title_register_en = ${vars.login}, you must validate your subscription title_register_pl = ${vars.login}, czekamy tylko na Twoja aktywację konta
En occurrence, on récupérera le titre du message d'enregistrement à travers le code "title_registrer_LANG" où LANG signifie le code ISO de la langue de l'utilisateur. Le contenu du mail à envoyer va se faire via la récupération du fichier spécifié sous "body_register_LANG".
Le code du mailerTool qui préparera et enverrai des mails, se présente ainsi :
public class MailerTool { final Logger logger = LoggerFactory.getLogger(MailerTool.class); private JavaMailSender mailSender; private String sender; private VelocityEngine velocityEngine; private LocaleResolver localeResolver; @Autowired private ServletContext servletContext; /** * Mail's template data (subject and body). Not empty keys "title" and "template" are obligatory. If they are empty, * an Exception is catched on setTemplate(). * @var HashMap*/ private Map mailData; /** * Variables used in template. * @var Map */ private Map vars; public void setMailData(Map mailData) throws Exception { if(!mailData.containsKey("config") || ((String) mailData.get("config")).equals("")) throw new Exception("Vars must contain config value"); this.mailData = mailData; } public void setVars(Map vars) { this.vars = vars; } public void setMailSender(JavaMailSender mailSender) { this.mailSender = mailSender; } public JavaMailSender getMailSender() { return mailSender; } public void setSender(String sender) { this.sender = sender; } public String getSender() { return sender; } public void setVelocityEngine(VelocityEngine velocityEngine) { this.velocityEngine = velocityEngine; } public VelocityEngine getVelocityEngine() { return velocityEngine; } public void setLocaleResolver(LocaleResolver localeResolver) { this.localeResolver = localeResolver; } public LocaleResolver getLocaleResolver() { return localeResolver; } public void setServletContext(ServletContext servletContext) { logger.info("Setting servletContext to MailerTool " + servletContext); // System.out.println("Setting servletContext to MailerTool " + servletContext); this.servletContext = servletContext; } public ServletContext getServletContext() { return servletContext; } public String prepareTemplate(String bodyTpl) { return VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, bodyTpl, vars); } public String prepareTitle(String titleTpl) { VelocityContext context = new VelocityContext(); context.put("vars", vars); StringWriter writer = new StringWriter(); Velocity.init(); Velocity.evaluate(context, writer, "MAILER_TITLE", titleTpl); return writer.toString(); } public void send() throws Exception, MailException { final String mailReceiver = (String) mailData.get("to"); Properties config = new Properties(); try { String realPath = ""; // for unit testing if (servletContext instanceof MockServletContext) { realPath = "D:/resin-4.0.32/webapps/ROOT/"; } else { realPath = servletContext.getRealPath(System.getProperty("file.separator")); } FileInputStream fileInputStream = new FileInputStream(realPath+System.getProperty("file.separator")+"WEB-INF"+System.getProperty("file.separator")+"views_mails"+System.getProperty("file.separator")+"config.properties"); config.load(fileInputStream); fileInputStream.close(); } catch (Exception e) { throw new Exception("Properties can't be found",e); } HttpServletRequest req = new MockHttpServletRequest(); if (servletContext != null && !(servletContext instanceof MockServletContext)) { logger.info("=> Real servletContext found"); ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); req = sra.getRequest(); logger.info("=> Found request from servletRequestAttributes"+req); } Locale locale = RequestContextUtils.getLocale(req); logger.info("=> Found locale " + locale); final String subject = config.getProperty("title_"+(String) mailData.get("config")+"_"+locale.getLanguage().toLowerCase()); final String bodyTpl = config.getProperty("body_"+(String) mailData.get("config")+"_"+locale.getLanguage().toLowerCase()); MimeMessagePreparator preparator = new MimeMessagePreparator() { public void prepare(MimeMessage mimeMessage) throws Exception { mimeMessage.setRecipient(Message.RecipientType.TO, new InternetAddress(mailReceiver)); mimeMessage.setFrom(new InternetAddress(sender)); mimeMessage.setSubject(prepareTitle(subject)); mimeMessage.setText(prepareTemplate(bodyTpl)); } }; mailSender.send(preparator); } }
Dans le code ci-dessus, à la première étape on charge les propriétés. Ensuite on récupère l'instance implémentant l'interface HttpServletRequest. Elle sera utilisée pour déterminer la langue du message à envoyer. Plus loin on détermine les fichiers de contenu qui vont être gérés par MimeMessagePreparator. Après la préparation du contenu par les méthodes prepareTitle() et prepareTemplate(), on expédie le mail au destinataire.
Voici l'implémentation du mailerTool dans le code du service chargé de gérer les actions d'un utilisateur enregistré :
public class SubscriberServiceImpl implements SubscriberService { @Autowired private MailerTool mailerTool; private static final String TPL_REGISTER = "register"; // ... @Override @PreAuthorize("isAnonymous()") public Subscriber save(Subscriber subscriber) { DefaultTransactionDefinition def = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(def); Subscriber result = null; try { // ... HashMap<String, Object> mailData = new HashMap<String, Object>(); mailData.put("config", TPL_REGISTER); mailData.put("to", result.getEmail()); Map<String, Object> tplVars = new HashMap<String, Object>(); tplVars.put("login", result.getLogin()); tplVars.put("idCrypted", cryptograph.encrypt(Long.toString(result.getId()))); mailerTool.setMailData(mailData); mailerTool.setVars(tplVars); mailerTool.send(); transactionManager.commit(status); } catch(Exception e) { logger.info("Exception catched" + e.getMessage()); result = null; transactionManager.rollback(status); } return result; } // ... }
L'implémentation est plutôt banale. Tout d'abord on définit les données qui vont être transmises au mailerTool. Ensuite on effectue la transmission grâce au setMailData() et setVars(). Ensuite on effectue l'envoi du mail en appelant la méthode send() décrite précédemment.