Friday, July 11, 2008

Password expiration email utility

I had trouble finding a free utility that would send employees emails before their windows passwords were ready to expire, so I wrote a C# console application that does it. Here is the source code:



using System;
using System.Collections.Generic;
using System.Text;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Configuration;
using ActiveDs;
using System.Collections;
using System.IO;
using System.Net.Mail;


namespace PasswordNotifcation
{
class Program
{
const int ADS_SCRIPT = 0x0001;
const int ADS_ACCOUNTDISABLE = 0x0002;
const int ADS_HOMEDIR_REQUIRED = 0x0008;
const int ADS_LOCKOUT = 0x0010;
const int ADS_PASSWD_NOTREQD = 0x0020;
const int ADS_PASSWD_CANT_CHANGE = 0x0040;
const int ADS_ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080;
const int ADS_TEMP_DUPLICATE_ACCOUNT = 0x0100;
const int ADS_NORMAL_ACCOUNT = 0x0200;
const int ADS_INTERDOMAIN_TRUST_ACCOUNT = 0x0800;
const int ADS_WORKSTATION_TRUST_ACCOUNT = 0x1000;
const int ADS_SERVER_TRUST_ACCOUNT = 0x2000;
const int ADS_DONT_EXPIRE_PASSWORD = 0x10000;
const int ADS_MNS_LOGON_ACCOUNT = 0x20000;
const int ADS_SMARTCARD_REQUIRED = 0x40000;
const int ADS_TRUSTED_FOR_DELEGATION = 0x80000;
const int ADS_NOT_DELEGATED = 0x100000;
const int ADS_USE_DES_KEY_ONLY = 0x200000;
const int ADS_DONT_REQ_PREAUTH = 0x400000;
const int ADS_PASSWORD_EXPIRED = 0x800000;
const int ADS_TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000;


public static void SendMailMessage(string ToAddress,string FromAddress, string MessageSubject, string MessageText, string ToCC, string ToBCC, string MailServer)
{
System.Net.Mail.MailMessage mail = new System.Net.Mail.MailMessage(FromAddress, ToAddress, MessageSubject, MessageText);

if(ToCC != string.Empty && ToCC != null)
{
mail.CC.Add(ToCC);
}
if(ToBCC != string.Empty && ToBCC != null)
{
mail.Bcc.Add(ToBCC);
}

System.Net.Mail.SmtpClient cli = new System.Net.Mail.SmtpClient();
cli.Host = MailServer;
cli.Send(mail);
}



static void Main(string[] args)
{
if (ArgFound("-?", args))
{
Console.WriteLine(@"PasswordNotification.exe - Sends emails to any users that have passwords that are ready to expire.");
Console.WriteLine(@"Command Line Example");
Console.WriteLine(@"PasswordNotification.exe");
Console.WriteLine(@"");
Console.WriteLine(@"This utility requires two files. 1) a config file called Settings.config that contains application settings, and 2)a set of email template files that conain the warning text of each email.");
return;

}

try
{


System.Xml.XmlDocument doc = new System.Xml.XmlDocument();

doc.Load(AppDomain.CurrentDomain.SetupInformation.ApplicationBase + @"\settings.config");

System.Xml.XmlNodeList nl = doc.SelectNodes("//Warning");



System.Xml.XmlNode nodtmp = doc.SelectSingleNode("//StartingOU");
string strStartingOU = nodtmp.Attributes["Value"].Value;


nodtmp = doc.SelectSingleNode("//SmtpServer");
string strSmtpServer = nodtmp.Attributes["Value"].Value;

nodtmp = doc.SelectSingleNode("//MailFrom");
string strEmailFrom = nodtmp.Attributes["Value"].Value;

TimeSpan tsMaxPasswordAge = GetMaxPasswordAge();
int iMaxPwdAge = tsMaxPasswordAge.Days;

DirectoryEntry objDirEnt = new DirectoryEntry(strStartingOU);

DirectorySearcher ds = new DirectorySearcher(objDirEnt);
SearchResultCollection src = null;

ds.Filter = "(objectClass=user)";

src = ds.FindAll();
if (src.Count > 0)
{
foreach (SearchResult sr in src)
{
DirectoryEntry UserEntry = sr.GetDirectoryEntry();



WriteLogMessage("Processing:" + UserEntry.Name);

WriteLogMessage("Max Password Age:" + iMaxPwdAge);

//Console.WriteLine("Account control:" + UserEntry.Properties["userAccountcontrol"].Value);

int userAccountControl = Convert.ToInt32(UserEntry.Properties["userAccountcontrol"].Value);
if ((userAccountControl & ADS_DONT_EXPIRE_PASSWORD) > 0)
{
WriteLogMessage("Password never expires for:" + UserEntry.Name);
}
else
{
WriteLogMessage("Password expiration date found for:" + UserEntry.Name);

ActiveDs.IADsUser native = (ActiveDs.IADsUser)UserEntry.NativeObject;

DateTime passwordLastChanged = new DateTime(9999, 1, 1);
try
{

passwordLastChanged = native.PasswordLastChanged;


}

catch { }
string strEmailTo = "";
try
{
strEmailTo = native.EmailAddress;
}
catch { };

WriteLogMessage("Password last changed date:" + passwordLastChanged.ToString());

if (passwordLastChanged.Year != 9999)
{
DateTime expireDate = passwordLastChanged.AddDays(iMaxPwdAge);

TimeSpan ts = expireDate - DateTime.Now;

int iDaysTilExpired = ts.Days;

WriteLogMessage("Days til password expires:" + iDaysTilExpired);


foreach (System.Xml.XmlNode nod in nl)
{
int iDays = Convert.ToInt32(nod.Attributes["Days"].Value);


if (iDays == iDaysTilExpired)
{

if (strEmailTo != "")
{

string strSubject = nod.Attributes["Subject"].Value;
string strTemplatePath = nod.Attributes["Template"].Value;


using (TextReader streamReader = new StreamReader(strTemplatePath))
{
string strContents = streamReader.ReadToEnd();

strContents = strContents.Replace("%%days%%", iDays.ToString());

Console.WriteLine("Sending email to:" + strEmailTo);

SendMailMessage(strEmailTo, strEmailFrom, strSubject, strContents, "", "", strSmtpServer);
WriteLogMessage("Email sent to:" + strEmailTo);

}
}
else
{
WriteLogMessage("Email address is blank so don't send");
}

}
}
}
else
{
WriteLogMessage("Password last changed date is not set.");

}
}
}
}
}
catch (Exception ex)
{
WriteLogMessage("Error -- Exception thrown:" + ex.ToString());
}

}

static public bool ArgFound(string strArg, string[] args)
{
foreach (string strTempArg in args)
{
if (strTempArg.ToLower().StartsWith(strArg.ToLower()))
{
return true;
}
}
return false;

}

static private void WriteLogMessage(string Message)
{
Console.WriteLine(Message);
System.IO.StreamWriter sw = new System.IO.StreamWriter(@"log.txt", true);
DateTime dtTimestamp = System.DateTime.Now;

string strLine = dtTimestamp.Year + "-" + dtTimestamp.Month + "-" + dtTimestamp.Day + " " + dtTimestamp.Hour + ":" + dtTimestamp.Minute + ":" + dtTimestamp.Second + "." + dtTimestamp.Millisecond + "\t";
strLine += Message;

sw.WriteLine(strLine);
sw.Flush();
sw.Close();

}


public static TimeSpan GetMaxPasswordAge()
{
using (Domain d = Domain.GetCurrentDomain())
using (DirectoryEntry domain = d.GetDirectoryEntry())
{
DirectorySearcher ds = new DirectorySearcher(
domain,
"(objectClass=*)",
null,
SearchScope.Base
);
SearchResult sr = ds.FindOne();
TimeSpan maxPwdAge = TimeSpan.MinValue;
if (sr.Properties.Contains("maxPwdAge"))
maxPwdAge = TimeSpan.FromTicks((long)sr.Properties["maxPwdAge"][0]);
return maxPwdAge.Duration();
}
}

}
}






The code also requires a config file called settings.config that looks like this. You will need to change it to match the Active directory OU where you want the utility to start searching for users, or you can change it to the root OU. You can add as many "warning" sections as you wish for different notification schemes. The example below will send a warning 10 days, 3 days, and 1 day before a password is ready to expire. You will also need to set up the settings for your SMTP server to send the email.




<?xml version="1.0" encoding="utf-8" ?>
<Settings>

<StartingOU Value="LDAP://123.123.123.12/ou=employees,dc=mydomain,dc=com"></StartingOU>
<SmtpServer Value="mail.mydomain.com"></SmtpServer>
<MailFrom Value="System@mydomain.com"></MailFrom>
<Warning Days="1" Subject="Your password will expire soon" Template="Template1.txt"></Warning>
<Warning Days="3" Subject="Your password will expire soon" Template="Template1.txt"></Warning>
<Warning Days="10" Subject="Your password will expire soon" Template="Template1.txt"></Warning>

</Settings>








The config file above also references a templatefile called "Template1.txt" for the text of your email. The template should look something like this:





Your windows password will expire in %%days%% days.

Please go to http://123.123.123.12/EmployeeTools to reset it.

Thank You





Set up your console application as a scheduled task that runs every day. Set up the scheduled task to have read access to the active directory container for your users.