/*
ExternalMailTask.java
This task is run nightly to check all User accounts in Ganymede
for their external mail access expiration time.
Created: 19 Oct 2009
Module By: James Ratcliff, falazar@arlut.utexas.edu
-----------------------------------------------------------------------
Ganymede Directory Management System
Copyright (C) 1996-2014
The University of Texas at Austin
Ganymede is a registered trademark of The University of Texas at Austin
Contact information
Author Email: ganymede_author@arlut.utexas.edu
Email mailing list: ganymede@arlut.utexas.edu
US Mail:
Computer Science Division
Applied Research Laboratories
The University of Texas at Austin
PO Box 8029, Austin TX 78713-8029
Telephone: (512) 835-3200
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package arlut.csd.ganymede.gasharl;
import java.rmi.RemoteException;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Random;
import java.util.Vector;
import arlut.csd.ganymede.common.Invid;
import arlut.csd.ganymede.common.NotLoggedInException;
import arlut.csd.ganymede.common.Query;
import arlut.csd.ganymede.common.QueryNode;
import arlut.csd.ganymede.common.QueryAndNode;
import arlut.csd.ganymede.common.QueryDataNode;
import arlut.csd.ganymede.common.Result;
import arlut.csd.ganymede.common.ReturnVal;
import arlut.csd.ganymede.common.SchemaConstants;
import arlut.csd.ganymede.server.DateDBField;
import arlut.csd.ganymede.server.DBEditObject;
import arlut.csd.ganymede.server.DBObject;
import arlut.csd.ganymede.server.DBLog;
import arlut.csd.ganymede.server.DBSession;
import arlut.csd.ganymede.server.Ganymede;
import arlut.csd.ganymede.server.GanymedeServer;
import arlut.csd.ganymede.server.GanymedeSession;
import arlut.csd.ganymede.server.StringDBField;
import arlut.csd.ganymede.server.PasswordDBField;
import arlut.csd.Util.RandomUtils;
/*------------------------------------------------------------------------------
class
ExternalMailTask
------------------------------------------------------------------------------*/
/**
* This task is run nightly to check all user accounts in Ganymede
* for their External Mail expiration time.
*
* @author James Ratcliff falazar@arlut.utexas.edu
*/
public class ExternalMailTask implements Runnable {
static final boolean debug = true;
static final public String name = "external mail task";
/* -- */
private GanymedeSession mySession = null;
private DBSession myDBSession = null;
private Thread currentThread = null;
/**
*
* Just Do It (tm)
*
* @see java.lang.Runnable
*
*/
public void run()
{
currentThread = java.lang.Thread.currentThread();
Ganymede.debug("External Mail Task: Starting");
String error = GanymedeServer.checkEnabled();
if (error != null)
{
Ganymede.debug("Deferring External Mail task - semaphore disabled: " + error);
return;
}
try
{
try
{
mySession = new GanymedeSession("ExternalMailTask");
myDBSession = mySession.getDBSession();
}
catch (RemoteException ex)
{
Ganymede.debug("ExternalMail Task: Couldn't establish session");
return;
}
// we don't want no wizards
mySession.enableWizards(false);
ReturnVal retVal = mySession.openTransaction(ExternalMailTask.name);
if (!ReturnVal.didSucceed(retVal))
{
Ganymede.debug("ExternalMail Task: Couldn't open transaction");
return;
}
// do the stuff
checkExpiringCredentials();
retVal = mySession.commitTransaction();
if (!ReturnVal.didSucceed(retVal))
{
// if doNormalProcessing is true, the
// transaction was not cleared, but was
// left open for a re-try. Abort it.
if (retVal.doNormalProcessing)
{
Ganymede.debug("ExternalMail Task: couldn't fully commit, trying to abort.");
mySession.abortTransaction();
}
Ganymede.debug("ExternalMail Task: Couldn't successfully commit transaction");
}
else
{
Ganymede.debug("ExternalMail Task: Transaction committed");
}
}
catch (InterruptedException ex)
{
}
catch (NotLoggedInException ex)
{
}
finally
{
if (myDBSession.isTransactionOpen())
{
Ganymede.debug("ExternalMail Task: Forced to terminate early, aborting transaction");
}
mySession.logout();
}
}
/**
* <p>Checks the users external account expiration date</p>
*
* <ul>
* <li>If 1 month before, we assign a new set of credentials, and save old ones.</li>
* <li>If 1 day before, we send a warning letter.</li>
* <li>If expired, we remove old ones, and reset exp date, 6 months ahead.</li>
* </ul>
*/
private void checkExpiringCredentials() throws InterruptedException, NotLoggedInException
{
Query q;
DBObject object;
Date currentTime = new Date();
Calendar lowerBound = new GregorianCalendar();
Calendar upperBound = new GregorianCalendar();
Enumeration en;
QueryNode matchNode = new QueryAndNode(new QueryDataNode(userSchema.MAILEXPDATE,
QueryDataNode.DEFINED,
null),
new QueryDataNode(userSchema.ALLOWEXTERNAL,
QueryDataNode.DEFINED,
null));
/* -- */
q = new Query(SchemaConstants.UserBase, matchNode, false);
for (Result result: mySession.internalQuery(q))
{
if (currentThread.isInterrupted())
{
throw new InterruptedException("scheduler ordering shutdown");
}
object = myDBSession.viewDBObject(result.getInvid());
if (object == null || object.isInactivated())
{
continue;
}
Date mailExpDate = (Date) object.getFieldValueLocal(userSchema.MAILEXPDATE);
if (mailExpDate == null)
{
continue;
}
// four weeks before expiration, assign new credentials and
// send mail about that. the old credentials are kept and
// both are usable until the old ones are actually expired.
lowerBound.setTime(currentTime);
lowerBound.add(Calendar.DATE, 27);
upperBound.setTime(currentTime);
upperBound.add(Calendar.DATE, 28);
if (mailExpDate.after(lowerBound.getTime()) && mailExpDate.before(upperBound.getTime()))
{
String ckpLabel = "credentialing" + object.getInvid();
myDBSession.checkpoint(ckpLabel);
ReturnVal retVal = assignNewCredentials(object);
if (ReturnVal.didSucceed(retVal))
{
myDBSession.popCheckpoint(ckpLabel);
}
else
{
Ganymede.debug("External Mail Task failure in assignNewCredentials(" + object.getLabel() + ")\n" + retVal.getDialogText());
myDBSession.rollback(ckpLabel);
}
continue;
}
// then one day before, we send a warning.
lowerBound.setTime(currentTime);
upperBound.setTime(currentTime);
upperBound.add(Calendar.DATE, 1);
if (mailExpDate.after(lowerBound.getTime()) && mailExpDate.before(upperBound.getTime()))
{
warnPasswordExpire(object);
continue;
}
// on or after the expiration date, we clear the old
// credentials and assign a new expiration date in the future.
if (mailExpDate.before(currentTime))
{
String ckpLabel = "clearing" + object.getInvid();
myDBSession.checkpoint(ckpLabel);
ReturnVal retVal = clearOldCredentials(object);
if (ReturnVal.didSucceed(retVal))
{
myDBSession.popCheckpoint(ckpLabel);
}
else
{
Ganymede.debug("External Mail Task failure in clearOldCredentials(" + object.getLabel() + ")\n" + retVal.getDialogText());
myDBSession.rollback(ckpLabel);
}
continue;
}
}
}
/**
* <p>Assigns a new set of random external credentials and triggers
* email describing the change to the user.
*
* <p>Note that the password text is held in a PasswordDBField, and
* so would not ordinarily be sent over email through Ganymede's
* normal transaction email mechanism. The userCustom class
* controlling the gasharl User object contains custom logic to
* describe the credential change and the reason for it in the
* userCustom.preCommitHook() method.</p>
*/
private ReturnVal assignNewCredentials(DBObject userObject)
{
try
{
ReturnVal result = mySession.edit_db_object(userObject.getInvid());
if (!ReturnVal.didSucceed(result))
{
return result;
}
DBEditObject editUserObject = (DBEditObject) result.getObject();
StringDBField usernameField = editUserObject.getStringField(userSchema.MAILUSER);
PasswordDBField passwordField = editUserObject.getPassField(userSchema.MAILPASSWORD2);
String username = null;
String password = null;
// if we already have MAILUSER and MAILPASSWORD2 fields set,
// we'll need to remember those values for later
if (editUserObject.isDefined(userSchema.MAILUSER) &&
editUserObject.isDefined(userSchema.MAILPASSWORD2))
{
username = (String) usernameField.getValueLocal();
password = passwordField.getPlainText();
}
// Set new values.
result = usernameField.setValueLocal(RandomUtils.getRandomUsername());
if (!ReturnVal.didSucceed(result))
{
return result;
}
result = ReturnVal.merge(result, passwordField.setPlainTextPass(RandomUtils.getRandomPassword(20)));
if (!ReturnVal.didSucceed(result))
{
return result;
}
// if we had MAILUSER and MAILPASSWORD2 set when we entered,
// we'll load those old values into the OLDMAILUSER and
// OLDMAILPASSWORD2 fields for use during the interval between
// assigning new credentials and revoking the old ones
if (username != null && password != null)
{
StringDBField oldUsernameField = editUserObject.getStringField(userSchema.OLDMAILUSER);
PasswordDBField oldPasswordField = editUserObject.getPassField(userSchema.OLDMAILPASSWORD2);
// Copy the (now deprecated) values to the
// oldUsernameField and oldPasswordField fields
result = ReturnVal.merge(result, oldUsernameField.setValueLocal(username));
if (!ReturnVal.didSucceed(result))
{
return result;
}
result = ReturnVal.merge(result, oldPasswordField.setPlainTextPass(password));
}
return result;
}
catch (NotLoggedInException ex)
{
return Ganymede.loginError(ex);
}
}
/**
* <p>Send out a warning that the old credentials are going to be removed tomorrow,
* to the user's and admin groups' email addresses.</p>
*/
private void warnPasswordExpire(DBObject userObject)
{
Vector objVect = new Vector();
objVect.add(userObject.getInvid());
String mailUsername = (String) userObject.getFieldValueLocal(userSchema.MAILUSER);
PasswordDBField mailPasswordField = userObject.getPassField(userSchema.MAILPASSWORD2);
String mailPassword = mailPasswordField.getPlainText();
String titleString = "Old External Email Credentials Expiring Very Soon For User " + userObject.getLabel();
String messageString = "The old external email credentials for user account " + userObject.getLabel() + " will be expiring within 24 hours. \n" +
"You have been granted access to laboratory email from outside the internal ARL:UT network.\n\n" +
"In order to send mail from outside the laboratory, you will need to configure your external email client " +
"to send mail through smail.arlut.utexas.edu using TLS-encrypted SMTP.\n\n" +
"The user name you should be using for external access is:\n\n" +
"\tUsername: " + mailUsername + "\n\n" +
"and the new external access password for your account is:\n\n" +
"\tPassword: " + mailPassword + "\n\n" +
"The previously assigned external password will soon cease functioning.\n\n" +
"You should continue to use your internal email username and password for reading email from mailboxes.arlut.utexas.edu " +
"via SSL-protected IMAP.";
Ganymede.log.sendMail(null, titleString, messageString, DBLog.MailMode.USERS, objVect);
}
/**
* <p>Clear out old credentials and reset the expiration date.</p>
*
* <p>Send email to the user's and admin groups' email addresses.</p>
*/
private ReturnVal clearOldCredentials(DBObject userObject)
{
try
{
ReturnVal result = mySession.edit_db_object(userObject.getInvid());
if (!ReturnVal.didSucceed(result))
{
return result;
}
DBEditObject editUserObject = (DBEditObject) result.getObject();
DateDBField mailExpDateField = editUserObject.getDateField(userSchema.MAILEXPDATE);
StringDBField oldUsernameField = editUserObject.getStringField(userSchema.OLDMAILUSER);
PasswordDBField oldPasswordField = editUserObject.getPassField(userSchema.OLDMAILPASSWORD2);
Calendar myCal = new GregorianCalendar();
myCal.add(Calendar.DATE, 168); // 24 weeks from today.
result = mailExpDateField.setValueLocal(myCal.getTime());
if (!ReturnVal.didSucceed(result))
{
return result;
}
result = ReturnVal.merge(result, oldUsernameField.setValueLocal(null));
if (!ReturnVal.didSucceed(result))
{
return result;
}
result = ReturnVal.merge(result, oldPasswordField.setUndefined(true));
if (!ReturnVal.didSucceed(result))
{
return result;
}
Vector objVect = new Vector();
objVect.add(userObject.getInvid());
String mailUsername = (String) userObject.getFieldValueLocal(userSchema.MAILUSER);
PasswordDBField newPasswordField = userObject.getPassField(userSchema.MAILPASSWORD2);
String mailPassword = newPasswordField.getPlainText();
String titleString = "External Email Credentials Changed For User " + userObject.getLabel();
String messageString = "The external email credentials for User account " + userObject.getLabel() + " have been changed. \n" +
"You have been granted access to laboratory email from outside the internal ARL:UT network.\n\n" +
"In order to send mail from outside the laboratory, you will need to configure your external email client " +
"to send email through smail.arlut.utexas.edu using TLS-encrypted SMTP.\n\n" +
"The user name you should be using for external access is:\n\n" +
"\tUsername: " + mailUsername + "\n\n" +
"and the new external access password for your account is:\n\n" +
"\tPassword: " + mailPassword + "\n\n" +
"The previously assigned external password will no longer function. \n\n" +
"You should continue to use your internal email username and password for reading email from mailboxes.arlut.utexas.edu " +
"via SSL-protected IMAP.";
Ganymede.log.sendMail(null, titleString, messageString, DBLog.MailMode.USERS, objVect);
return result;
}
catch (NotLoggedInException ex)
{
return Ganymede.loginError(ex);
}
}
}