001    package hirondelle.web4j.webmaster;
002    
003    import hirondelle.web4j.model.AppException;
004    import hirondelle.web4j.readconfig.InitParam;
005    import hirondelle.web4j.util.Util;
006    import hirondelle.web4j.util.WebUtil;
007    
008    import java.util.ArrayList;
009    import java.util.List;
010    import java.util.Properties;
011    import java.util.StringTokenizer;
012    import java.util.Timer;
013    import java.util.TimerTask;
014    import java.util.logging.Logger;
015    
016    import javax.mail.Authenticator;
017    import javax.mail.Message;
018    import javax.mail.PasswordAuthentication;
019    import javax.mail.Session;
020    import javax.mail.Transport;
021    import javax.mail.internet.InternetAddress;
022    import javax.mail.internet.MimeMessage;
023    import javax.servlet.ServletConfig;
024    
025    /**
026     Default implementation of {@link Emailer}.
027     
028     <P>Uses these <tt>init-param</tt> settings in <tt>web.xml</tt>  :
029     <ul>
030     <li><tt>Webmaster</tt> : the email address of the webmaster.
031     <li><tt>MailServerConfig</tt> : configuration data to be passed to the mail server, as a list of name=value pairs.
032     Each name=value pair appears on a single line by itself. Used for <tt>mail.host</tt> settings, and so on. 
033     The special value <tt>NONE</tt> indicates that emails are suppressed, and will not be sent. 
034     <li><tt>MailServerCredentials</tt> : user name and password for access to the outgoing mail server. 
035     The user name is separated from the password by a pipe character '|'.
036     The special value <tt>NONE</tt> means that no credentials are needed (often the case when the wep app
037     and the outgoing mail server reside on the same network). 
038     </ul> 
039     
040     <P>Example <tt>web.xml</tt> settings, using a Gmail account:
041    <PRE>    &lt;init-param&gt;
042          &lt;param-name&gt;Webmaster&lt;/param-name&gt;
043          &lt;param-value&gt;myaccount@gmail.com&lt;/param-value&gt; 
044        &lt;/init-param&gt;
045        
046        &lt;init-param&gt;
047          &lt;param-name&gt;MailServerConfig&lt;/param-name&gt;
048          &lt;param-value&gt;
049            mail.smtp.host=smtp.gmail.com       
050            mail.smtp.auth=true
051            mail.smtp.port=465
052            mail.smtp.socketFactory.port=465
053            mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
054          &lt;/param-value&gt; 
055        &lt;/init-param&gt;
056    
057        &lt;init-param&gt;
058          &lt;param-name&gt;MailServerCredentials&lt;/param-name&gt;
059          &lt;param-value&gt;myaccount@gmail.com|mypassword&lt;/param-value&gt; 
060        &lt;/init-param&gt;
061    */
062    public class EmailerImpl implements Emailer {
063    
064      /** Called by the framework upon startup, to extract configuration info from <tt>web.xml</tt>.  */
065      public static void init(ServletConfig aConfig) {
066        WEBMASTER = WEBMASTER.fetch(aConfig);
067        MAIL_SERVER_CREDENTIALS = MAIL_SERVER_CREDENTIALS.fetch(aConfig);
068        MAIL_SERVER_CONFIG = MAIL_SERVER_CONFIG.fetch(aConfig);
069        fLogger.fine("Configured Webmaster : " + WEBMASTER.getValue());
070        fLogger.fine("Configured MailServerConfig : " + MAIL_SERVER_CONFIG.getValue());
071      }
072    
073      public  void sendFromWebmaster(List<String> aToAddresses, String aSubject, String aBody) throws AppException {
074        if (isMailDisabled()) {
075          fLogger.fine("Mailing is disabled, since mail server is " + Util.quote(NONE));
076        }
077        else {
078          validateState(getWebmasterEmailAddress(), aToAddresses, aSubject, aBody);
079          fLogger.fine("Sending email using request thread.");
080          sendEmail(getWebmasterEmailAddress(), aToAddresses, aSubject, aBody);
081        }
082      }
083    
084      // PRIVATE 
085    
086      /**
087       When an init-param takes this value in web.xml, then the corresponding item is 'turned off'.
088       Turning off MAIL_SERVER_CONFIG will disable emailing entirely.
089       Turning off the MAIL_SERVER_CREDENTIALS means no credentials will be used.
090      */
091      private static final String NONE = "NONE";
092    
093      private static InitParam WEBMASTER = new InitParam("Webmaster");
094      private static InitParam MAIL_SERVER_CREDENTIALS =  new InitParam("MailServerCredentials", NONE);
095      private static InitParam MAIL_SERVER_CONFIG = new InitParam("MailServerConfig", NONE);
096      
097      private static final Logger fLogger = Util.getLogger(EmailerImpl.class);
098    
099      private boolean isMailDisabled() {
100        return  MAIL_SERVER_CONFIG.getValue().equalsIgnoreCase(NONE);
101      }
102      
103      private boolean areCredentialsEnabled() {
104        return  ! MAIL_SERVER_CREDENTIALS.getValue().equalsIgnoreCase(NONE);
105      }
106    
107      /** Return the mail server config in the form of a Properties object. */
108      private Properties getMailServerConfigProperties() {
109        Properties result = new Properties();
110        String rawValue = MAIL_SERVER_CONFIG.getValue();
111        /*  Example data: mail.smtp.host = smtp.blah.com */
112        if(Util.textHasContent(MAIL_SERVER_CONFIG.getValue())){
113          List<String> lines = getAsLines(rawValue);
114          for(String line : lines){
115            int delimIdx = line.indexOf("=");
116            String name = line.substring(0,delimIdx);
117            String value = line.substring(delimIdx+1);
118            if(isMissing(name) || isMissing(value)){
119              throw new RuntimeException(
120                "This line for the " + MAIL_SERVER_CONFIG.getName() + " setting in web.xml does not have the expected form: " + Util.quote(line)
121              );
122            }
123            result.put(name.trim(), value.trim());
124          }
125        }
126        return result;
127      }
128      
129      private List<String> getAsLines(String aRawValue){
130        List<String> result = new  ArrayList<String>();
131        StringTokenizer parser = new StringTokenizer(aRawValue, "\n\r");
132        while ( parser.hasMoreTokens() ) {
133          result.add( parser.nextToken().trim() );
134        }
135        return result;
136      }
137      
138      private static boolean isMissing(String aText){
139        return ! Util.textHasContent(aText);
140      }
141      
142      private String getWebmasterEmailAddress() {
143        return WEBMASTER.getValue();
144      }
145    
146      private void validateState(String aFrom, List<String> aToAddresses, String aSubject, String aBody) throws AppException {
147        AppException ex = new AppException();
148        if (!WebUtil.isValidEmailAddress(aFrom)) {
149          ex.add("From-Address is not a valid email address.");
150        }
151        if (!Util.textHasContent(aSubject)) {
152          ex.add("Email subject has no content");
153        }
154        if (!Util.textHasContent(aBody)) {
155          ex.add("Email body has no content");
156        }
157        for(String email: aToAddresses){
158          if (!WebUtil.isValidEmailAddress(email)) {
159            ex.add("To-Address is not a valid email address: " + Util.quote(email));
160          }
161        }
162        if (ex.isNotEmpty()) {
163          fLogger.severe("Cannot send email : " + ex);
164          throw ex;
165        }
166      }
167    
168      private void sendEmail(String aFrom, List<String> aToAddresses, String aSubject, String aBody) throws AppException {
169        fLogger.fine("Sending mail to " + Util.quote(aFrom));
170        try {
171          Session session = Session.getDefaultInstance(getMailServerConfigProperties(), getAuthenticator());
172          MimeMessage message = new MimeMessage(session);
173          message.setFrom(new InternetAddress(aFrom));
174          for(String toAddr: aToAddresses){
175            message.addRecipient(Message.RecipientType.TO, new InternetAddress(toAddr));
176          }
177          message.setSubject(aSubject);
178          message.setText(aBody);
179          Transport.send(message); // thread-safe?
180        }
181        catch (Throwable ex) {
182          fLogger.severe("CANNOT SEND EMAIL: " + ex);
183          throw new AppException("Cannot send email", ex);
184        }
185        fLogger.fine("Mail is sent.");
186      }
187      
188      private Authenticator getAuthenticator(){
189        Authenticator  result = null;
190        if( areCredentialsEnabled() ){
191          result = new SMTPAuthenticator();
192        }
193        return result;
194      }
195      
196      private final class SMTPAuthenticator extends Authenticator {
197        public PasswordAuthentication getPasswordAuthentication()   {
198          PasswordAuthentication result = null;
199          /** Format is pipe separated : bob|passwd. */
200          String rawValue = MAIL_SERVER_CREDENTIALS.getValue();
201          int delimIdx = rawValue.indexOf("|");
202          if(delimIdx != -1){
203            String userName = rawValue.substring(0,delimIdx);
204            String password = rawValue.substring(delimIdx+1);
205            result = new PasswordAuthentication(userName, password);
206          }
207          else {
208            throw new RuntimeException("Missing pipe separator between user name and password: " + rawValue);
209          }
210          return result;
211        }
212      }
213    }