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