001/* 002 * Copyright 2022 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.forms.helper; 017 018import java.io.IOException; 019import java.io.InputStreamReader; 020import java.io.Reader; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027import java.util.Optional; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.context.Context; 032import org.apache.avalon.framework.context.ContextException; 033import org.apache.avalon.framework.context.Contextualizable; 034import org.apache.avalon.framework.service.ServiceException; 035import org.apache.avalon.framework.service.ServiceManager; 036import org.apache.avalon.framework.service.Serviceable; 037import org.apache.cocoon.components.ContextHelper; 038import org.apache.cocoon.environment.Request; 039import org.apache.commons.io.IOUtils; 040import org.apache.commons.lang.StringUtils; 041import org.apache.excalibur.source.Source; 042import org.apache.excalibur.source.SourceResolver; 043 044import org.ametys.cms.data.File; 045import org.ametys.core.ui.Callable; 046import org.ametys.core.user.User; 047import org.ametys.core.user.UserIdentity; 048import org.ametys.core.user.UserManager; 049import org.ametys.core.util.I18nUtils; 050import org.ametys.core.util.mail.SendMailHelper; 051import org.ametys.core.util.mail.SendMailHelper.MailBuilder; 052import org.ametys.core.util.mail.SendMailHelper.NamedStream; 053import org.ametys.plugins.forms.dao.FormDAO; 054import org.ametys.plugins.forms.question.FormQuestionType; 055import org.ametys.plugins.forms.question.sources.UsersSourceType; 056import org.ametys.plugins.forms.question.types.ChoicesListQuestionType; 057import org.ametys.plugins.forms.question.types.FileQuestionType; 058import org.ametys.plugins.forms.question.types.SimpleTextQuestionType; 059import org.ametys.plugins.forms.repository.Form; 060import org.ametys.plugins.forms.repository.FormEntry; 061import org.ametys.plugins.forms.repository.FormQuestion; 062import org.ametys.plugins.repository.AmetysObjectResolver; 063import org.ametys.runtime.config.Config; 064import org.ametys.runtime.i18n.I18nizableText; 065import org.ametys.runtime.plugin.component.AbstractLogEnabled; 066 067import jakarta.mail.MessagingException; 068 069/** 070 * The helper for form mail dialog 071 */ 072public class FormMailHelper extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 073{ 074 /** Avalon ROLE. */ 075 public static final String ROLE = FormMailHelper.class.getName(); 076 077 /** The param key for read restriction enable */ 078 public static final String READ_RESTRICTION_ENABLE = "read-restriction-enable"; 079 080 /** The value for entry user in the receiver combobox */ 081 public static final String RECEIVER_COMBOBOX_ENTRY_USER_VALUE = "entry-user"; 082 083 /** The request key to ignore right */ 084 public static final String IGNORE_RIGHT_KEY = "ignore-right"; 085 086 /** Pattern for adding entry in acknowledgement of receipt if present in body */ 087 protected static final String _FORM_ENTRY_PATTERN = "{form}"; 088 089 /** Ametys object resolver. */ 090 protected AmetysObjectResolver _resolver; 091 092 /** I18n Utils */ 093 protected I18nUtils _i18nUtils; 094 095 /** The user manager */ 096 protected UserManager _userManager; 097 098 /** The source resolver. */ 099 protected SourceResolver _sourceResolver; 100 101 /** The context */ 102 protected Context _context; 103 104 /** The analyse of files for virus helper */ 105 protected FormDAO _formDAO; 106 107 /** 108 * The type of limitation for the mail 109 */ 110 public enum LimitationMailType 111 { 112 /** The mail for queue */ 113 QUEUE, 114 /** The mail for the limit */ 115 LIMIT; 116 } 117 118 public void service(ServiceManager manager) throws ServiceException 119 { 120 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 121 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 122 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 123 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 124 _formDAO = (FormDAO) manager.lookup(FormDAO.ROLE); 125 } 126 127 public void contextualize(Context context) throws ContextException 128 { 129 _context = context; 130 } 131 132 /** 133 * Get the fields with email regex constraint so they can be used as receivers address for form mail 134 * @param formId Id of the form 135 * @return a map of the form question, key is the question id,value is the question title 136 */ 137 @Callable 138 public Map<String, Object> getAvailableReceiverFields(String formId) 139 { 140 Map<String, Object> results = new HashMap<>(); 141 List<Object> receiverfields = new ArrayList<>(); 142 143 for (FormQuestion question : getQuestionWithMail(formId)) 144 { 145 Map<String, String> properties = new HashMap<>(); 146 properties.put("id", question.getNameForForm()); 147 properties.put("title", question.getTitle()); 148 receiverfields.add(properties); 149 } 150 151 Map<String, String> properties = new HashMap<>(); 152 properties.put("id", RECEIVER_COMBOBOX_ENTRY_USER_VALUE); 153 properties.put("title", _i18nUtils.translate(new I18nizableText("plugin.forms", "PLUGINS_FORMS_ACKNOWLEDGEMENT_RECEIPT_ENTRY_USER"))); 154 receiverfields.add(properties); 155 results.put("data", receiverfields); 156 157 return results; 158 } 159 160 /** 161 * Get the list of questions which return a mail 162 * @param formId the form id 163 * @return the list of questions 164 */ 165 public List<FormQuestion> getQuestionWithMail(String formId) 166 { 167 List<FormQuestion> questions = new ArrayList<>(); 168 Form form = _resolver.resolveById(formId); 169 170 for (FormQuestion question : form.getQuestions()) 171 { 172 FormQuestionType type = question.getType(); 173 if (type instanceof SimpleTextQuestionType) 174 { 175 if (question.hasValue(SimpleTextQuestionType.ATTRIBUTE_REGEXP) && question.getValue(SimpleTextQuestionType.ATTRIBUTE_REGEXP).equals(SimpleTextQuestionType.EMAIL_REGEX_VALUE)) 176 { 177 questions.add(question); 178 } 179 } 180 else if (type instanceof ChoicesListQuestionType cLType && cLType.getSourceType(question) instanceof UsersSourceType) 181 { 182 questions.add(question); 183 } 184 } 185 186 return questions; 187 } 188 189 /** 190 * Get the receiver from the entry 191 * @param entry the entry 192 * @param receiverPath the path to get receiver 193 * @return the receiver from the entry 194 */ 195 public Optional<String> getReceiver(FormEntry entry, Optional<String> receiverPath) 196 { 197 if (receiverPath.isEmpty()) 198 { 199 return Optional.empty(); 200 } 201 202 if (receiverPath.get().equals(RECEIVER_COMBOBOX_ENTRY_USER_VALUE)) 203 { 204 UserIdentity userIdentity = entry.getUser(); 205 User user = _userManager.getUser(userIdentity); 206 return Optional.ofNullable(user != null ? user.getEmail() : null); 207 } 208 else 209 { 210 FormQuestion question = entry.getForm().getQuestion(receiverPath.get()); 211 FormQuestionType type = question.getType(); 212 if (type instanceof SimpleTextQuestionType) 213 { 214 return Optional.ofNullable(entry.getValue(receiverPath.get())); 215 } 216 else if (type instanceof ChoicesListQuestionType cLType && cLType.getSourceType(question) instanceof UsersSourceType) 217 { 218 if (!entry.isMultiple(receiverPath.get())) 219 { 220 UserIdentity userIdentity = entry.getValue(receiverPath.get()); 221 if (userIdentity != null) 222 { 223 User user = _userManager.getUser(userIdentity); 224 return user != null ? Optional.ofNullable(user.getEmail()) : Optional.empty(); 225 } 226 } 227 } 228 229 return Optional.empty(); 230 } 231 } 232 233 /** 234 * Send the notification emails. 235 * @param form the form. 236 * @param entry the user input. 237 * @param adminEmails list of email address where to send the notification 238 */ 239 public void sendEmailsForAdmin(Form form, FormEntry entry, String[] adminEmails) 240 { 241 try 242 { 243 String sender = Config.getInstance().getValue("smtp.mail.from"); 244 String params = "?type=results&form-name=" + form.getTitle() + "&locale=" + _formDAO.getFormLocale(form); 245 String subjectParams = params; 246 UserIdentity userIdentity = entry.getUser(); 247 if (userIdentity != null) 248 { 249 User user = _userManager.getUser(userIdentity); 250 if (user != null) 251 { 252 subjectParams += "&user=" + user.getFullName(); 253 } 254 } 255 String subject = getMail("subject.txt" + subjectParams, entry, Map.of(), false); 256 String html = getMail("results.html" + params, entry, Map.of(), false); 257 String text = getMail("results.txt" + params, entry, Map.of(), false); 258 259 for (String email : adminEmails) 260 { 261 if (StringUtils.isNotEmpty(email)) 262 { 263 _sendMail(form, entry, subject, html, text, sender, email, false, false); 264 } 265 } 266 } 267 catch (IOException e) 268 { 269 getLogger().error("Error creating the notification message.", e); 270 } 271 } 272 273 /** 274 * Send limitation mail when the limit is reached 275 * @param entry the form entry 276 * @param adminEmails list of email address where to send the notification 277 * @param limitationType the type of limitation 278 */ 279 public void sendLimitationReachedMailForAdmin(FormEntry entry, String[] adminEmails, LimitationMailType limitationType) 280 { 281 try 282 { 283 Form form = entry.getForm(); 284 285 String sender = Config.getInstance().getValue("smtp.mail.from"); 286 String type = limitationType.name().toLowerCase(); 287 String params = "?type=" + type + "&form-name=" + form.getTitle() + "&locale=" + _formDAO.getFormLocale(form); 288 String subject = getMail("subject.txt" + params, entry, Map.of(), false); 289 String html = getMail(type + ".html" + params, entry, Map.of(), false); 290 String text = getMail(type + ".txt" + params, entry, Map.of(), false); 291 292 for (String email : adminEmails) 293 { 294 if (StringUtils.isNotEmpty(email)) 295 { 296 _sendMail(form, entry, subject, html, text, sender, email, false, false); 297 } 298 } 299 } 300 catch (IOException e) 301 { 302 getLogger().error("Error creating the limit message.", e); 303 } 304 } 305 306 /** 307 * Send the receipt email. 308 * @param form the form. 309 * @param entry the current entry 310 */ 311 public void sendReceiptEmail(Form form, FormEntry entry) 312 { 313 if (form.getReceiptSender().isPresent()) 314 { 315 Optional<String> receiver = getReceiver(entry, form.getReceiptReceiver()); 316 if (receiver.isPresent()) 317 { 318 String sender = form.getReceiptSender().get(); 319 String subject = form.getReceiptSubject().get(); 320 String bodyTxt = form.getReceiptBody().get(); 321 String bodyHTML = bodyTxt.replaceAll("\r?\n", "<br/>"); 322 323 // Always check reading restriction for the receipt email 324 _sendMail(form, entry, subject, bodyHTML, bodyTxt, sender, receiver.get(), true, true); 325 } 326 } 327 } 328 329 /** 330 * Send mail when the form entry if out of the queue 331 * @param form the form 332 * @param entry the form entry 333 */ 334 public void sendOutOfQueueMail(Form form, FormEntry entry) 335 { 336 Optional<String> receiver = getReceiver(entry, form.getQueueMailReceiver()); 337 if (receiver.isPresent()) 338 { 339 String sender = form.getQueueMailSender().get(); 340 String subject = form.getQueueMailSubject().get(); 341 String bodyTxt = form.getQueueMailBody().get(); 342 String bodyHTML = bodyTxt.replaceAll("\r?\n", "<br/>"); 343 344 // Always check reading restriction for the out of queue email 345 _sendMail(form, entry, subject, bodyHTML, bodyTxt, sender, receiver.get(), true, true); 346 } 347 } 348 349 /** 350 * Get a mail pipeline's content. 351 * @param resource the mail resource pipeline 352 * @param entry the user input. 353 * @param additionalParameters the additional parameters 354 * @param withReadingRestriction <code>true</code> to enable reading restriction for form data in the mail 355 * @return the mail content. 356 * @throws IOException if an error occurs. 357 */ 358 public String getMail(String resource, FormEntry entry, Map<String, Object> additionalParameters, boolean withReadingRestriction) throws IOException 359 { 360 Source src = null; 361 Request request = ContextHelper.getRequest(_context); 362 Form form = entry.getForm(); 363 364 try 365 { 366 request.setAttribute(IGNORE_RIGHT_KEY, true); 367 368 String uri = "cocoon:/mail/entry/" + resource; 369 Map<String, Object> parameters = new HashMap<>(); 370 parameters.put("formId", form.getId()); 371 parameters.put("entryId", entry.getId()); 372 parameters.put(READ_RESTRICTION_ENABLE, withReadingRestriction); 373 374 parameters.putAll(additionalParameters); 375 376 src = _sourceResolver.resolveURI(uri, null, parameters); 377 Reader reader = new InputStreamReader(src.getInputStream(), "UTF-8"); 378 return IOUtils.toString(reader); 379 } 380 finally 381 { 382 request.setAttribute(IGNORE_RIGHT_KEY, false); 383 _sourceResolver.release(src); 384 } 385 } 386 387 /** 388 * Get the files of a user input. 389 * @param form The current form 390 * @param entry The current entry 391 * @return the files submitted by the user. 392 */ 393 protected Collection<NamedStream> _getFiles(Form form, FormEntry entry) 394 { 395 return form.getQuestions() 396 .stream() 397 .filter(q -> q.getType() instanceof FileQuestionType) 398 .map(q -> entry.<File>getValue(q.getNameForForm())) 399 .filter(Objects::nonNull) 400 .map(f -> new NamedStream(f.getInputStream(), f.getName(), f.getMimeType())) 401 .collect(Collectors.toList()); 402 } 403 404 private void _sendMail(Form form, FormEntry entry, String subject, String html, String text, String sender, String recipient, boolean addFormInformation, boolean withReadingRestriction) 405 { 406 try 407 { 408 String htmlBody = html; 409 String textBody = text; 410 411 Collection<NamedStream> records = _getFiles(form, entry); 412 try 413 { 414 if (addFormInformation) 415 { 416 if (textBody.contains(_FORM_ENTRY_PATTERN)) 417 { 418 String entry2text = getMail("entry.txt", entry, Map.of(), withReadingRestriction); 419 textBody = StringUtils.replace(textBody, _FORM_ENTRY_PATTERN, entry2text); 420 } 421 422 if (htmlBody.contains(_FORM_ENTRY_PATTERN)) 423 { 424 String entry2html = getMail("entry.html", entry, Map.of(), withReadingRestriction); 425 htmlBody = StringUtils.replace(htmlBody, _FORM_ENTRY_PATTERN, entry2html); 426 } 427 } 428 429 MailBuilder mailBuilder = SendMailHelper.newMail() 430 .withAsync(true) 431 .withSubject(subject) 432 .withHTMLBody(htmlBody) 433 .withTextBody(textBody) 434 .withSender(sender) 435 .withRecipient(recipient); 436 437 if (!records.isEmpty()) 438 { 439 mailBuilder = mailBuilder.withAttachmentsAsStream(records); 440 } 441 mailBuilder.sendMail(); 442 } 443 finally 444 { 445 for (NamedStream record : records) 446 { 447 IOUtils.close(record.inputStream()); 448 } 449 } 450 } 451 catch (MessagingException | IOException e) 452 { 453 getLogger().error("Error sending the mail to " + recipient, e); 454 } 455 } 456}