<commit_before>package hudson.plugins.emailext;

import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import hudson.model.Result;
import hudson.model.User;
import hudson.plugins.emailext.plugins.ContentBuilder;
import hudson.plugins.emailext.plugins.EmailTrigger;
import hudson.plugins.emailext.plugins.EmailTriggerDescriptor;
import hudson.scm.ChangeLogSet.Entry;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.MailMessageIdAction;
import hudson.tasks.Mailer;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import hudson.util.FormValidation;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;

import javax.mail.Address;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * {@link Publisher} that sends notification e-mail.
 *
 * @author <EMAIL>
 *
 */
public class ExtendedEmailPublisher extends Notifier {
	
	private static final Logger LOGGER = Logger.getLogger(Mailer.class.getName());

	public static final String COMMA_SEPARATED_SPLIT_REGEXP = "[,\\s]+";

	private static final Map<String,EmailTriggerDescriptor> EMAIL_TRIGGER_TYPE_MAP = new HashMap<String,EmailTriggerDescriptor>();
	
	public static final String DEFAULT_SUBJECT_TEXT = "$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS!";
	public static final String DEFAULT_BODY_TEXT = "$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS:\n\n" +
		"Check console output at $BUILD_URL to view the results.";
	
	public static final String PROJECT_DEFAULT_SUBJECT_TEXT = "$PROJECT_DEFAULT_SUBJECT";
	public static final String PROJECT_DEFAULT_BODY_TEXT = "$PROJECT_DEFAULT_CONTENT";

    public static final String CHARSET = "utf-8";
	
	public static void addEmailTriggerType(EmailTriggerDescriptor triggerType) throws EmailExtException {
		if(EMAIL_TRIGGER_TYPE_MAP.containsKey(triggerType.getMailerId()))
			throw new EmailExtException("An email trigger type with name " +
					triggerType.getTriggerName() + " was already added.");
		EMAIL_TRIGGER_TYPE_MAP.put(triggerType.getMailerId(), triggerType);
	}
	
	public static void removeEmailTriggerType(EmailTriggerDescriptor triggerType) {
		if(EMAIL_TRIGGER_TYPE_MAP.containsKey(triggerType.getMailerId()))
			EMAIL_TRIGGER_TYPE_MAP.remove(triggerType.getMailerId());
	}
	
	public static EmailTriggerDescriptor getEmailTriggerType(String mailerId) {
		return EMAIL_TRIGGER_TYPE_MAP.get(mailerId);
	}
	
	public static Collection<EmailTriggerDescriptor> getEmailTriggers() {
		return EMAIL_TRIGGER_TYPE_MAP.values();
	}
	
	public static Collection<String> getEmailTriggerNames() {
		return EMAIL_TRIGGER_TYPE_MAP.keySet();
	}
	
	public static List<EmailTrigger> getTriggersForNonConfiguredInstance() {
		List<EmailTrigger> retList = new ArrayList<EmailTrigger>();
		for(String mailerId : EMAIL_TRIGGER_TYPE_MAP.keySet()) {
			retList.add(EMAIL_TRIGGER_TYPE_MAP.get(mailerId).getNewInstance(null));
		}
		return retList;
	}
	
	/**
	 * A comma-separated list of email recipient that will be used for every trigger.
	 */
	public String recipientList;

	/** This is the list of email triggers that the project has configured */
	private List<EmailTrigger> configuredTriggers = new ArrayList<EmailTrigger>();

	/**
	 * The contentType of the emails for this project (text/html, text/plain, etc).
	 */
	public String contentType;

	/**
	 * The default subject of the emails for this project.  ($PROJECT_DEFAULT_SUBJECT)
	 */
	public String defaultSubject;

	/**
	 * The default body of the emails for this project.  ($PROJECT_DEFAULT_BODY)
	 */
	public String defaultContent;

	/**
	 * Get the list of configured email triggers for this project.
	 */
	public List<EmailTrigger> getConfiguredTriggers() {
		if(configuredTriggers == null)
			configuredTriggers = new ArrayList<EmailTrigger>();
		return configuredTriggers;
	}

	/**
	 * Get the list of non-configured email triggers for this project.
	 */
	public List<EmailTrigger> getNonConfiguredTriggers() {
		List<EmailTrigger> confTriggers = getConfiguredTriggers();
		
		List<EmailTrigger> retList = new ArrayList<EmailTrigger>();
		for(String mailerId : EMAIL_TRIGGER_TYPE_MAP.keySet()) {
			boolean contains = false;
			for(EmailTrigger trigger : confTriggers) {
				if(trigger.getDescriptor().getMailerId().equals(mailerId)) {
					contains = true;
					break;
				}
			}
			if(!contains) {
				retList.add(EMAIL_TRIGGER_TYPE_MAP.get(mailerId).getNewInstance(null));
			}
		}
		return retList;
	}

	/**
	 * Return true if the project has been configured, otherwise returns false
	 */
	public boolean isConfigured() {
		return !getConfiguredTriggers().isEmpty();
	}
	
	/**
	 * Return true if the project has been configured, otherwise returns false
	 */
	public boolean getConfigured() {
		return isConfigured();
	}

	@Override
	public boolean prebuild(AbstractBuild<?,?> build, BuildListener listener) {
		return _perform(build,listener,true);
	}

	@Override
	public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
		return _perform(build,listener,false);
	}
	
	private boolean _perform(AbstractBuild<?,?> build, BuildListener listener, boolean forPreBuild) {
	   	boolean emailTriggered = false;
		
	   	Map<String,EmailTrigger> triggered = new HashMap<String, EmailTrigger>();
	   	
		for(EmailTrigger trigger : configuredTriggers) {
			if(trigger.isPreBuild() == forPreBuild && trigger.trigger(build)) {
				String tName = trigger.getDescriptor().getTriggerName();
				triggered.put(tName,trigger);
				listener.getLogger().println("Email was triggered for: " + tName);
				emailTriggered = true;
			}
		}
		
		//Go through and remove triggers that are replaced by others
		List<String> replacedTriggers = new ArrayList<String>();
		
		for(String triggerName : triggered.keySet()) {
			replacedTriggers.addAll(triggered.get(triggerName).getDescriptor().getTriggerReplaceList());
		}
		for(String triggerName : replacedTriggers) {
			triggered.remove(triggerName);
			listener.getLogger().println("Trigger " + triggerName + " was overridden by another trigger and will not send an email.");
		}
		
		if(emailTriggered && triggered.isEmpty()) {
			listener.getLogger().println("There is a circular trigger replacement with the email triggers.  No email is sent.");
			return false;
		}
		else if(triggered.isEmpty()) {
			listener.getLogger().println("No emails were triggered.");
			return true;
		}
		
		for(String triggerName :triggered.keySet()) {
			listener.getLogger().println("Sending email for trigger: " + triggerName);
			sendMail(triggered.get(triggerName).getEmail(), build, listener);
		}
		
		return true;
	}
	
	private boolean sendMail(EmailType mailType, AbstractBuild<?,?> build, BuildListener listener) {
		try {
			MimeMessage msg = createMail(mailType, build, listener);
			Address[] allRecipients = msg.getAllRecipients();
			if (allRecipients != null) {
				StringBuilder buf = new StringBuilder("Sending email to:");
				for (Address a : allRecipients)
					buf.append(' ').append(a);
				listener.getLogger().println(buf);
				Transport.send(msg);
				if (build.getAction(MailMessageIdAction.class) == null)
					build.addAction(new MailMessageIdAction(msg.getMessageID()));
				return true;
			} else {
				listener.getLogger().println("An attempt to send an e-mail"
					+ " to empty list of recipients, ignored.");
			}
		} catch(Exception e) {
			LOGGER.log(Level.WARNING, "Could not send email.",e);
			e.printStackTrace(listener.error("Could not send email as a part of the post-build publishers."));
		}
		
		return false;
	}

	private MimeMessage createMail(EmailType type, AbstractBuild<?,?> build, BuildListener listener) throws MessagingException, IOException, InterruptedException {
		boolean overrideGlobalSettings = ExtendedEmailPublisher.DESCRIPTOR.getOverrideGlobalSettings();

		MimeMessage msg;
		
		// If not overriding global settings, use the Mailer class to create a session and set the from address
		// Else we'll do it ourselves
		if (!overrideGlobalSettings) {
			msg = new MimeMessage(Mailer.descriptor().createSession());
			msg.setFrom(new InternetAddress(Mailer.descriptor().getAdminAddress()));
		} else {
			msg = new MimeMessage(ExtendedEmailPublisher.DESCRIPTOR.createSession());
			msg.setFrom(new InternetAddress(ExtendedEmailPublisher.DESCRIPTOR.getAdminAddress()));
		}

        // Set the contents of the email

        msg.setSentDate(new Date());

        setSubject( type, build, msg );

        setContent( type, build, msg );

        EnvVars env = build.getEnvironment(listener);

		// Get the recipients from the global list of addresses
		List<InternetAddress> recipientAddresses = new ArrayList<InternetAddress>();
		if (type.getSendToRecipientList()) {
            addAddressesFromRecipientList(recipientAddresses, recipientList, env, listener);
		}
		// Get the list of developers who made changes between this build and the last
		// if this mail type is configured that way
		if (type.getSendToDevelopers()) {
			Set<User> users;
			if (type.getIncludeCulprits()) {
				users = build.getCulprits();
			} else {
				users = new HashSet<User>();
				for (Entry change : build.getChangeSet()) {
					users.add(change.getAuthor());
				}
			}
			for (User user : users) {
				String adrs = user.getProperty(Mailer.UserProperty.class).getAddress();
				if (adrs != null)
					addAddressesFromRecipientList(recipientAddresses, adrs, env, listener);
				else {
					listener.getLogger().println("Failed to send e-mail to " + user.getFullName() + " because no e-mail address is known, and no default e-mail domain is configured");
				}
			}
		}
		//Get the list of recipients that are uniquely specified for this type of email
		if (type.getRecipientList() != null && type.getRecipientList().trim().length() > 0) {
            addAddressesFromRecipientList( recipientAddresses, type.getRecipientList(), env, listener );
		}
		
		msg.setRecipients(Message.RecipientType.TO, recipientAddresses.toArray(new InternetAddress[recipientAddresses.size()]));

		AbstractBuild<?,?> pb = build.getPreviousBuild();
		if (pb!=null) {
			// Send mails as replies until next successful build
			MailMessageIdAction b = pb.getAction(MailMessageIdAction.class);
			if(b!=null && pb.getResult()!=Result.SUCCESS) {
				msg.setHeader("In-Reply-To",b.messageId);
				msg.setHeader("References",b.messageId);
			}
		}

		return msg;
	}

    private void setSubject( final EmailType type, final AbstractBuild<?, ?> build, MimeMessage msg )
        throws MessagingException
    {
        String subject = new ContentBuilder().transformText(type.getSubject(), this, type, build);
        msg.setSubject(subject, CHARSET);
    }

    private void setContent( final EmailType type, final AbstractBuild<?, ?> build, MimeMessage msg )
        throws MessagingException
    {
        final String text = new ContentBuilder().transformText(type.getBody(), this, type, build);

        String messageContentType = contentType;
        // contentType is null if the project was not reconfigured after upgrading.
        if (messageContentType == null || "default".equals(messageContentType)) {
            messageContentType = DESCRIPTOR.getDefaultContentType();
            // The defaultContentType is null if the main Hudson configuration
            // was not reconfigured after upgrading.
            if (messageContentType == null) {
                messageContentType = "text/plain";
            }
        }
        messageContentType += "; charset=" + CHARSET;

        msg.setContent(text, messageContentType);
    }

    private static void addAddressesFromRecipientList(List<InternetAddress> addresses, String recipientList,
                                                      EnvVars envVars, BuildListener listener) {
        try {
            List<InternetAddress> internetAddresses = new EmailRecepientUtils().convertRecipientString( recipientList, envVars );
			addresses.addAll(internetAddresses);
		} catch(AddressException ae) {
			LOGGER.log(Level.WARNING, "Could not create email address.", ae);
			listener.getLogger().println("Failed to create e-mail address for " + ae.getRef());
		}
	}
	
	@Override
	public boolean needsToRunAfterFinalized() {
		return true;
	}

	public BuildStepMonitor getRequiredMonitorService() {
		return BuildStepMonitor.BUILD;
	}

	@Override
	public BuildStepDescriptor<Publisher> getDescriptor() {
		return DESCRIPTOR;
	}
	
	@Extension
	public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

	/*
	 * These settings are the settings that are global.
	 */
	public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
		/**
		 * The default e-mail address suffix appended to the user name found from changelog,
		 * to send e-mails. Null if not configured.
		 */
		private String defaultSuffix;

		/**
		 * Hudson's own URL, to put into the e-mail.
		 */
		private String hudsonUrl;

		/**
		 * If non-null, use SMTP-AUTH with these information.
		 */
		private String smtpAuthPassword,smtpAuthUsername;

		/**
		 * The e-mail address that Hudson puts to "From:" field in outgoing e-mails.
		 * Null if not configured.
		 */
		private String adminAddress;

		/**
		 * The SMTP server to use for sending e-mail. Null for default to the environment,
		 * which is usually <tt>localhost</tt>.
		 */
		private String smtpHost;
		
		/**
		 * If true use SSL on port 465 (standard SMTPS) unless <code>smtpPort</code> is set.
		 */
		private boolean useSsl;
		
		/**
		 * The SMTP port to use for sending e-mail. Null for default to the environment,
		 * which is usually <tt>25</tt>.
		 */
		private String smtpPort;
		
		/**
		 * This is a global default content type (mime type) for emails.
		 */
		private String defaultContentType;

		/**
		 * This is a global default subject line for sending emails.
		 */
		private String defaultSubject;
		
		/**
		 * This is a global default body for sending emails.
		 */
		private String defaultBody;

		private boolean overrideGlobalSettings;
		
		@Override
		public String getDisplayName() {
			return "Editable Email Notification";
		}
		
		public String getAdminAddress() {
			String v = adminAddress;
			if (v == null) {
				v = "address not configured yet <nobody>";
			}
			return v;
		}

		public String getDefaultSuffix() {
			return defaultSuffix;
		}
		
		/** JavaMail session. */
		public Session createSession() {
			/*
			 * 				Mailer.DescriptorImpl desc = Mailer.descriptor();
				smtpHost = nullify(desc.getSmtpServer());
				adminAddress = desc.getAdminAddress();
				defaultSuffix = nullify(desc.getDefaultSuffix());
				hudsonUrl = desc.getUrl();
				smtpAuthUsername = desc.getSmtpAuthUserName();
				smtpAuthPassword = desc.getSmtpAuthPassword();
				useSsl = desc.getUseSsl();
				smtpPort = desc.getSmtpPort();
			 */
			Properties props = new Properties(System.getProperties());
			if(smtpHost!=null)
				props.put("mail.smtp.host",smtpHost);
			if (smtpPort!=null) {
				props.put("mail.smtp.port", smtpPort);
			}
			if (useSsl) {
				/* This allows the user to override settings by setting system properties but
				 * also allows us to use the default SMTPs port of 465 if no port is already set.
				 * It would be cleaner to use smtps, but that's done by calling session.getTransport()...
				 * and thats done in mail sender, and it would be a bit of a hack to get it all to
				 * coordinate, and we can make it work through setting mail.smtp properties.
				 */
				if (props.getProperty("mail.smtp.socketFactory.port") == null) {
					String port = smtpPort==null?"465":smtpPort;
					props.put("mail.smtp.port", port);
					props.put("mail.smtp.socketFactory.port", port);
				}
				if (props.getProperty("mail.smtp.socketFactory.class") == null) {
					props.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory");
				}
				props.put("mail.smtp.socketFactory.fallback", "false");
			}
			if (smtpAuthUsername!=null)
				props.put("mail.smtp.auth","true");
			return Session.getInstance(props,getAuthenticator());
		}
		
		private Authenticator getAuthenticator() {
			final String un = getSmtpAuthUsername();
			if (un == null) return null;
			return new Authenticator() {
				@Override
				protected PasswordAuthentication getPasswordAuthentication() {
					return new PasswordAuthentication(getSmtpAuthUsername(), getSmtpAuthPassword());
				}
			};
		}

		public String getHudsonUrl() {
			if (hudsonUrl == null) {
				return Hudson.getInstance().getRootUrl();
			}
			return hudsonUrl;
		}

		public String getSmtpServer() {
			return smtpHost;
		}

		public String getSmtpAuthUsername() {
			return smtpAuthUsername;
		}
		
		public String getSmtpAuthPassword() {
			return smtpAuthPassword;
		}

		public boolean getUseSsl() {
			return useSsl;
		}
		
		public String getSmtpPort() {
			return smtpPort;
		}
		
		public String getDefaultContentType() {
			return defaultContentType;
		}

        public String getDefaultSubject() {
			return defaultSubject;
		}
		
		public String getDefaultBody() {
			return defaultBody;
		}
		
		public boolean getOverrideGlobalSettings() {
			return overrideGlobalSettings;
		}

		public boolean isApplicable(Class<? extends AbstractProject> jobType) {
			return true;
                }

		@Override
		public Publisher newInstance(StaplerRequest req, JSONObject formData) throws hudson.model.Descriptor.FormException {
			// Save the recipient lists
			String listRecipients = formData.getString("recipientlist_recipients");
			
			// Save configuration for each trigger type
			ExtendedEmailPublisher m = new ExtendedEmailPublisher();
			m.recipientList = listRecipients;
			m.contentType = formData.getString("project_content_type");
			m.defaultSubject = formData.getString("project_default_subject");
			m.defaultContent = formData.getString("project_default_content");
			m.configuredTriggers = new ArrayList<EmailTrigger>();
			
			// Create a new email trigger for each one that is configured
			for (String mailerId : EMAIL_TRIGGER_TYPE_MAP.keySet()) {
				if("true".equalsIgnoreCase(formData.optString("mailer_" + mailerId + "_configured"))) {
					EmailType type = createMailType(formData, mailerId);
					EmailTrigger trigger = EMAIL_TRIGGER_TYPE_MAP.get(mailerId).getNewInstance(type);
					m.configuredTriggers.add(trigger);
				}
			}
			
			return m;
		}
		
		private EmailType createMailType(JSONObject formData, String mailType) {
			EmailType m = new EmailType();
			String prefix = "mailer_" + mailType + '_';
			m.setSubject(formData.getString(prefix + "subject"));
			m.setBody(formData.getString(prefix + "body"));
			m.setRecipientList(formData.getString(prefix + "recipientList"));
			m.setSendToRecipientList(formData.optBoolean(prefix + "sendToRecipientList"));
			m.setSendToDevelopers(formData.optBoolean(prefix + "sendToDevelopers"));
			m.setIncludeCulprits(formData.optBoolean(prefix + "includeCulprits"));
			return m;
		}
		
		public DescriptorImpl() {
			super(ExtendedEmailPublisher.class);
			load();
			if (defaultBody == null && defaultSubject == null) {
				defaultBody = DEFAULT_BODY_TEXT;
				defaultSubject = DEFAULT_SUBJECT_TEXT;
			}
		}

		@Override
		public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
			// Most of this stuff is the same as the built-in email publisher

			// Configure the smtp server
			smtpHost = nullify(req.getParameter("ext_mailer_smtp_server"));
			adminAddress = req.getParameter("ext_mailer_admin_address");
			defaultSuffix = nullify(req.getParameter("ext_mailer_default_suffix"));
			
			// Specify the url to this hudson instance
			String url = nullify(req.getParameter("ext_mailer_hudson_url"));
			if (url != null && !url.endsWith("/")) {
				url += '/';
			}
			if (url == null) {
				url = Hudson.getInstance().getRootUrl();
			}
			hudsonUrl = url;

			// specify authentication information
			if (req.getParameter("extmailer.useSMTPAuth") != null) {
				smtpAuthUsername = nullify(req.getParameter("extmailer.SMTPAuth.userName"));
				smtpAuthPassword = nullify(req.getParameter("extmailer.SMTPAuth.password"));
			} else {
				smtpAuthUsername = smtpAuthPassword = null;
			}
			
			// specify if the mail server uses ssl for authentication
			useSsl = req.getParameter("ext_mailer_smtp_use_ssl") != null;
			
			// specify custom smtp port
			smtpPort = nullify(req.getParameter("ext_mailer_smtp_port"));
			
			defaultContentType = nullify(req.getParameter("ext_mailer_default_content_type"));

			// Allow global defaults to be set for the subject and body of the email
			defaultSubject = nullify(req.getParameter("ext_mailer_default_subject"));
			defaultBody = nullify(req.getParameter("ext_mailer_default_body"));
			
			overrideGlobalSettings = req.getParameter("ext_mailer_override_global_settings") != null;
			
			save();
			return super.configure(req, formData);
		}
		
		private String nullify(String v) {
			if(v!=null && v.length()==0)
				v=null;
			return v;
		}
		
		@Override
		public String getHelpFile() {
			return "/plugin/email-ext/help/main.html";
		}
		
		public FormValidation doAddressCheck(
				@QueryParameter final String value) throws IOException, ServletException {
			try {
				new InternetAddress(value);
				return FormValidation.ok();
			} catch (AddressException e) {
				return FormValidation.error(e.getMessage());
			}
		}
		
		public FormValidation doRecipientListRecipientsCheck(
				@QueryParameter final String value) throws IOException, ServletException {
			return new EmailRecepientUtils().validateFormRecipientList( value );
		}
		
	}



}
<commit_msg>[FIXED HUDSON-7057] Invalid Illegal email address exception. Also improved error message when validating email recipient list.<commit_after>package hudson.plugins.emailext;

import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import hudson.model.Result;
import hudson.model.User;
import hudson.plugins.emailext.plugins.ContentBuilder;
import hudson.plugins.emailext.plugins.EmailTrigger;
import hudson.plugins.emailext.plugins.EmailTriggerDescriptor;
import hudson.scm.ChangeLogSet.Entry;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.MailMessageIdAction;
import hudson.tasks.Mailer;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import hudson.util.FormValidation;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;

import javax.mail.Address;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * {@link Publisher} that sends notification e-mail.
 *
 * @author <EMAIL>
 *
 */
public class ExtendedEmailPublisher extends Notifier {
	
	private static final Logger LOGGER = Logger.getLogger(Mailer.class.getName());

	private static final Map<String,EmailTriggerDescriptor> EMAIL_TRIGGER_TYPE_MAP = new HashMap<String,EmailTriggerDescriptor>();
	
	public static final String DEFAULT_SUBJECT_TEXT = "$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS!";
	public static final String DEFAULT_BODY_TEXT = "$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS:\n\n" +
		"Check console output at $BUILD_URL to view the results.";
	
	public static final String PROJECT_DEFAULT_SUBJECT_TEXT = "$PROJECT_DEFAULT_SUBJECT";
	public static final String PROJECT_DEFAULT_BODY_TEXT = "$PROJECT_DEFAULT_CONTENT";

    public static final String CHARSET = "utf-8";
	
	public static void addEmailTriggerType(EmailTriggerDescriptor triggerType) throws EmailExtException {
		if(EMAIL_TRIGGER_TYPE_MAP.containsKey(triggerType.getMailerId()))
			throw new EmailExtException("An email trigger type with name " +
					triggerType.getTriggerName() + " was already added.");
		EMAIL_TRIGGER_TYPE_MAP.put(triggerType.getMailerId(), triggerType);
	}
	
	public static void removeEmailTriggerType(EmailTriggerDescriptor triggerType) {
		if(EMAIL_TRIGGER_TYPE_MAP.containsKey(triggerType.getMailerId()))
			EMAIL_TRIGGER_TYPE_MAP.remove(triggerType.getMailerId());
	}
	
	public static EmailTriggerDescriptor getEmailTriggerType(String mailerId) {
		return EMAIL_TRIGGER_TYPE_MAP.get(mailerId);
	}
	
	public static Collection<EmailTriggerDescriptor> getEmailTriggers() {
		return EMAIL_TRIGGER_TYPE_MAP.values();
	}
	
	public static Collection<String> getEmailTriggerNames() {
		return EMAIL_TRIGGER_TYPE_MAP.keySet();
	}
	
	public static List<EmailTrigger> getTriggersForNonConfiguredInstance() {
		List<EmailTrigger> retList = new ArrayList<EmailTrigger>();
		for(String mailerId : EMAIL_TRIGGER_TYPE_MAP.keySet()) {
			retList.add(EMAIL_TRIGGER_TYPE_MAP.get(mailerId).getNewInstance(null));
		}
		return retList;
	}
	
	/**
	 * A comma-separated list of email recipient that will be used for every trigger.
	 */
	public String recipientList;

	/** This is the list of email triggers that the project has configured */
	private List<EmailTrigger> configuredTriggers = new ArrayList<EmailTrigger>();

	/**
	 * The contentType of the emails for this project (text/html, text/plain, etc).
	 */
	public String contentType;

	/**
	 * The default subject of the emails for this project.  ($PROJECT_DEFAULT_SUBJECT)
	 */
	public String defaultSubject;

	/**
	 * The default body of the emails for this project.  ($PROJECT_DEFAULT_BODY)
	 */
	public String defaultContent;

	/**
	 * Get the list of configured email triggers for this project.
	 */
	public List<EmailTrigger> getConfiguredTriggers() {
		if(configuredTriggers == null)
			configuredTriggers = new ArrayList<EmailTrigger>();
		return configuredTriggers;
	}

	/**
	 * Get the list of non-configured email triggers for this project.
	 */
	public List<EmailTrigger> getNonConfiguredTriggers() {
		List<EmailTrigger> confTriggers = getConfiguredTriggers();
		
		List<EmailTrigger> retList = new ArrayList<EmailTrigger>();
		for(String mailerId : EMAIL_TRIGGER_TYPE_MAP.keySet()) {
			boolean contains = false;
			for(EmailTrigger trigger : confTriggers) {
				if(trigger.getDescriptor().getMailerId().equals(mailerId)) {
					contains = true;
					break;
				}
			}
			if(!contains) {
				retList.add(EMAIL_TRIGGER_TYPE_MAP.get(mailerId).getNewInstance(null));
			}
		}
		return retList;
	}

	/**
	 * Return true if the project has been configured, otherwise returns false
	 */
	public boolean isConfigured() {
		return !getConfiguredTriggers().isEmpty();
	}
	
	/**
	 * Return true if the project has been configured, otherwise returns false
	 */
	public boolean getConfigured() {
		return isConfigured();
	}

	@Override
	public boolean prebuild(AbstractBuild<?,?> build, BuildListener listener) {
		return _perform(build,listener,true);
	}

	@Override
	public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
		return _perform(build,listener,false);
	}
	
	private boolean _perform(AbstractBuild<?,?> build, BuildListener listener, boolean forPreBuild) {
	   	boolean emailTriggered = false;
		
	   	Map<String,EmailTrigger> triggered = new HashMap<String, EmailTrigger>();
	   	
		for(EmailTrigger trigger : configuredTriggers) {
			if(trigger.isPreBuild() == forPreBuild && trigger.trigger(build)) {
				String tName = trigger.getDescriptor().getTriggerName();
				triggered.put(tName,trigger);
				listener.getLogger().println("Email was triggered for: " + tName);
				emailTriggered = true;
			}
		}
		
		//Go through and remove triggers that are replaced by others
		List<String> replacedTriggers = new ArrayList<String>();
		
		for(String triggerName : triggered.keySet()) {
			replacedTriggers.addAll(triggered.get(triggerName).getDescriptor().getTriggerReplaceList());
		}
		for(String triggerName : replacedTriggers) {
			triggered.remove(triggerName);
			listener.getLogger().println("Trigger " + triggerName + " was overridden by another trigger and will not send an email.");
		}
		
		if(emailTriggered && triggered.isEmpty()) {
			listener.getLogger().println("There is a circular trigger replacement with the email triggers.  No email is sent.");
			return false;
		}
		else if(triggered.isEmpty()) {
			listener.getLogger().println("No emails were triggered.");
			return true;
		}
		
		for(String triggerName :triggered.keySet()) {
			listener.getLogger().println("Sending email for trigger: " + triggerName);
			sendMail(triggered.get(triggerName).getEmail(), build, listener);
		}
		
		return true;
	}
	
	private boolean sendMail(EmailType mailType, AbstractBuild<?,?> build, BuildListener listener) {
		try {
			MimeMessage msg = createMail(mailType, build, listener);
			Address[] allRecipients = msg.getAllRecipients();
			if (allRecipients != null) {
				StringBuilder buf = new StringBuilder("Sending email to:");
				for (Address a : allRecipients)
					buf.append(' ').append(a);
				listener.getLogger().println(buf);
				Transport.send(msg);
				if (build.getAction(MailMessageIdAction.class) == null)
					build.addAction(new MailMessageIdAction(msg.getMessageID()));
				return true;
			} else {
				listener.getLogger().println("An attempt to send an e-mail"
					+ " to empty list of recipients, ignored.");
			}
		} catch(Exception e) {
			LOGGER.log(Level.WARNING, "Could not send email.",e);
			e.printStackTrace(listener.error("Could not send email as a part of the post-build publishers."));
		}
		
		return false;
	}

	private MimeMessage createMail(EmailType type, AbstractBuild<?,?> build, BuildListener listener) throws MessagingException, IOException, InterruptedException {
		boolean overrideGlobalSettings = ExtendedEmailPublisher.DESCRIPTOR.getOverrideGlobalSettings();

		MimeMessage msg;
		
		// If not overriding global settings, use the Mailer class to create a session and set the from address
		// Else we'll do it ourselves
		if (!overrideGlobalSettings) {
			msg = new MimeMessage(Mailer.descriptor().createSession());
			msg.setFrom(new InternetAddress(Mailer.descriptor().getAdminAddress()));
		} else {
			msg = new MimeMessage(ExtendedEmailPublisher.DESCRIPTOR.createSession());
			msg.setFrom(new InternetAddress(ExtendedEmailPublisher.DESCRIPTOR.getAdminAddress()));
		}

        // Set the contents of the email

        msg.setSentDate(new Date());

        setSubject( type, build, msg );

        setContent( type, build, msg );

        EnvVars env = build.getEnvironment(listener);

		// Get the recipients from the global list of addresses
		List<InternetAddress> recipientAddresses = new ArrayList<InternetAddress>();
		if (type.getSendToRecipientList()) {
            addAddressesFromRecipientList(recipientAddresses, recipientList, env, listener);
		}
		// Get the list of developers who made changes between this build and the last
		// if this mail type is configured that way
		if (type.getSendToDevelopers()) {
			Set<User> users;
			if (type.getIncludeCulprits()) {
				users = build.getCulprits();
			} else {
				users = new HashSet<User>();
				for (Entry change : build.getChangeSet()) {
					users.add(change.getAuthor());
				}
			}
			for (User user : users) {
				String adrs = user.getProperty(Mailer.UserProperty.class).getAddress();
				if (adrs != null)
					addAddressesFromRecipientList(recipientAddresses, adrs, env, listener);
				else {
					listener.getLogger().println("Failed to send e-mail to " + user.getFullName() + " because no e-mail address is known, and no default e-mail domain is configured");
				}
			}
		}
		//Get the list of recipients that are uniquely specified for this type of email
		if (type.getRecipientList() != null && type.getRecipientList().trim().length() > 0) {
            addAddressesFromRecipientList( recipientAddresses, type.getRecipientList(), env, listener );
		}
		
		msg.setRecipients(Message.RecipientType.TO, recipientAddresses.toArray(new InternetAddress[recipientAddresses.size()]));

		AbstractBuild<?,?> pb = build.getPreviousBuild();
		if (pb!=null) {
			// Send mails as replies until next successful build
			MailMessageIdAction b = pb.getAction(MailMessageIdAction.class);
			if(b!=null && pb.getResult()!=Result.SUCCESS) {
				msg.setHeader("In-Reply-To",b.messageId);
				msg.setHeader("References",b.messageId);
			}
		}

		return msg;
	}

    private void setSubject( final EmailType type, final AbstractBuild<?, ?> build, MimeMessage msg )
        throws MessagingException
    {
        String subject = new ContentBuilder().transformText(type.getSubject(), this, type, build);
        msg.setSubject(subject, CHARSET);
    }

    private void setContent( final EmailType type, final AbstractBuild<?, ?> build, MimeMessage msg )
        throws MessagingException
    {
        final String text = new ContentBuilder().transformText(type.getBody(), this, type, build);

        String messageContentType = contentType;
        // contentType is null if the project was not reconfigured after upgrading.
        if (messageContentType == null || "default".equals(messageContentType)) {
            messageContentType = DESCRIPTOR.getDefaultContentType();
            // The defaultContentType is null if the main Hudson configuration
            // was not reconfigured after upgrading.
            if (messageContentType == null) {
                messageContentType = "text/plain";
            }
        }
        messageContentType += "; charset=" + CHARSET;

        msg.setContent(text, messageContentType);
    }

    private static void addAddressesFromRecipientList(List<InternetAddress> addresses, String recipientList,
                                                      EnvVars envVars, BuildListener listener) {
        try {
            List<InternetAddress> internetAddresses = new EmailRecepientUtils().convertRecipientString( recipientList, envVars );
			addresses.addAll(internetAddresses);
		} catch(AddressException ae) {
			LOGGER.log(Level.WARNING, "Could not create email address.", ae);
			listener.getLogger().println("Failed to create e-mail address for " + ae.getRef());
		}
	}
	
	@Override
	public boolean needsToRunAfterFinalized() {
		return true;
	}

	public BuildStepMonitor getRequiredMonitorService() {
		return BuildStepMonitor.BUILD;
	}

	@Override
	public BuildStepDescriptor<Publisher> getDescriptor() {
		return DESCRIPTOR;
	}
	
	@Extension
	public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

	/*
	 * These settings are the settings that are global.
	 */
	public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
		/**
		 * The default e-mail address suffix appended to the user name found from changelog,
		 * to send e-mails. Null if not configured.
		 */
		private String defaultSuffix;

		/**
		 * Hudson's own URL, to put into the e-mail.
		 */
		private String hudsonUrl;

		/**
		 * If non-null, use SMTP-AUTH with these information.
		 */
		private String smtpAuthPassword,smtpAuthUsername;

		/**
		 * The e-mail address that Hudson puts to "From:" field in outgoing e-mails.
		 * Null if not configured.
		 */
		private String adminAddress;

		/**
		 * The SMTP server to use for sending e-mail. Null for default to the environment,
		 * which is usually <tt>localhost</tt>.
		 */
		private String smtpHost;
		
		/**
		 * If true use SSL on port 465 (standard SMTPS) unless <code>smtpPort</code> is set.
		 */
		private boolean useSsl;
		
		/**
		 * The SMTP port to use for sending e-mail. Null for default to the environment,
		 * which is usually <tt>25</tt>.
		 */
		private String smtpPort;
		
		/**
		 * This is a global default content type (mime type) for emails.
		 */
		private String defaultContentType;

		/**
		 * This is a global default subject line for sending emails.
		 */
		private String defaultSubject;
		
		/**
		 * This is a global default body for sending emails.
		 */
		private String defaultBody;

		private boolean overrideGlobalSettings;
		
		@Override
		public String getDisplayName() {
			return "Editable Email Notification";
		}
		
		public String getAdminAddress() {
			String v = adminAddress;
			if (v == null) {
				v = "address not configured yet <nobody>";
			}
			return v;
		}

		public String getDefaultSuffix() {
			return defaultSuffix;
		}
		
		/** JavaMail session. */
		public Session createSession() {
			/*
			 * 				Mailer.DescriptorImpl desc = Mailer.descriptor();
				smtpHost = nullify(desc.getSmtpServer());
				adminAddress = desc.getAdminAddress();
				defaultSuffix = nullify(desc.getDefaultSuffix());
				hudsonUrl = desc.getUrl();
				smtpAuthUsername = desc.getSmtpAuthUserName();
				smtpAuthPassword = <PASSWORD>.getSmtpAuthPassword();
				useSsl = desc.getUseSsl();
				smtpPort = desc.getSmtpPort();
			 */
			Properties props = new Properties(System.getProperties());
			if(smtpHost!=null)
				props.put("mail.smtp.host",smtpHost);
			if (smtpPort!=null) {
				props.put("mail.smtp.port", smtpPort);
			}
			if (useSsl) {
				/* This allows the user to override settings by setting system properties but
				 * also allows us to use the default SMTPs port of 465 if no port is already set.
				 * It would be cleaner to use smtps, but that's done by calling session.getTransport()...
				 * and thats done in mail sender, and it would be a bit of a hack to get it all to
				 * coordinate, and we can make it work through setting mail.smtp properties.
				 */
				if (props.getProperty("mail.smtp.socketFactory.port") == null) {
					String port = smtpPort==null?"465":smtpPort;
					props.put("mail.smtp.port", port);
					props.put("mail.smtp.socketFactory.port", port);
				}
				if (props.getProperty("mail.smtp.socketFactory.class") == null) {
					props.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory");
				}
				props.put("mail.smtp.socketFactory.fallback", "false");
			}
			if (smtpAuthUsername!=null)
				props.put("mail.smtp.auth","true");
			return Session.getInstance(props,getAuthenticator());
		}
		
		private Authenticator getAuthenticator() {
			final String un = getSmtpAuthUsername();
			if (un == null) return null;
			return new Authenticator() {
				@Override
				protected PasswordAuthentication getPasswordAuthentication() {
					return new PasswordAuthentication(getSmtpAuthUsername(), getSmtpAuthPassword());
				}
			};
		}

		public String getHudsonUrl() {
			if (hudsonUrl == null) {
				return Hudson.getInstance().getRootUrl();
			}
			return hudsonUrl;
		}

		public String getSmtpServer() {
			return smtpHost;
		}

		public String getSmtpAuthUsername() {
			return smtpAuthUsername;
		}
		
		public String getSmtpAuthPassword() {
			return <PASSWORD>;
		}

		public boolean getUseSsl() {
			return useSsl;
		}
		
		public String getSmtpPort() {
			return smtpPort;
		}
		
		public String getDefaultContentType() {
			return defaultContentType;
		}

        public String getDefaultSubject() {
			return defaultSubject;
		}
		
		public String getDefaultBody() {
			return defaultBody;
		}
		
		public boolean getOverrideGlobalSettings() {
			return overrideGlobalSettings;
		}

		public boolean isApplicable(Class<? extends AbstractProject> jobType) {
			return true;
                }

		@Override
		public Publisher newInstance(StaplerRequest req, JSONObject formData) throws hudson.model.Descriptor.FormException {
			// Save the recipient lists
			String listRecipients = formData.getString("recipientlist_recipients");
			
			// Save configuration for each trigger type
			ExtendedEmailPublisher m = new ExtendedEmailPublisher();
			m.recipientList = listRecipients;
			m.contentType = formData.getString("project_content_type");
			m.defaultSubject = formData.getString("project_default_subject");
			m.defaultContent = formData.getString("project_default_content");
			m.configuredTriggers = new ArrayList<EmailTrigger>();
			
			// Create a new email trigger for each one that is configured
			for (String mailerId : EMAIL_TRIGGER_TYPE_MAP.keySet()) {
				if("true".equalsIgnoreCase(formData.optString("mailer_" + mailerId + "_configured"))) {
					EmailType type = createMailType(formData, mailerId);
					EmailTrigger trigger = EMAIL_TRIGGER_TYPE_MAP.get(mailerId).getNewInstance(type);
					m.configuredTriggers.add(trigger);
				}
			}
			
			return m;
		}
		
		private EmailType createMailType(JSONObject formData, String mailType) {
			EmailType m = new EmailType();
			String prefix = "mailer_" + mailType + '_';
			m.setSubject(formData.getString(prefix + "subject"));
			m.setBody(formData.getString(prefix + "body"));
			m.setRecipientList(formData.getString(prefix + "recipientList"));
			m.setSendToRecipientList(formData.optBoolean(prefix + "sendToRecipientList"));
			m.setSendToDevelopers(formData.optBoolean(prefix + "sendToDevelopers"));
			m.setIncludeCulprits(formData.optBoolean(prefix + "includeCulprits"));
			return m;
		}
		
		public DescriptorImpl() {
			super(ExtendedEmailPublisher.class);
			load();
			if (defaultBody == null && defaultSubject == null) {
				defaultBody = DEFAULT_BODY_TEXT;
				defaultSubject = DEFAULT_SUBJECT_TEXT;
			}
		}

		@Override
		public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
			// Most of this stuff is the same as the built-in email publisher

			// Configure the smtp server
			smtpHost = nullify(req.getParameter("ext_mailer_smtp_server"));
			adminAddress = req.getParameter("ext_mailer_admin_address");
			defaultSuffix = nullify(req.getParameter("ext_mailer_default_suffix"));
			
			// Specify the url to this hudson instance
			String url = nullify(req.getParameter("ext_mailer_hudson_url"));
			if (url != null && !url.endsWith("/")) {
				url += '/';
			}
			if (url == null) {
				url = Hudson.getInstance().getRootUrl();
			}
			hudsonUrl = url;

			// specify authentication information
			if (req.getParameter("extmailer.useSMTPAuth") != null) {
				smtpAuthUsername = nullify(req.getParameter("extmailer.SMTPAuth.userName"));
				smtpAuthPassword = nullify(req.getParameter("extmailer.SMTPAuth.password"));
			} else {
				smtpAuthUsername = smtpAuthPassword = <PASSWORD>;
			}
			
			// specify if the mail server uses ssl for authentication
			useSsl = req.getParameter("ext_mailer_smtp_use_ssl") != null;
			
			// specify custom smtp port
			smtpPort = nullify(req.getParameter("ext_mailer_smtp_port"));
			
			defaultContentType = nullify(req.getParameter("ext_mailer_default_content_type"));

			// Allow global defaults to be set for the subject and body of the email
			defaultSubject = nullify(req.getParameter("ext_mailer_default_subject"));
			defaultBody = nullify(req.getParameter("ext_mailer_default_body"));
			
			overrideGlobalSettings = req.getParameter("ext_mailer_override_global_settings") != null;
			
			save();
			return super.configure(req, formData);
		}
		
		private String nullify(String v) {
			if(v!=null && v.length()==0)
				v=null;
			return v;
		}
		
		@Override
		public String getHelpFile() {
			return "/plugin/email-ext/help/main.html";
		}
		
		public FormValidation doAddressCheck(
				@QueryParameter final String value) throws IOException, ServletException {
			try {
				new InternetAddress(value);
				return FormValidation.ok();
			} catch (AddressException e) {
				return FormValidation.error(e.getMessage());
			}
		}
		
		public FormValidation doRecipientListRecipientsCheck(
				@QueryParameter final String value) throws IOException, ServletException {
			return new EmailRecepientUtils().validateFormRecipientList( value );
		}
		
	}



}

{'fraction_non_alphanumeric': 0.10554295119889674, 'fraction_numerical': 0.0007117754348503047, 'mean_word_length': 4.0323483322140135, 'pattern_counts': {'":': 2, '<': 96, '<?xml version=': 0, '>': 98, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1693755', 'n_tokens_mistral': 15084, 'n_tokens_neox': 12874, 'n_words': 3309}