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> <init-param> 039 <param-name>Webmaster</param-name> 040 <param-value>myaccount@gmail.com</param-value> 041 </init-param> 042 043 <init-param> 044 <param-name>MailServerConfig</param-name> 045 <param-value> 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 </param-value> 052 </init-param> 053 054 <init-param> 055 <param-name>MailServerCredentials</param-name> 056 <param-value>myaccount@gmail.com|mypassword</param-value> 057 </init-param> 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 }