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