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> <init-param>
042 <param-name>Webmaster</param-name>
043 <param-value>myaccount@gmail.com</param-value>
044 </init-param>
045
046 <init-param>
047 <param-name>MailServerConfig</param-name>
048 <param-value>
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 </param-value>
055 </init-param>
056
057 <init-param>
058 <param-name>MailServerCredentials</param-name>
059 <param-value>myaccount@gmail.com|mypassword</param-value>
060 </init-param>
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 }