/*
* Copyright (C) 2005-2014 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see .
*/
package org.alfresco.repo.action.executer;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.action.ParameterDefinitionImpl;
import org.alfresco.repo.admin.SysAdminParams;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.template.DateCompareMethod;
import org.alfresco.repo.template.HasAspectMethod;
import org.alfresco.repo.template.I18NMessageMethod;
import org.alfresco.repo.template.TemplateNode;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.tenant.TenantUtil;
import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.preference.PreferenceService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.TemplateImageResolver;
import org.alfresco.service.cmr.repository.TemplateService;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.AuthorityType;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.util.Pair;
import org.alfresco.util.UrlUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.routines.EmailValidator;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.extensions.surf.util.I18NUtil;
import org.springframework.mail.MailException;
import org.springframework.mail.MailPreparationException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.util.StringUtils;
/**
* Mail action executor implementation.
*
* Note on executing this action as System: it is allowed to execute {@link #NAME mail} actions as system.
* However there is a limitation if you do so. Because the system user is not a normal user and specifically because
* there is no corresponding {@link ContentModel#TYPE_PERSON cm:person} node for system, it is not possible to use
* any reference to that person in the associated email template. Various email templates use a '{@link TemplateNode person}' object
* in the FTL model to access things like first name, last name etc.
* In the case of mail actions sent while running as system, none of these will be available.
*
* @author Roy Wetherall
*/
/*
* mrogers
Thinking over MNT-11488 last night I was considering the requirements for a single message for each action and the possibility of a "bulk mail action." that sends many messages. However it occurs to me that we already have this split in the API (Although i couldn't find any documentation which has left the expected functionality confused and the implementation adrift.) So I'm changing my guidance.
There is a need to document (javadoc) the interface so we tie down expected behaviour. And then refactor since the code is confused.
Here's my thinking:
If the to_many parameter is set then it should be a "bulk" email which sends many individual messages.
If the to_many parameter is incompatible with the TO parameter which will be is ignored or will throw an Illegal Argument Exception.
If the to_many parameter is incompatible with the proposed CC parameter which will be is ignored or will throw an Illegal Argument Exception.
If the to_many parameter is incompatible with the proposed BCC parameter which will be is ignored or will throw an Illegal Argument Exception.
If the to_many parameter is not specified then it results in a single message regardless of other settings.
We should probably add CC and BCC parameters which can be specified alonside TO
We should make TO multi-valued
If we allow multiple TO then the single message is only in the locale appropriate to the first TO.
We should allow a list of USER authority name or a email address in TO or TO_MANY.
We should probably also allow GROUP authority names in TO and TO_MANY however for now lets just make sure it works with TO_MANY
Other implications follow through from this big switch approach and affect the implementation.
For example should we allow PARAM_SEND_AFTER_COMMIT for bulk email (since it makes implementation hard.)
Likewise the template handling with locale is clarified. For bulk its an individual message so it has an individual locale.
And with a bulk email we should probably carry on sending even after errors and then have some sort of bulk report of errors.
TEMPLATES can be used with either TO and TO_MANY
*/
public class MailActionExecuter extends ActionExecuterAbstractBase
implements InitializingBean, TestModeable
{
private static Log logger = LogFactory.getLog(MailActionExecuter.class);
/**
* Action executor constants
*/
public static final String NAME = "mail";
public static final String PARAM_LOCALE = "locale";
public static final String PARAM_TO = "to";
public static final String PARAM_TO_MANY = "to_many";
public static final String PARAM_SUBJECT = "subject";
public static final String PARAM_SUBJECT_PARAMS = "subjectParams";
public static final String PARAM_TEXT = "text";
public static final String PARAM_HTML = "html";
public static final String PARAM_FROM = "from";
public static final String PARAM_FROM_PERSONAL_NAME = "fromPersonalName";
public static final String PARAM_TEMPLATE = "template";
public static final String PARAM_TEMPLATE_MODEL = "template_model";
public static final String PARAM_IGNORE_SEND_FAILURE = "ignore_send_failure";
public static final String PARAM_SEND_AFTER_COMMIT = "send_after_commit";
/**
* From address
*/
private static final String FROM_ADDRESS = "alfresco@alfresco.org";
/**
* The java mail sender
*/
private JavaMailSender mailService;
/**
* The Template service
*/
private TemplateService templateService;
/**
* The Person service
*/
private PersonService personService;
/**
* The Authentication service
*/
private AuthenticationService authService;
/**
* The Node Service
*/
private NodeService nodeService;
/**
* The Authority Service
*/
private AuthorityService authorityService;
/**
* The Service registry
*/
private ServiceRegistry serviceRegistry;
/**
* System Administration parameters, including URL information
*/
private SysAdminParams sysAdminParams;
/**
* The Preference Service
*/
private PreferenceService preferenceService;
/**
* The Tenant Service
*/
private TenantService tenantService;
/**
* Mail header encoding scheme
*/
private String headerEncoding = null;
/**
* Default from address
*/
private String fromDefaultAddress = null;
/**
* Is the from field enabled? Or must we always use the default address.
*/
private boolean fromEnabled = true;
private boolean sendTestMessage = false;
private String testMessageTo = null;
private String testMessageSubject = "Test message";
private String testMessageText = "This is a test message.";
private boolean validateAddresses = true;
/**
* Test mode prevents email messages from being sent.
* It is used when unit testing when we don't actually want to send out email messages.
*
* MER 20/11/2009 This is a quick and dirty fix. It should be replaced by being
* "mocked out" or some other better way of running the unit tests.
*/
private boolean testMode = false;
private MimeMessage lastTestMessage;
private int testSentCount;
private TemplateImageResolver imageResolver;
/**
* @param javaMailSender the java mail sender
*/
public void setMailService(JavaMailSender javaMailSender)
{
this.mailService = javaMailSender;
}
/**
* @param templateService the TemplateService
*/
public void setTemplateService(TemplateService templateService)
{
this.templateService = templateService;
}
/**
* @param personService the PersonService
*/
public void setPersonService(PersonService personService)
{
this.personService = personService;
}
public void setPreferenceService(PreferenceService preferenceService)
{
this.preferenceService = preferenceService;
}
/**
* @param authService the AuthenticationService
*/
public void setAuthenticationService(AuthenticationService authService)
{
this.authService = authService;
}
/**
* @param serviceRegistry the ServiceRegistry
*/
public void setServiceRegistry(ServiceRegistry serviceRegistry)
{
this.serviceRegistry = serviceRegistry;
}
/**
* @param authorityService the AuthorityService
*/
public void setAuthorityService(AuthorityService authorityService)
{
this.authorityService = authorityService;
}
/**
* @param nodeService the NodeService to set.
*/
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
/**
* @param tenantService the TenantService to set.
*/
public void setTenantService(TenantService tenantService)
{
this.tenantService = tenantService;
}
/**
* @param headerEncoding The mail header encoding to set.
*/
public void setHeaderEncoding(String headerEncoding)
{
this.headerEncoding = headerEncoding;
}
/**
* @param fromAddress The default mail address.
*/
public void setFromAddress(String fromAddress)
{
this.fromDefaultAddress = fromAddress;
}
public void setSysAdminParams(SysAdminParams sysAdminParams)
{
this.sysAdminParams = sysAdminParams;
}
public void setImageResolver(TemplateImageResolver imageResolver)
{
this.imageResolver = imageResolver;
}
public void setTestMessageTo(String testMessageTo)
{
this.testMessageTo = testMessageTo;
}
public String getTestMessageTo()
{
return testMessageTo;
}
public void setTestMessageSubject(String testMessageSubject)
{
this.testMessageSubject = testMessageSubject;
}
public void setTestMessageText(String testMessageText)
{
this.testMessageText = testMessageText;
}
public void setSendTestMessage(boolean sendTestMessage)
{
this.sendTestMessage = sendTestMessage;
}
/**
* This stores an email address which, if it is set, overrides ALL email recipients sent from
* this class. It is intended for dev/test usage only !!
*/
private String testModeRecipient;
/**
* Send a test message
*
* @return true, message sent
* @throws AlfrescoRuntimeException
*/
public boolean sendTestMessage()
{
if(testMessageTo == null || testMessageTo.length() == 0)
{
throw new AlfrescoRuntimeException("email.outbound.err.test.no.to");
}
if(testMessageSubject == null || testMessageSubject.length() == 0)
{
throw new AlfrescoRuntimeException("email.outbound.err.test.no.subject");
}
if(testMessageText == null || testMessageText.length() == 0)
{
throw new AlfrescoRuntimeException("email.outbound.err.test.no.text");
}
Map params = new HashMap();
params.put(PARAM_TO, testMessageTo);
params.put(PARAM_SUBJECT, testMessageSubject);
params.put(PARAM_TEXT, testMessageText);
Action ruleAction = serviceRegistry.getActionService().createAction(NAME, params);
MimeMessageHelper message = prepareEmail(ruleAction, null,
new Pair(testMessageTo, getLocaleForUser(testMessageTo)), getFrom(ruleAction));
try
{
mailService.send(message.getMimeMessage());
onSend();
}
catch (MailException me)
{
onFail();
StringBuffer txt = new StringBuffer();
txt.append(me.getClass().getName() + ", " + me.getMessage());
Throwable cause = me.getCause();
while (cause != null)
{
txt.append(", ");
txt.append(cause.getClass().getName() + ", " + cause.getMessage());
cause = cause.getCause();
}
Object[] args = {testMessageTo, txt.toString()};
throw new AlfrescoRuntimeException("email.outbound.err.send.failed", args, me);
}
return true;
}
public void setTestModeRecipient(String testModeRecipient)
{
this.testModeRecipient = testModeRecipient;
}
public void setValidateAddresses(boolean validateAddresses)
{
this.validateAddresses = validateAddresses;
}
@Override
public void init()
{
if(logger.isDebugEnabled())
{
logger.debug("Init called, testMessageTo=" + testMessageTo);
}
numberSuccessfulSends.set(0);
numberFailedSends.set(0);
super.init();
if (sendTestMessage && testMessageTo != null)
{
AuthenticationUtil.runAs(new RunAsWork