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 }