All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.yadaframework.components.YadaEmailService Maven / Gradle / Ivy

There is a newer version: 0.7.7.R4
Show newest version
package net.yadaframework.components;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;

import jakarta.annotation.Resource;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import jakarta.servlet.http.HttpServletRequest;
import net.yadaframework.core.YadaConfiguration;
import net.yadaframework.core.YadaConstants;
import net.yadaframework.exceptions.YadaEmailException;
import net.yadaframework.exceptions.YadaInternalException;
import net.yadaframework.web.YadaEmailContent;
import net.yadaframework.web.YadaEmailParam;

@Service
// Deve stare in questo package perchè tirato dentro da YadaWebConfig, altrimenti SpringTemplateEngine non viene iniettato
public class YadaEmailService {
	private Logger log = LoggerFactory.getLogger(YadaEmailService.class);	
	
	@Autowired private YadaConfiguration config;
	@Autowired private JavaMailSender mailSender;
	
	// Using @Resource we don't need a Qualifier annotation
	@Resource
	private SpringTemplateEngine emailTemplateEngine;
    
    @Autowired private MessageSource messageSource;
    
//    @Autowired private ServletContext servletContext;
    @Autowired private ApplicationContext applicationContext;
    @Autowired private YadaWebUtil yadaWebUtil;
//    @Autowired private YadaUtil yadaUtil;
    
    /**
     * Convert a site-relative link to absolute, because in emails we can't use @{}.
     * Example: th:href="${@yadaEmailService.buildLink('/read/234')}"
     * @param relativeLink
     * @return absolute link
     */
    public String buildLink(String relativeLink) {
    	String myServerAddress = config.getServerAddress();
    	String relative = StringUtils.prependIfMissing(relativeLink, "/");
    	return myServerAddress + relative;
    }

	public boolean sendSupportRequest(String username, String supportRequest, HttpServletRequest request, Locale locale) {
		final String emailName = "supportRequest";
		final String[] toEmail = config.getSupportRequestRecipients();
		final String[] subjectParams = {username};
		
		String clientIp = yadaWebUtil.getClientIp(request);
		String userAgent = request.getHeader("user-agent");

		final Map templateParams = new HashMap();
		templateParams.put("username", username);
		templateParams.put("supportRequest", supportRequest);
		templateParams.put("clientIp", clientIp);
		templateParams.put("userAgent", userAgent);

		Map inlineResources = new HashMap();
		inlineResources.put("logosmall", config.getEmailLogoImage());
		return sendHtmlEmail(toEmail, emailName, subjectParams, templateParams, inlineResources, locale, true);
	}

	/**
	 * @param toEmail
	 * @param emailName
	 * @param subjectParams
	 * @param templateParams
	 * @param inlineResources
	 * @param locale
	 * @param addTimestamp
	 * @deprecated use {@link YadaEmailBuilder} for a cleaner api
	 */
	@Deprecated
    public boolean sendHtmlEmail(String[] toEmail, String emailName, Object[] subjectParams, Map templateParams, Map inlineResources, Locale locale, boolean addTimestamp) {
    	return sendHtmlEmail(toEmail, null, emailName, subjectParams, templateParams, inlineResources, locale, addTimestamp);
    }
    
    /**
     * Invia una email usando un template thymeleaf.
     * Il template è localizzato, per cui si può chiamare ad esempio .html oppure _de.html.
     * Il subject è localizzato e parametrizzato, e la sua chiave è email.subject.
     * @param toEmail
     * @param replyTo can be null
     * @param emailName nome del template, ovvero del file html senza estensione e localizzazione _xx
     * @param subjectParams Valori opzionali da inserire nella stringa localizzata da usare come subject, può essere null
     * @param templateParams variabili da usare nel template, come fossero attributi del Model. Può essere null.
     * @param inlineResources mappa chiave-valore di immagini inline di tipo "cid:". Il valore è un path relativo al context-path, come per esempio "/res/img/pippo.jpg"
     * @param locale
     * @param addTimestamp true to add a timestamp to the subject
     * @return true se l'email è stata spedita
     * @deprecated use {@link YadaEmailBuilder} for a cleaner api
     */
	@Deprecated
	public boolean sendHtmlEmail(String[] toEmail, String replyTo, String emailName, Object[] subjectParams, Map templateParams, Map inlineResources, Locale locale, boolean addTimestamp) {
		return sendHtmlEmail(config.getEmailFrom(), toEmail, replyTo, emailName, subjectParams, templateParams, inlineResources, null, locale, addTimestamp);
	}
	
	/**
	 * 
	 * @param fromEmail
	 * @param toEmail
	 * @param replyTo
	 * @param emailName
	 * @param subjectParams
	 * @param templateParams
	 * @param inlineResources
	 * @param attachments
	 * @param locale
	 * @param addTimestamp
	 * @deprecated use {@link YadaEmailBuilder} for a cleaner api
	 */
	@Deprecated
	public boolean sendHtmlEmail(String[] fromEmail, String[] toEmail, String replyTo, String emailName, Object[] subjectParams, Map templateParams, Map inlineResources, Map attachments, Locale locale, boolean addTimestamp) {
		return sendHtmlEmail(fromEmail, toEmail, replyTo, emailName, subjectParams, templateParams, inlineResources, attachments, locale, addTimestamp, false);
	}

    /**
     * Invia una email usando un template thymeleaf.
     * Il template è localizzato, per cui si può chiamare ad esempio .html oppure _de.html.
     * Il subject è localizzato e parametrizzato, e la sua chiave è email.subject.
     * @param toEmail
     * @param replyTo can be null
     * @param emailName nome del template, ovvero del file html senza estensione e localizzazione _xx
     * @param subjectParams Valori opzionali da inserire nella stringa localizzata da usare come subject, può essere null
     * @param templateParams variabili da usare nel template, come fossero attributi del Model. Può essere null.
     * @param inlineResources mappa chiave-valore di immagini inline di tipo "cid:". Il valore è un path relativo al context-path, come per esempio "/res/img/pippo.jpg"
     * @param attachments mappa filename-File di file da inviare come attachment. Il filename deve avere la giusta estensione per avere il corretto mime type. 
     * @param locale
     * @param addTimestamp true to add a timestamp to the subject
	 * @param batch true to send the separate email to each recipient
     * @return true se l'email è stata spedita
     * @deprecated use {@link YadaEmailBuilder} for a cleaner api
     */
	@Deprecated
	public boolean sendHtmlEmail(String[] fromEmail, String[] toEmail, String replyTo, String emailName, Object[] subjectParams, Map templateParams, Map inlineResources, Map attachments, Locale locale, boolean addTimestamp, boolean batch) {
		YadaEmailParam yadaEmailParam = new YadaEmailParam();
		yadaEmailParam.fromEmail = fromEmail;
		yadaEmailParam.toEmail = toEmail;
		yadaEmailParam.replyTo = replyTo;
		yadaEmailParam.emailName = emailName;
		yadaEmailParam.subjectParams = subjectParams;
		yadaEmailParam.templateParams = templateParams;
		yadaEmailParam.inlineResources = inlineResources;
		yadaEmailParam.attachments = attachments;
		yadaEmailParam.locale = locale;
		yadaEmailParam.addTimestamp = addTimestamp;
		return sendHtmlEmail(yadaEmailParam, batch);
	}
	
    /**
     * Invia una email usando un template thymeleaf.
     * Il template è localizzato, per cui si può chiamare ad esempio .html oppure _de.html.
     * Il subject è localizzato e parametrizzato, e la sua chiave è email.subject.
     * @return true se l'email è stata spedita
     * @deprecated use {@link YadaEmailBuilder} for a cleaner api
     */
	@Deprecated
	public boolean sendHtmlEmail(YadaEmailParam yadaEmailParam) {
		return this.sendHtmlEmail(yadaEmailParam, false);
	}
	
	/**
	 * Send a HTML email. Better use {@link YadaEmailBuilder} for a cleaner api 
	 * @param yadaEmailParam
	 * @param batch
	 * @return true if the email has been sent correctly.
	 */
	public boolean sendHtmlEmail(YadaEmailParam yadaEmailParam, boolean batch) {
		String[] fromEmail = yadaEmailParam.fromEmail;
		String[] toEmail = yadaEmailParam.toEmail;
		String replyTo = yadaEmailParam.replyTo;
		String emailName = yadaEmailParam.emailName;
		Object[] subjectParams = yadaEmailParam.subjectParams;
		Map templateParams = yadaEmailParam.templateParams;
		Map inlineResources = yadaEmailParam.inlineResources;
		Map attachments = yadaEmailParam.attachments;
		Locale locale = yadaEmailParam.locale;
		boolean addTimestamp = yadaEmailParam.addTimestamp;
		//
		final String emailTemplate = getMailTemplateFile(emailName, locale);
		final String subject = messageSource.getMessage("email.subject." + emailName, subjectParams,  locale);
		final String body = loadTemplate(locale, templateParams, emailTemplate);
	    YadaEmailContent ec = new YadaEmailContent();
	    ec.from = fromEmail!=null?fromEmail:config.getEmailFrom();
	    if (replyTo!=null) {
	    	ec.replyTo = replyTo;
	    }
	    ec.to = toEmail;
	    ec.subject = subject + (addTimestamp?" (" + timestamp(locale) +")":"");
	    ec.body = body;
	    ec.html = true;
	    if (inlineResources!=null) {
		    ec.inlineResourceIds = new String[inlineResources.size()];
		    ec.inlineResources = new org.springframework.core.io.Resource[inlineResources.size()];
		    int i=0;
			for (Entry entry : inlineResources.entrySet()) {
				ec.inlineResourceIds[i] = entry.getKey();
//				Must support fully qualified URLs, e.g. "file:C:/test.dat".
//				Must support classpath pseudo-URLs, e.g. "classpath:test.dat".
//				Should support relative file paths, e.g. "WEB-INF/test.dat". (This will be implementation-specific, typically provided by an ApplicationContext implementation.)
				DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
				ec.inlineResources[i] = defaultResourceLoader.getResource(entry.getValue());
				if (!ec.inlineResources[i].exists()) {
					log.error("Invalid resource: " + entry.getValue());
				}
//				ec.inlineResources[i] = new ServletContextResourceLoader(servletContext).getResource(entry.getValue());
				i++;
			}
	    }
	    if (attachments!=null && attachments.size()>0) {
	    	Set keySet = attachments.keySet();
	    	int size = keySet.size();
	    	ec.attachedFilenames = new String[size];
	    	ec.attachedFiles = new File[size];
	    	int i=0;
	    	for (String filename : attachments.keySet()) {
	    		ec.attachedFilenames[i] = filename;
	    		ec.attachedFiles[i] = attachments.get(filename);
	    		i++;
			}
	    }

		if (batch) {
			List yadaEmailContents = new ArrayList<>();
			for (String to : ec.to) {
				yadaEmailContents.add(copyYadaEmailContent(ec, to));
			}
			return sendEmailBatch(yadaEmailContents);
		} else {
			return sendEmail(ec);
		}
	}

	private YadaEmailContent copyYadaEmailContent(YadaEmailContent ec, String... to) {
		YadaEmailContent newEc = new YadaEmailContent();
		newEc.from = ec.from;
		newEc.replyTo = ec.replyTo;
		newEc.to = to;
		newEc.cc = ec.cc;
		newEc.bcc = ec.bcc;
		newEc.subject = ec.subject;
		newEc.body = ec.body;
		newEc.html = ec.html;
		newEc.inlineFiles = ec.inlineFiles;
		newEc.inlineFileIds = ec.inlineFileIds;
		newEc.inlineResources = ec.inlineResources;
		newEc.inlineResourceIds = ec.inlineResourceIds;
		newEc.attachedFiles = ec.attachedFiles;
		newEc.attachedFilenames = ec.attachedFilenames;
		return newEc;
	}

	public String loadTemplate(Locale locale, Map templateParams, String emailName) {
		//		String myServerAddress = yadaWebUtil.getWebappAddress(request);
//		final WebContext ctx = new WebContext(request, response, servletContext, locale);
		// Using Context instead of WebContext, we can't access WebContent files and can't use @{somelink}
		final Context ctx = new Context(locale);
		// This allows the use of @beans inside the email template
		ctx.setVariable(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME, new ThymeleafEvaluationContext(applicationContext, null));
		// Non so come si registra un bean resolver dentro a ctx, quindi uso "config" invece di "@config"
		// TODO This "config" bean is deprecated and should be removed one day
		ctx.setVariable("config", config);
		//
		if (templateParams !=null) {
			for (Entry entry : templateParams.entrySet()) {
				ctx.setVariable(entry.getKey(), entry.getValue());
			}
		}
//		ctx.setVariable("beans", new Beans(applicationContext)); // So I can use "beans.myBean" in the template (workaround for the missing "@myBean" support)
		final String body = this.emailTemplateEngine.process("/" + YadaConstants.EMAIL_TEMPLATES_FOLDER + "/" + emailName, ctx);
		return body;
	}

	public String getTemplatePath(String templateName) {
		return YadaConstants.EMAIL_TEMPLATES_PREFIX + "/" + YadaConstants.EMAIL_TEMPLATES_FOLDER + "/" + templateName;
	}

    /**
     * Dato un nome di template senza estensione, ne ritorna il nome completo di localizzazione.
     * Per esempio "saluti" diventa "saluti_it" se esiste, altrimenti resta "saluti".
     * Se il template non esiste, lancia InternalException
     * @param templateNameNoHtml
     * @param locale
     * @return
     */
	public String getMailTemplateFile(String templateNameNoHtml, Locale locale) {
//    	String base = "/WEB-INF/emailTemplates/";
    	String prefix = templateNameNoHtml; // emailChange
    	String languagePart = "_" + locale.getLanguage(); // _it
    	String suffix = ".html";
    	String filename = prefix + languagePart + suffix; // emailChange_it.html
    	// TODO check if the / before filename is still needed
		ClassPathResource classPathResource = new ClassPathResource(getTemplatePath(filename));
		if (classPathResource.exists()) {
			return prefix + languagePart;
		}
		filename = prefix + suffix; // emailChange.html
		classPathResource = new ClassPathResource(getTemplatePath(filename));
		if (classPathResource.exists()) {
			return prefix;
		}
    	throw new YadaInternalException("Email template not found: " + templateNameNoHtml);
    }

    /**
     * Send an email by specifying the content directly, without a template
	 * @param from [address, name]
	 * @param to
	 * @param replyTo
	 * @param cc
	 * @param bcc
	 * @param subject
	 * @param body
	 * @param html
	 * @param inlineFile
	 * @param inlineFilename
	 * @return true if the email has been sent
	 */
	public boolean sendEmail(String[] from, String to, String replyTo, String cc, String bcc, String subject, String body, boolean html, File inlineFile, String inlineFilename) {
		YadaEmailContent content = new YadaEmailContent();
		content.from = from;
		content.replyTo = replyTo;
		content.to = new String[]{to};
		content.cc = new String[]{cc};
		content.bcc = new String[]{bcc};
		content.subject = subject;
		content.body = body;
		content.html = html;
		content.inlineFiles = new File[] {inlineFile};
		content.inlineFileIds = new String[] {inlineFilename};
		return sendEmail(content);
	}

	public boolean sendEmail(YadaEmailContent yadaEmailContent) {
		try {
			MimeMessage msg = createMimeMessage(yadaEmailContent);
			if (msg!=null) {
				log.debug("Sending email to '{}'...", Arrays.asList(yadaEmailContent.to));
				mailSender.send(msg);
				log.debug("Email sent to '{}'", Arrays.asList(yadaEmailContent.to));
				return true;
			}
		} catch (Exception e) {
			String to = ArrayUtils.toString(yadaEmailContent.to, "");
			log.error("Error while sending email message to '{}'", to, e);
			if (config.isEmailThrowExceptions()) {
				throw new YadaEmailException(e);
			}
		}
		return false;
	}

	public boolean sendEmailBatch(List yadaEmailContents) {
		boolean result = true;
		List messageList = new ArrayList();
		for (YadaEmailContent yadaEmailContent : yadaEmailContents) {
			try {
				MimeMessage mimeMessage = createMimeMessage(yadaEmailContent);
				if (mimeMessage!=null) {
					messageList.add(mimeMessage);
				}
			} catch (Exception e) {
				result = false;
				log.error("Error while creating batch email message to {} (ignored)", yadaEmailContent.to, e);
			}
		}
		if (messageList.size()>0) {
			mailSender.send(messageList.toArray(new MimeMessage[messageList.size()])); // Batch
		}
		return result;
	}
	
	private String[] purifyRecipients(String[] addresses, YadaEmailContent yadaEmailContent) {
		// Se  esiste, mando solo se l'email è nella lista
		List validEmail = config.getValidDestinationEmails();
		if (validEmail!=null && !validEmail.isEmpty()) {
			List recipients = new ArrayList(Arrays.asList(addresses)); // Il doppio passaggio serve perchè asList è fixed size
			// Tengo solo quelli validi
			if (recipients.retainAll(validEmail)) {
				// Alcuni sono stati rimossi. Logghiamo quelli rimossi
				List invalidEmails = new ArrayList(Arrays.asList(addresses)); // Il doppio passaggio serve perchè asList è fixed size
				invalidEmails.removeAll(validEmail);
				for (String address : invalidEmails) {
					log.warn("Email not authorized in configuration (not in ). Skipping message for '{}' from='{}' subject='{}' body='{}'",
						new Object[]{address, yadaEmailContent.from, yadaEmailContent.subject, yadaEmailContent.body});
				}
			}
			return recipients.toArray(new String[0]);
		} else {
			return addresses;
		}
	}
	
	private MimeMessage createMimeMessage(YadaEmailContent yadaEmailContent) throws MessagingException {
		if (!config.isEmailEnabled()) {
			log.warn("Emails not enabled. Skipping message from='{}' to='{}' cc='{}' bcc='{}' subject='{}' body='{}'", 
					new Object[]{yadaEmailContent.from, yadaEmailContent.to, yadaEmailContent.cc, yadaEmailContent.bcc, yadaEmailContent.subject, yadaEmailContent.body});
			return null;
		}
		int totRecipients = 0;
		if (yadaEmailContent.to!=null) {
			yadaEmailContent.to = purifyRecipients(yadaEmailContent.to, yadaEmailContent);
			totRecipients += yadaEmailContent.to.length;
		}		
		if (yadaEmailContent.cc!=null) {
			yadaEmailContent.cc = purifyRecipients(yadaEmailContent.cc, yadaEmailContent);
			totRecipients += yadaEmailContent.cc.length;
		}		
		if (yadaEmailContent.bcc!=null) {
			yadaEmailContent.bcc = purifyRecipients(yadaEmailContent.bcc, yadaEmailContent);
			totRecipients += yadaEmailContent.bcc.length;
		}		
		if ( totRecipients == 0) {
			return null;
		}
		if (!config.isProductionEnvironment()) { 
			String env = config.getApplicationEnvironment();
			yadaEmailContent.subject = "[" + (StringUtils.isBlank(env)?"TEST":env.toUpperCase()) + "] " + yadaEmailContent.subject;
//			emailContent.body = "

Questa email è stata inviata durante un test del sistema, si prega di ignorarla, grazie.

" + // "

This email has been sent during a system test, please ignore it, thank you.

" + emailContent.body; } MimeMessage msg = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(msg, true, "UTF-8"); // true = multipart try { helper.setFrom(yadaEmailContent.from[0], yadaEmailContent.from[1]); } catch (UnsupportedEncodingException e) { log.error("Invalid encoding - ignoring 'from' personal name", e); helper.setFrom(yadaEmailContent.from[0]); } if(yadaEmailContent.replyTo!=null) { helper.setReplyTo(yadaEmailContent.replyTo); } if(yadaEmailContent.to!=null) { helper.setTo(yadaEmailContent.to); } if(yadaEmailContent.cc!=null) { helper.setCc(yadaEmailContent.cc); } if(yadaEmailContent.bcc!=null) { helper.setBcc(yadaEmailContent.bcc); } helper.setSubject(yadaEmailContent.subject); helper.setText(yadaEmailContent.body, yadaEmailContent.html); // true = html if (yadaEmailContent.inlineFiles!=null && yadaEmailContent.inlineFileIds!=null && yadaEmailContent.inlineFiles.length==yadaEmailContent.inlineFileIds.length) { for (int i = 0; i < yadaEmailContent.inlineFiles.length; i++) { File file = yadaEmailContent.inlineFiles[i]; String fileId = yadaEmailContent.inlineFileIds[i]; helper.addInline(fileId, file); } } if (yadaEmailContent.inlineResources!=null && yadaEmailContent.inlineResourceIds!=null && yadaEmailContent.inlineResources.length==yadaEmailContent.inlineResourceIds.length) { for (int i = 0; i < yadaEmailContent.inlineResources.length; i++) { org.springframework.core.io.Resource resource = yadaEmailContent.inlineResources[i]; String resourceId = yadaEmailContent.inlineResourceIds[i]; helper.addInline(resourceId, resource); } } if (yadaEmailContent.attachedFiles!=null && yadaEmailContent.attachedFilenames!=null && yadaEmailContent.attachedFiles.length==yadaEmailContent.attachedFilenames.length) { for (int i = 0; i < yadaEmailContent.attachedFiles.length; i++) { File file = yadaEmailContent.attachedFiles[i]; String filename = yadaEmailContent.attachedFilenames[i]; helper.addAttachment(filename, file); // helper.addInline(filename, file); } } log.info("Sending email to={}, from={}, replyTo={}, cc={}, bcc={}, subject={}", new Object[] {yadaEmailContent.to, yadaEmailContent.from, yadaEmailContent.replyTo, yadaEmailContent.cc, yadaEmailContent.bcc, yadaEmailContent.subject}); log.debug("Email body = {}", yadaEmailContent.body); return msg; } public String timestamp(Locale locale) { return DateFormatUtils.format(new Date(), "yyyy-MM-dd@HH:mm", locale); } public String timestamp() { return DateFormatUtils.ISO_DATETIME_FORMAT.format(new Date()); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy