Wednesday, April 19, 2006

Removing Disabled Users from Distribution lists via a script

One thing that I seem to do with monotonous regularity is disable user accounts as people churn though the companies that I work for. Usually once an account is disabled the chances of it being re-enabled while possible are always slim. One of the things that mostly gets forgotten when disabling an account is to also remove it from any distribution lists that account maybe in. Now with the new hot-fix this is not as much of a problem because a disabled mailbox can now be configured to still receive email. However it is still desirable for these disabled accounts not to be receiving email from distribution lists. Instead of going though each disabled user to work out if it’s a member of any distribution list and then remove it I decided to create a script that would fist let me list all the disable users that are in a distribution lists and also have an option so it will remove these users from that group.

For the script itself I’ve used ADSI and the ADO data shaping provider again. As in the past the ADO data shaping provider is great if you want to create hierarchal datasets which is perfect for what I want to do here. Basically the script first goes though and creates a parent record set with the names of all the mail enabled groups in Active directory. A second ADSI query is then performed which does a bitwise filter to retrieve a recordset of all the disabled users with a mailbox. A child recordset is then created with an entry for each group a user is in. The two recordsets are then related on the Group’s DistinguishedName which then gives me a retrievable hierarchy such as

GroupName-|
|-UserName

The display section of the code then just looks at groups with at least 1 disabled user as a member. Some extra code is added to skip the system mailboxes and some ADSI code also is used to remove the user from the group if the script is being run in remove mode.

To run the script in display mode just run the script without any command-line parameters eg

Cscript.disabusers.vbs

To run the script in remove mode meaning that it will remove the disabled accounts from the group memberships of any mail-enabled groups (this script does not different between group types) run the script with remove as the command-line parameter eg

Cscript disabusers.vbs remove

I’ve put a downloadable copy of the script here the code itself looks like


if wscript.arguments.length = 0 then
wscript.echo "Display Mode"
else
if lcase(wscript.arguments(0)) = "remove" then
mode = "remove"
wscript.echo "Remove Mode"
else
wscript.echo "Display Mode"
end if
end if
wscript.echo
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
set conn1 = createobject("ADODB.Connection")
strConnString = "Data Provider=NONE; Provider=MSDataShape"
conn1.Open strConnString
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
strDefaultNamingContext = iAdRootDSE.Get("defaultNamingContext")
set objParentRS = createobject("adodb.recordset")
set objChildRS = createobject("adodb.recordset")
strSQL = "SHAPE APPEND" & _
" NEW adVarChar(255) AS GRPDisplayName, " & _
" NEW adVarChar(255) AS GRPDN, " & _
" ((SHAPE APPEND " & _
" NEW adVarChar(255) AS USDisplayName, " & _
" NEW adVarChar(255) AS USDN, " & _
" NEW adVarChar(255) AS USGRPDisplayName, " & _
" NEW adVarChar(255) AS USGRPDN " & _
")" & _
" RELATE GRPDN TO USGRPDN) AS rsGRPUS "
objParentRS.LockType = 3
objParentRS.Open strSQL, conn1
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
GALQueryFilter = "(&(mailnickname=*)(|(objectCategory=group)))"
strQuery = "<LDAP://" & strDefaultNamingContext & ">;" & GALQueryFilter &
";distinguishedName,displayname,legacyExchangeDN,homemdb;subtree"
Com.ActiveConnection = Conn
Com.CommandText = strQuery
Set Rs = Com.Execute
while not rs.eof
objParentRS.addnew
objParentRS("GRPDisplayName") = rs.fields("displayname")
objParentRS("GRPDN") = rs.fields("distinguishedName")
objParentRS.update
rs.movenext
wend
GALQueryFilter =
"(&(&(mailnickname=*)(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=2)))"
strQuery = "<LDAP://" & strDefaultNamingContext & ">;" & GALQueryFilter &
";distinguishedName,displayname,legacyExchangeDN,homemdb;subtree"
Com.ActiveConnection = Conn
Com.CommandText = strQuery
Set Rs1 = Com.Execute
Set objChildRS = objParentRS("rsGRPUS").Value
while not rs1.eof
if instr(rs1.fields("displayname"),"SystemMailbox{") = 0 then
set objuser = getobject("LDAP://" & rs1.fields("distinguishedName"))
For each objgroup in objuser.groups
objChildRS.addnew
objChildRS("USDisplayName") = rs1.fields("displayname")
objChildRS("USDN") = rs1.fields("distinguishedName")
objChildRS("USGRPDisplayName") = objgroup.name
objChildRS("USGRPDN") = objgroup.distinguishedName
objChildRS.update
next
end if
rs1.movenext
wend
objParentRS.MoveFirst
wscript.echo "GroupName,Disabled User's Name"
wscript.echo
Do While Not objParentRS.EOF
Set objChildRS = objParentRS("rsGRPUS").Value
if objChildRS.recordCount <> 0 then
Do While Not objChildRS.EOF
Wscript.echo objParentRS.fields("GRPDisplayName") & "," &
objChildRS.fields("USDisplayName")
if mode = "remove" then
set objgroup = getobject("LDAP://" & objChildRS.fields("USGRPDN"))
Set objUser = getobject("LDAP://" & objChildRS.fields("USDN"))
objGroup.Remove(objUser.AdsPath)
objgroup.setinfo
wscript.echo "User-Removed"
end if
objChildRS.MoveNext
loop
end if
objParentRS.MoveNext
Loop

61 comments:

Anonymous said...

Hello Glen,
Thanks for sharing this tool, it helps me alot in my daily admin work.
after running the command
c:\cscript disabusers.vbs remove

i get this error:
disabusers.vbs(56, 3) (null): 0x80005000



Ramon

Briam said...

I have been looking for something like this also...

I run it in non 'remove' mode and I also get the following:

Display Mode

C:\temp\disabusers.vbs(56, 3) (null): 0x80005000

Glen said...

Whats happening with this line is that the script is doing a serverless bind. In some enviroments its possible this wont work for more information on serverless binds see below. What you can do as a work around is to put the name of a domain controller in all instances inthe script where there is

getobject("LDAP://"

so replace this with

getobject("LDAP://DCServername/"

please let me know if this fixs the problem

The other thing to do from a troubleshooting perspective is you can build the Adpath string as a seperate varible and echo it out to see what it is before you use it so you can replace the line

set objuser = getobject("LDAP://" & rs1.fields("distinguishedName"))

with

userstr = "LDAP://" & rs1.fields("distinguishedName")
wscript.echo userstr
set objuser = getobject(userstr)

Cheers
Glen


http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpre...
"

Serverless binding refers to a process in which a client attempts to bind to
an Active Directory object without explicitly specifying an Active Directory
server in the binding string, for
example,LDAP://CN=jsmith,DC=fabrikam,DC=Com. This is possible because the
Lightweight Directory Access Protocol (LDAP) provider relies on the locator
services of Windows 2000 to find the best domain controller (DC) for the
client. However, the client must have an account on the Active Directory
domain controller in order to take advantage of the serverless binding
feature, and the domain controller used by a serverless bind will always be
located in the default domain (the domain associated with the current
security context of the thread that's doing the binding)."

Briam said...

Thanks for the quick response. I was getting good serverless bind but it dies on this account:

LDAP://CN=1155/TC CR 1709 (10 x5504),OU=Conf. Rooms,OU=Resources,OU=Corporate Ma
naged,OU=Southeast Managed,OU=United States,DC=na,DC=mirant,DC=net
C:\temp\disabusersATLNADC03.vbs(59, 3) (null): 0x80005000

Anonymous said...

Glen,
Thanks for your reply. I added the DCServername like you instructed on all instances of
getobject("LDAP://DCServername/",

still the same problem.

In which part of the code where you want me to put this particular code:

userstr = "LDAP://" & rs1.fields("distinguishedName")
wscript.echo userstr
set objuser = getobject(userstr)



Thanks again
ramon

Glen said...

Hi Briam,

Thanks for posting that information i was able to reproduce the error your having with this. The problem is occuring because of the / character in the Common Name. In ADSI the / character is a special character (actually the only special character there is) which seperates different elements eg server name from DN. So what you need to do to fix the is escape the / with a \ eg \/. What I've done is update the download for this post with some code that does the escape if any / exist in the DN so hopefully this should fix the issue it seemed to work okay for me. The three lines i updated where

set objuser = getobject("LDAP://" & replace(rs1.fields("distinguishedName"),"/","\/"))

set objgroup = getobject("LDAP://" & replace(objChildRS.fields("USGRPDN"),"/","\/"))

Set objUser = getobject("LDAP://" & replace(objChildRS.fields("USDN"),"/","\/"))

Again thanks for the feedback hopefully this will help out other people that might have the same issue.

Roman i suspect you may be having the same issue so try to download the script again from the link in the post and give it another try. With the diag code basically where you needed to insert that diag code was to replace line 56 with those 3 lines of code.

If this still doesn't work you could try putting in a on error resume next at the top of the script and see what result you get.

Briam said...

Thanks. That worked perfectly!

Anonymous said...

Thanks Glen, I will give that a shot and will let you know.

Ramon

Anonymous said...

Nice Work Glen!!! two thumbs up.
thank you very much for all your help

Ramon

Briam said...

This is helping move to a cleaner DL/User environment. Thank you. I was wondering if it would be dificult to have a simular script that looks for empty DLs... and remove them.

Glen said...

Hi Braim,

It shouldn't be too hard i'll have a look at it next week when i have time and post it if i come up with something

Cheers
Glen

Briam said...

Thanks. I am sure that there are several out there who would benefit from such a script. Have I mentioned that you are awesome!

Josep said...

Hi Glen,

That script is awesome, but in my domain not all the disabled accounts must be removed of all groups. We have one "Disabled Users" OU in every location, so I would like to have the script just looking at those OUs. How I can do that? Where in the script I can say to just look for user accounts in those OUs? Many thanks and keep the awesome work.

Glen said...

Hi Joesp,

As long as all your disabled users Ou's are named the same then you should just be able to do a filter by looking for the existance of the OU name in the DN name eg If you add a if statement into the following that will filter users that only have ou=disabledusers in users DN

if instr(lcase(objuser.distinguishedName),"ou=disabledusers,") then
For each objgroup in objuser.groups
objChildRS.addnew
objChildRS("USDisplayName") = rs1.fields("displayname")
objChildRS("USDN") = rs1.fields("distinguishedName")
objChildRS("USGRPDisplayName") = objgroup.name
objChildRS("USGRPDN") = objgroup.distinguishedName
objChildRS.update
next

Josep said...

Hi Glen

I added this lines, but I get one error. Unexpected end of statement..

Tks!
Josep

Glen said...

Sorry look like i left of the last line which should have been closing out the if statement

eg just add a "end if" after the

objChildRS.update
next

If that doesn't work let me know and i'll post up a modified version.

Anonymous said...

That worked OK mate
Really appreciated
It's strange those days to find somebody ready to help for free!!
Best rgrds from Barcelona
Josep

Anonymous said...

hello,

In your post you mention that disabled AD users with an exchange mailbox can receive emails. Is that true? I've tried to simulate that in my environment and messages sent to the disabled users get an NDR. Can you please shed some light on this? thanks

Anonymous said...

Hello Glen,
Great work mate and thanks for sharing. Not sure if this is possible but I was wondering how, if at all, you can search the PublicDelegates and PublicDelegatesBL attributes on a user object (not hard) and then remove, via script, the disabled accounts delegate access on that mailbox, and anyone that has access to their mailbox.

Glen said...

The ADSI part of doing this would be quite easy because those prop's store the DN's of the accounts you need to modify removing the delegates from AD shouldn't be to much of an issue. But i think you maybe be aluding to an issue where say someone has setup a calender delegate and they get there meeting requests forwarded to another user account which then gets disabled and starts then generating NDR's back to the meeting sender. Deleting just the AD delegates wont remove the forwarding rule that was setup nor the ACL's in the mailbox. I guess it all depends how far to you want to go ? Maybe one for next week

Cheers
Glen

Tillman Smoot said...

Maybe I'm having a problem, but I too need to parse through an OU and remove the users from DLs. I have tried pasting the above supplement into the code, but it doesnt seem to work. Maybe I'm pasting it into the wrong spot? Can you tell me where I'm supposed to be putting it?

Glen said...

The ADSI to remove a user from a group is pretty basic. This is not really a good example to use because there is so much noise happening around it. Have a look at http://support.microsoft.com/?kbid=232241 which has a much better example

Anonymous said...

Hi Glenn

thks soo much for your nifty script to remove the disabled users from the groups, would it be possible to include something in the scripts to move the disabled users to a certain OU and then remove them from the group.. reason is what im doin in my co here is when someone resigns, we'll disable their account and move to a disable OU for a mth before removing.. pls advise to my email @ austin77@gmail.com thks..

Austinlay said...

Hi Glen

based on my earlier anonymous post request, is it possible to also include in the script to move the disabled user to a particular ou, remove them from the groups and hide them from address book..cheers mate

Glen said...

Have a look at http://msgdev.mvps.org/exdevblog/disabuserv3.zip

This is a latch version so it wont move objects unless you select yes which is what i would recommend else you may find you end up moving objects you dont want.

To run the script i added in an extra run mode called move and you will also need to put the DN of the Ou you want to move in the

stmoMoveOU = "OU=Disabled Users......."

varible

Cheers
Glen

Austinlay said...

Hi Glenn

thks for the disabuserv3 which was wat i was asking for.. but seems when i ran the scrpt from dos ( disabuserv3 move) it was searching my "Resigned" which has all the disabled account, is there a way to exclude searching the Resigned ou? thks mate

Glen said...

The easiest way is just to filter it with a if statement eg

instr(lcase(rs1.fields("distinguishedName")),lcase("CN=Microsoft Exchange System Objects"))

This filters any objects in the Exchange System Objects Container you could do something simular for your resigned users OU.

Cheers
Glen

Anonymous said...

Great script. Thx.

Anonymous said...

Thanks for the script, but I kinda have another problem. I have alot of disabled users (leave of absence, sick leave and maternity leave). Many of these users will be away for month, and therefore they get disabled.

I want to remove them from Distribution Lists so people don't get their mail returned because the mailbox is full. Your script fixes this perfectly.

BUT, when my users returns my main problem raises. How do I add the users back to the Distribution Lists they were members of before they got disabled?

Anyone found any solution for this?

Glen said...

Group membership history isn't tracked anywhere that i know of. But what you could do is in the script that removes users from groups make it write the groups that a user was in before it removes them to one of the Exchange custom attributes (eg write a ; separated list of DN of the groups the user was in). Then when you want to reverse it all you need is a script that reads that property from the custom attribute and adds the user back into the appropriate groups. You need to watch the size limit on the custom attribute with in about 1024 characters (this should be okay unless the users in a very large number of groups).

Anonymous said...

Hi Glen

I have run this script in display mode for reporting purposes. The script runs without any errors but I there isn't any data populated under the headings. I think this could be because whereas the users are in the domain I am logged into the DL's reside in another domain of which there's a trust relationship to?

Many thanks for putting this out there!

Giles

Glen said...

Hi Giles,

This would certainly cause an issue because the ADSI query for the group only looks in the domain you are currently logged on to. To change this you would need to hardcode the name of the domain your DL is in the following line

GALQueryFilter = "(&(mailnickname=*)(|(objectCategory=group)))"
strQuery = "LDAP://" & strDefaultNamingContext & ";" & GALQueryFilter &
";distinguishedName,displayname,legacyExchangeDN,homemdb;subtree"

So you just need to replace the strDefaultNamingContext variable with the domain your Group is in. (In the normal Ldap format eg DC=domain,DC=com)

cheers
Glen

Anonymous said...

How can I pipe the display out to a file for logging purposes?

Anonymous said...

Hey glen,

About the group membership history and being able to put groups the user was in before to a custom Exchange attribute. Is there a script for that? Also is it possible to have the script you have created already and hopefully this new one to be able to log to a file and append info to it everytime it is run. This would be pretty cool for logging purposes.

Thanks for the Script..Hopefully you can help me out with my request.

thanks

Glen said...

Okay have a look at http://msgdev.mvps.org/exdevblog/disabusersl.zip

Cheers
Glen

Glen said...

Ps i've done no real testing so use at you own risk do you own test.

Cheers
Glen

Anonymous said...

Thanks glen. Im going to test it out. In what exchange attribute are you putting the distribution lists that the user was apart of? So i can check in ADSI?

thanks...

Anonymous said...

Hey Glen..tried that vbs file and im getting the following:

Line: 14
Char: 1
Error: the specified module could not be found
code: 8007007e
Source: nul

Glen said...

Line 14 is a simple ADO definition

set conn = createobject("ADODB.Connection")

Are you using Vista perhaps if you are i don't think this script would work well under vista try it on a XP or Windows 2003 machine. Otherwise you might have problem with ADO on the machine you trying to run it from . You might want to test this by just create a script with this line it in.

Cheers
Glen

Anonymous said...

Hi Glen,

Thanks for the excellent script! It works perfectly on my domain, however if I try to run it on a child domain I get the following error:

disabusers.vbs(53, 1) ADODB.Field:

Either BOF or EOF is True, or the current record has been deleted.

Requested operation requires a current record.

Also, is there a way to make this script crawl all sub-domains and also remove security groups?! That would be awesome!

Glen said...

While its possible to do i don't have a mutlti domain environment in which to build such a script

Cheers

Glen

Anonymous said...

Would you be able to point me in the right direction? I might be able to get the code working myself if you can set me on my way.

Glen said...

Try replace LDAP:// with GC:// this should make it query the Global Catalouge instead of just the domain. Otherwise start googling :)

Cheers
Glen

Anonymous said...

Hi Glen, in your post you said "Usually once an account is disabled the chances of it being re-enabled while possible are always slim."

Just found an issue today when a display name of a disabled user was changed, the account got re-enabled. Don't know if this is RUS doing something funky under the hood or something else...... but until today I would have agreed with that comment. :)

Hitesh Shah said...

Hi Glen,
This script is really useful.However we need this script to run under multiple domains in the same forest.
we have tried replacing LDAP:// with GC:// but this is not working. Also googling didnt helped :)

Your help is highly appreciated.

Glen said...

Sorry i dont have a multi-domain enviroment i can test this in it should work with GC although you need to make sure you query a GC in the root domain perhaps.

Cheers
Glen

Hitesh Shah said...

Thanks Glen for quick reponse.
We did try the GC option in Root domain as well as in the child domain but with no luck.
You are the last hope for us. I know I am asking too much on this post but is it possible for you to set up and test in multi domain test enviornment? We appreciate your time and effort.
Thanks againt for looking into this.

Regards.

Glen said...

Yes sorry you are asking too much seriously do you know the amount of time and resources that would take ! i'm poor in both time and resources. If you can give me access to multi domain enviroment then maybe.

Cheers
Glen

Hitesh Shah said...

Thanks Glen.So nice of you.
I have the test setup with me. Let me check if I can provide you access by somemeans.

Anonymous said...

Thanks Glen! It worked like charm

Ryan said...

I have a question for you about this. I am wondering if this will scan the users that I have in the disabled ou and look for their entries in the distribution list. Will it then delete them out of the DL completley and not show that they are a member anymore. My only reason for asking is I am looking for a backup something that will tell me what DL's they were apart of if I need to re add them.

Glen said...

If the users are disabled then yes

Cheers
Glen

Rakesh Mishra said...

What in case of linked mailboxes. In that case the Mailbox account will be disabled and linked to account from other trusted forest.

Anonymous said...

You just saved me from hours of hassle doing each account individually. Not only do I appreciate it, but so do my higher ups. I cannot thank you enough.

DJ said...

Glen,
You are a rock star!! I've a question.. I ran it in display mode and it showed me the DL and user who is disabled in it with an OK button. Can I selectively remove them? Lets say I want to remove 2 specific users who are disabled from all the DLs and not the rest of them.. Can I achieve this?

Anonymous said...

Hi Glen,

I came accross your blog about removing disabled users accounts form Distribution lists etc. I am by no means a script person at all but should know this stuff I'm sure as I work in Alberta as a Network Administrator. I'm sure a lot would say I should pick this stuff up.

Anyway from reading this blog (not sure how old it is). This script you have written is what I am looking for, (I'll be testing). But after reading it occured to me that what we do here is set an expiry date for users who are leaving the company in a week or two. So my question I'm sure you are way ahead of me is. Is there a way to modify this script to look for this setting as I'm sure you are well aware that an Admin does get very busy and will sometime forget to actually disable that user account. Which pops to mind one more question. Can this with modification set a user who's expiry date has come and gone disable that account then call the rest of the script to do its thing and remove him/her from all DL's?

Hope this isn't too much to ask?

Thanks and keep up the great work and like many have said you are indeed a Rock Star!! Without people like you some of us would not be the company super stars...

Thanks again

David G.

Glen said...

Yes you could use a CustomAttribute to store the Expiry Time. But its probably hours of fun to rework the script to get it do what you want.

Cheers
Glen

Anonymous said...

Hi Glen,

Glad to hear. Does that mean when you have some free time this is something you might have a look at?

David G.

Anthony Miller said...

Any chance of converting this to a powershell script?

Sandro said...

Hi Glenn, I have been using this script for quite awhile and it works great. I now find myself needing the script to the search for the "mail" attribute and not the "mailnickname" attribute. If I change the attribute from "mailnickname to mail" I receive Line 44, Column 2 error, Multiple-step operation generated errors. Check each status vaule. Any help with this would be appreciated.

Glen Scales said...

It could be that there is a Null value that is causing the ADO code to fail. This script is 10 years old if you can use Powershell maybe something like https://gallery.technet.microsoft.com/office/How-to-remove-disabled-9b1cefb2 would serve you better as its much easier to customize.