?
Solved

ADSI VBscript for Checking Administrator Container

Posted on 2009-04-16
45
Medium Priority
?
756 Views
Last Modified: 2012-05-06
Hello,  I need help writing a script that will check the Administrator container in Active Directory for will check for any Admin Accounts that get re-enabled.  This would need to be a daily report that would have the ability to be emailed out to individuals.  is this doable within a script?
0
Comment
Question by:itsmevic
  • 28
  • 16
45 Comments
 
LVL 38

Expert Comment

by:Shift-3
ID: 24162721
When you say "Administrator container", do you mean membership in the Administrators group under the Builtin container?  Or the Domain Admins group? Or something else?
 
 Do you want to recurse memberships of nested groups?
 
 Do you want to list all enabled members in the report, or just ones which have been modified in the last day?
0
 

Author Comment

by:itsmevic
ID: 24163631
Here's the tree broken down, hopefully this will help.

ADMINISTRATOR   <-----OU
(Under that I have 7 containers)
           -  Container 1
           -  Container 2
           -  Container 3
           -  Container 4
           -  Container 5
           -  Container 6
          -   Container 7

Would like to monitor those 7 containers and the admins in them.  Would need to monitor for any disabled administrator accounts in those containers that have been re-enabled.  Would like to generate a report daily to be emailed.
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24165156
Hey there,

This script will list each member and their status, but there's no record in the AD of "when" an account has been enabled or disabled. The only way you could do this is to write to a log file, each user and their status, and actually "compare" that.  However, if an account has been disabled and enabled before running the script again, it wouldn't pick up a difference.

Anyway, if you like that idea, make sure this script works, which only displays status, and we can then make it write a log.

Regards,

Rob.
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open("Ads Provider")
 
Set rsUsers = CreateObject("ADODB.Recordset")                                        
 
Set objRootDSE = GetObject("LDAP://RootDSE")
 
strFilter = "(&(objectCategory=user))"
'strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
               
Const ADS_UF_ACCOUNTDISABLE = 2
 
Set rsUsers = objConnection.Execute(strCmd)
'loop throught recordset, pull out only users who hadn't responded yet
'unless the user is disabled (userAccountControl = 66048)
rsUsers.MoveFirst
While Not rsUsers.EOF
	Set objUser = GetObject(rsUsers.fields("adsPath"))
	intUAC = objUser.Get("userAccountControl")
	If intUAC And ADS_UF_ACCOUNTDISABLE Then
		WScript.Echo objUser.DisplayName & ": Disabled"
	Else
		WScript.Echo objUser.DisplayName & ": Enabled"
	End If
	rsUsers.MoveNext
Wend
rsUsers.Close
objConnection.Close

Open in new window

0
Independent Software Vendors: We Want Your Opinion

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

 

Author Comment

by:itsmevic
ID: 24169096
Hey there buddy,

     Basically in a nutshell anytime an account in one of those 7 business containers is enabled, it needs to be reported.

    But your saying AD doesn't do this and that a log file would need to be generated.  By chance I'm assuming we could develop a script that would detect this change?  Then tie that script into your other script so that it can bounce it's findings and produce some sort of report?  Do you think this is doable?
0
 

Author Comment

by:itsmevic
ID: 24169770
Hi Rob, yes it appears your script above does cycle through those users with an on screen dialog
that shows the name of the person and the "enabled" status then gives option to click ok to cycle through to the next user.

***************************
*John Doe:  Enabled   *
*                                  *
*         OK                    *
*                                  *
***************************
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24172908
Hi, OK, so can get the "current" status of the accounts, which is good, but unfortunately, there is no stored values within AD that tell when an account was disabled, nor by whom, unless you use a third party tool, like this one:
http://www.scriptlogic.com/Active_Directory_Auditing.asp

The best we could do with what we've got is run the script, say, on an hourly basis, to check their current state against a previous state, and then email when that has changed....

See the script below, which now writes the status to a log file, and the next time you run the script, compares that previous status to the current status.

It does not email yet, but we can add that....

Regards,

Rob.
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open("Ads Provider")
 
Set rsUsers = CreateObject("ADODB.Recordset")                                        
 
Set objRootDSE = GetObject("LDAP://RootDSE")
 
strLogFile = "\\server\share\AdminStatus.log"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Const intForReading = 1
 
Set dictPrevious = CreateObject("Scripting.Dictionary")
Set dictCurrent = CreateObject("Scripting.Dictionary")
 
' First read the previous status
If objFSO.FileExists(strLogFile) = True Then
	Set objFile = objFSO.OpenTextFile(strLogFile, intForReading, False)
	While Not objFile.AtEndOfStream
		strLine = objFile.ReadLine
		If Trim(strLine) <> "" Then
			dictPrevious.Add Split(strLine, ";")(0), Split(strLine, ";")(1)
		End If
	Wend
	objFile.Close
End If
 
strFilter = "(&(objectCategory=user))"
'strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
               
Const ADS_UF_ACCOUNTDISABLE = 2
 
Set rsUsers = objConnection.Execute(strCmd)
'Loop through recordset to check the status
rsUsers.MoveFirst
While Not rsUsers.EOF
	Set objUser = GetObject(rsUsers.fields("adsPath"))
	intUAC = objUser.Get("userAccountControl")
	If intUAC And ADS_UF_ACCOUNTDISABLE Then
		strStatus = "Disabled"
	Else
		strStatus = "Enabled"
	End If
	dictCurrent.Add objUser.AdsPath, strStatus
	If objFSO.FileExists(strLogFile) = True Then
		If dictPrevious.Exists(objUser.adsPath) = True Then
			If dictPrevious(objUser.adsPath) <> strStatus Then
				WScript.Echo objUser.DisplayName & " has been changed from " & dictPrevious(objUser.adsPath) & " to " & strStatus
			End If
		End If
	End If
	rsUsers.MoveNext
Wend
rsUsers.Close
objConnection.Close
' Now write the current status back to the log file
Set objFile = objFSO.CreateTextFile(strLogFile, True)
For Each strUser In dictCurrent
	objFile.WriteLine strUser & ";" & dictCurrent(strUser)
Next
objFile.Close
WScript.Echo "Script has finished. You can check the account status in " & strLogFile

Open in new window

0
 

Author Comment

by:itsmevic
ID: 24173972
Ok sounds good, I'm going to test this on Monday and will post back then and let you know.  Thanks again for all of your hard work man.
0
 

Author Comment

by:itsmevic
ID: 24186023
I see what your saying about needing to utilize a third party utility for more high level monitoring.  In the meantime, I've ran your latest script and I'm getting the following error:

Script C:\Documents and Settings\MyPC\Desktop\CheckMemberStatus.vbs
Line:  34
Char: 1
Error:  Table does not exist.
Code:  80040E37
Source: Provider

I guess we could just run this via a Windows Schedule task on an hourly basis, if it detected a change it would send out an alert via email.  I like the fact to that it appears to create a log file.  We also have two reports that are ran that show accounts that need to be suspended and accts that need to be disabled, if we could somehow tie these two reports into this script that would be awesome.  Details will follow on that but for now we'll just work on what we are discussing here.  Thanks again for your hard work, hope you had a GREAT weekend!
0
 

Author Comment

by:itsmevic
ID: 24188100
Rob FYI i have another VBScript ques out there...woohoo, just what you wanted to hear...haha.  Basically covers what you worked on before.  
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24189288
Hi there...."table does not exist" errors means that it can't find your OU....from this line:
strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"

the
objRootDSE.Get("defaultNamingContext")

fills in the DC=domain,DC=com part, so you need to change OU=Administrator to point to the correct OU, with the naming being in reverse order.

For example, if you had in the AD
domain.com\Sites\Main Office\Computers

you would make that
strCmd = "<LDAP://OU=Computers,OU=Main Office,OU=Sites," & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"

Regards,

Rob.
0
 

Author Comment

by:itsmevic
ID: 24189523
So let's say from the very most top level

my.domain.com
     Container1
     Container2
     Container3
     so on...
     so on...
     so on...
     
Weird made the changes below and pathed to LDAP properly and it still errored out on me.  Nice.

Same darn error to.  Geez

Line:     34
Char:    1
Error:    Table does not exist
Code:    80040E37
Source: Provider

'strCmd = "<LDAP://DC=MY,DC=DOMAIN,DC=COM," & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
strCmd = "<LDAP://DC=MY,DC=DOMAIN,DC=COM," & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"

Open in new window

0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24189563
Hi, well you don't need to explicitly put in any DC elements, that's already done by the objRootDSE bit, but, if you're looking at only the root domain level, you're going to enumate every single user on your domain......

I thought you had
domain.com
    OU=ADMINISTRATOR
          OU=Container1
          OU=Container2

So if you've got the containers directly under the root level, we'll probably have to include the actual container names into the code to filter the user accounts.....

Regards,

Rob.
0
 

Author Comment

by:itsmevic
ID: 24191375
Hopefully this will give you a better over-view.  I used the minus (-) to illustrate the tree being expanded.
I confirmed that the LDAP path was in this format:  LDAP://DC=TEST,DC=DOMAIN,DC=COM but like you said since the objRootDSE is there perhaps I will not have to do this path.

-  TEST.DOMAIN.COM  <-----------TOP LEVEL
    - OU=Administrators  
                       <----container 1
                       <----container 2
                       <----container 3
    -  OU=Service Accounts
                       <-----container 1
                       <-----container 2
                       <-----container 3
    -  OU=Printers
                       <-----container 1
                       <-----container 2
                       <-----container 3
    -  OU=Deactivated Accts
                      <-----container 1
                      <-----container 2
                      <-----container 3
       
I'm thinking will need to include like you said every OU and container underneath the OU.  Once we have the code, if you just tell me where to embed them at I can do that for you, no prob.  I think I can handle that much...hehe.  I hope this helps you Rob, let me know if you need anything else.
0
 

Author Comment

by:itsmevic
ID: 24191381
Your teaching me a lot here by the way, thank you ( :
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24191759
Actually, I'm confused....back in post ID: 24165156, you said that worked, giving you the status of each account.  Now, with the path the same, it doesn't work, which is odd.

How about, above this line:
Set rsUsers = objConnection.Execute(strCmd)

you put this
MsgBox "Looking in" & vbCrLf & strCmd

so that you can see which domain and ou it's querying....perhaps you have a test domain that is not the defaultNamingConext....

Regards,

Rob.
0
 

Author Comment

by:itsmevic
ID: 24195455
<LDAP://OU=Administrators," & objRootDSE.Get("defaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"

That is correct,  sorry for the confusion Rob, i'm trying not to convilute things.    It does provide me with an on screen dialog of names in the "Administrators" OU to click through and gives me their status:ENABLED.  Eeek so many scripts, I'm pulling my hair out...lol.  It appears it does not go in any order either.  For example, I initiate the script it will pull from OU=Administrators, CON=Southeast and just cycle through I'm assuming until it's hit every username name in each container under the Administrators OU.  So yes right now everything is working as needed, it is showing Disabled or Enabled accounts on screen.

Script Logic:   Provide an alert for any disabled User Accouts that have been re-enabled in the ADMINISTRATOR OU and it's containers.  To produce a text file or automated pre-formatted email that provides the details of the find i.e.  What account was enabled, Who enabled the account.  That's pretty much it in a nutshell.        

 
TEST.DOMAIN.COM  <-----------TOP LEVEL
    - OU=ADMINISTRATORS  
                       -  Department 1<-----------------------------------(CONTAINER 1)
                                     Southwest <----------(Subcontainer)
                                     Northwest <----------(Subcontainer)
                                     Southeast <-----------(Subcontainer)
                        - Department 2 <----------------------------------(CONTAINER 2)
                                     Southwest <--------(Subcontainer)
                                     Northeast <---------(Subcontainer)
                                     West <---------------(Subcontainer)
                                     East <----------------(Subcontainer)
   
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24199337
Yeah, I know what you mean about "so many scripts"! LOL!

So the script in ID: 24172908 is just an extension of the one that worked, using the same LDAP query, so it really should work.  It should create a text file with the Enabled or Disabled status of each account.

Then the next time it runs, it will check against that text file.

So try making sure you've got OU=ADMINISTATORS, in that LDAP query, and it should work....

Regards,

Rob.
0
 

Author Comment

by:itsmevic
ID: 24200008
Hi Rob, I'll run the script, it will run for a few seconds and error out with the below message.  It appears to be making past the LDAP://OU= part fine now that I've changed it.
1.bmp
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24200271
OK, so have you got other objects in those containers that are not user objects? If so, "userAccountControl" may not exist for those.  To get around that, we can only obtain "userAccountControl" for objects of the "user" class.

Change your While loop from this:

While Not rsUsers.EOF
      Set objUser = GetObject(rsUsers.fields("adsPath"))
      intUAC = objUser.Get("userAccountControl")
      If intUAC And ADS_UF_ACCOUNTDISABLE Then
            strStatus = "Disabled"
      Else
            strStatus = "Enabled"
      End If
      dictCurrent.Add objUser.AdsPath, strStatus
      If objFSO.FileExists(strLogFile) = True Then
            If dictPrevious.Exists(objUser.adsPath) = True Then
                  If dictPrevious(objUser.adsPath) <> strStatus Then
                        WScript.Echo objUser.DisplayName & " has been changed from " & dictPrevious(objUser.adsPath) & " to " & strStatus
                  End If
            End If
      End If
      rsUsers.MoveNext
Wend


While Not rsUsers.EOF
      Set objUser = GetObject(rsUsers.fields("adsPath"))
      If objUser.Class = "user" Then
            intUAC = objUser.Get("userAccountControl")
            If intUAC And ADS_UF_ACCOUNTDISABLE Then
                  strStatus = "Disabled"
            Else
                  strStatus = "Enabled"
            End If
            dictCurrent.Add objUser.AdsPath, strStatus
            If objFSO.FileExists(strLogFile) = True Then
                  If dictPrevious.Exists(objUser.adsPath) = True Then
                        If dictPrevious(objUser.adsPath) <> strStatus Then
                              WScript.Echo objUser.DisplayName & " has been changed from " & dictPrevious(objUser.adsPath) & " to " & strStatus
                        End If
                  End If
            End If
      End If
      rsUsers.MoveNext
Wend


Regards,

Rob.
0
 

Author Comment

by:itsmevic
ID: 24206202
Ug!

Still erroring out for some reason.  I read over the script looked for anything that should be there such as spaces, correct LDAP syntax, misspellings ect. didn't didn't find anything.  Getting error on:

Line:  40
Char: 13
Error:  The directory property cannot be found in the cache
Code:  8000500D

Found this on that error, and that's what I bounced my check off of.

http://www.computerperformance.co.uk/Logon/code/code_8000500D.htm

Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open("Ads Provider")
 
Set rsUsers = CreateObject("ADODB.Recordset")                                        
 
Set objRootDSE = GetObject("LDAP://RootDSE")
 
strLogFile = "\\my-server\mylogs\AdminStatus.log"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Const intForReading = 1
 
Set dictPrevious = CreateObject("Scripting.Dictionary")
Set dictCurrent = CreateObject("Scripting.Dictionary")
 
' First read the previous status
If objFSO.FileExists(strLogFile) = True Then
        Set objFile = objFSO.OpenTextFile(strLogFile, intForReading, False)
        While Not objFile.AtEndOfStream
                strLine = objFile.ReadLine
                If Trim(strLine) <> "" Then
                        dictPrevious.Add Split(strLine, ";")(0), Split(strLine, ";")(1)
                End If
        Wend
        objFile.Close
End If
 
strFilter = "(&(objectCategory=user))"
'strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
               
Const ADS_UF_ACCOUNTDISABLE = 2
 
Set rsUsers = objConnection.Execute(strCmd)
'Loop through recordset to check the status
rsUsers.MoveFirst
While Not rsUsers.EOF
      Set objUser = GetObject(rsUsers.fields("adsPath"))
      If objUser.Class = "user" Then
            intUAC = objUser.Get("userAccountControl")
            If intUAC And ADS_UF_ACCOUNTDISABLE Then
                  strStatus = "Disabled"
            Else
                  strStatus = "Enabled"
            End If
            dictCurrent.Add objUser.AdsPath, strStatus
            If objFSO.FileExists(strLogFile) = True Then
                  If dictPrevious.Exists(objUser.adsPath) = True Then
                        If dictPrevious(objUser.adsPath) <> strStatus Then
                              WScript.Echo objUser.DisplayName & " has been changed from " & dictPrevious(objUser.adsPath) & " to " & strStatus
                        End If
                  End If
            End If
      End If
      rsUsers.MoveNext
Wend
 
rsUsers.Close
objConnection.Close
' Now write the current status back to the log file
Set objFile = objFSO.CreateTextFile(strLogFile, True)
For Each strUser In dictCurrent
        objFile.WriteLine strUser & ";" & dictCurrent(strUser)
Next
objFile.Close
WScript.Echo "Script has finished. You can check the account status in " & strLogFile

Open in new window

0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24211026
Hi, that's very strange....the code works perfectly for me.  Ok, try this...I've added more output now, so it will tell you what account it is about to bind to to get the userAccountControl property, and we can see if there's anything odd about that object.....like whether it's a user object or not....

Regards,

Rob.
If LCase(Right(Wscript.FullName, 11)) = "wscript.exe" Then
    strPath = Wscript.ScriptFullName
    strCommand = "%comspec% /k cscript  """ & strPath & """"
    Set objShell = CreateObject("Wscript.Shell")
    objShell.Run(strCommand), 1, True
    Wscript.Quit
End If
 
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open("Ads Provider")
 
Set rsUsers = CreateObject("ADODB.Recordset")                                        
 
Set objRootDSE = GetObject("LDAP://RootDSE")
 
strLogFile = "\\my-server\mylogs\AdminStatus.log"
 
Set objFSO = CreateObject("Scripting.FileSystemObject")
Const intForReading = 1
 
Set dictPrevious = CreateObject("Scripting.Dictionary")
Set dictCurrent = CreateObject("Scripting.Dictionary")
 
' First read the previous status
If objFSO.FileExists(strLogFile) = True Then
        Set objFile = objFSO.OpenTextFile(strLogFile, intForReading, False)
        While Not objFile.AtEndOfStream
                strLine = objFile.ReadLine
                If Trim(strLine) <> "" Then
                        dictPrevious.Add Split(strLine, ";")(0), Split(strLine, ";")(1)
                End If
        Wend
        objFile.Close
End If
 
strFilter = "(&(objectCategory=user))"
'strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
               
Const ADS_UF_ACCOUNTDISABLE = 2
 
Set rsUsers = objConnection.Execute(strCmd)
'Loop through recordset to check the status
rsUsers.MoveFirst
While Not rsUsers.EOF
      WScript.Echo "About to bind to: " & rsUsers.fields("adsPath") & VbCrLf
      Set objUser = GetObject(rsUsers.fields("adsPath"))
      If objUser.Class = "user" Then
            intUAC = objUser.Get("userAccountControl")
            If intUAC And ADS_UF_ACCOUNTDISABLE Then
                  strStatus = "Disabled"
            Else
                  strStatus = "Enabled"
            End If
            dictCurrent.Add objUser.AdsPath, strStatus
            If objFSO.FileExists(strLogFile) = True Then
                  If dictPrevious.Exists(objUser.adsPath) = True Then
                        If dictPrevious(objUser.adsPath) <> strStatus Then
                              WScript.Echo objUser.DisplayName & " has been changed from " & dictPrevious(objUser.adsPath) & " to " & strStatus
                        End If
                  End If
            End If
      End If
      rsUsers.MoveNext
Wend
 
rsUsers.Close
objConnection.Close
' Now write the current status back to the log file
Set objFile = objFSO.CreateTextFile(strLogFile, True)
For Each strUser In dictCurrent
        objFile.WriteLine strUser & ";" & dictCurrent(strUser)
Next
objFile.Close
WScript.Echo "Script has finished. You can check the account status in " & strLogFile

Open in new window

0
 

Author Comment

by:itsmevic
ID: 24216594
Hi Rob, very cool output you've added.  Yes it opens in a Dos prompt window and cycles through the list of users fine.  Going through the list of users that it pulled up they appear fine to me and I didn't see anything out of the ordinary.  I see though it does end with the line:

c:\documents and settings\mypc\desktop>CheckAdmin-AcctsEnabled-Disabled.by.Admins.vbs<50, 13> Active Directory:  The directory property cannot be found in the cache
0
 

Author Comment

by:itsmevic
ID: 24220352
After further research the AD tree is broken into nothing but OU's, there doesn't appear to be containers.  Under the Administrator OU, there are 8 other OU's underneath it that representing business types, then under those are the users.  I say this because, when the I ran the script in the command prompt window I noticed this:

AD reads top level to lower level reads from right to left, so the left most OU is the parent OU.

About to bind to:  LDAP://CN=John Doe, OU=Disabled,OU=Test,OU=Users,OU=Unit1,DC=test,DC=domain,DC=com
About to bind to:  LDAP://CN=John Smith,OU=ToBeDeleted,OU=Test,OU=Users,OU=Unit2,DC=test,DC=domain,DC=com
and so on.

I also just playing around changed the script to read a particular OU under the parent OU of Administrator and got back a ton more results than when I ran it from the top level OU Administrators.  

So I'm thinking if we could create some sort of trigger that search ALL OU's under the PARENT Administrator OU we might get this to work properly.  

Hopefully, this will help you a little bit.
0
 

Author Comment

by:itsmevic
ID: 24220383
Administrators  <---Top Level OU
      - Unit 1 <---------OU  
             John Doe
             John Smith
             Sally Joe
             Joe Blow
             and so on....
      - Unit 2 <--------OU
             Computer User 1
             Computer User 2
             Computer User 3
             Computer User 4
             and so on....

          This same format continues under the Parent OU Administrator 6 more times....



 
   
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24221394
Hi, OK, so we get some output, which is good, but we still need to find out which object the error is coming from.

With the output it now produces, you get this line:
c:\documents and settings\mypc\desktop>CheckAdmin-AcctsEnabled-Disabled.by.Admins.vbs<50, 13> Active Directory:  The directory property cannot be found in the cache

which was expected. Now though, with the output, the line immediately above that one, that reads:
About to bind to:  LDAP://CN=John Smith,OU=ToBeDeleted,OU=Test,OU=Users,OU=Unit2,DC=test,DC=domain,DC=com

will give us the adsPath of the object where the "property cannot be found". Is there anything unusual about this object (is it a user?) that might cause this error?

Also, onto your other issue.....in this query:
strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"

You're specifying to *start* the search at the OU=Administrator OU, which is fine.  Then at the end of that string, you see ";subtree", which means that every object under that tree heirarchy (all the children of that OU) will be enumerated.

So if the Administrator OU *is* the parent of Unit 1 and Unit 2, then they will get searched, and so on....

Regards,

Rob.
0
 

Author Comment

by:itsmevic
ID: 24222026
Ok cool, thanks for explaining how the script flows through the tree.  I will diffinetely look at this in the A.M. on the "About bind to path" thing and report my findings to you.  Boy this one particular script is a toughy.
0
 

Author Comment

by:itsmevic
ID: 24225500
Really weird Rob?  Get this, so I ran the script again this morning to produce the output again, looked at the entry about the last line where it stops, the format of that line was exactly as such:

About to bind to: LDAP://John Doe (Admin), OU=CAL,OU=SAL,OU=Administrator,DC=UN,DC=TEST,DC=COM

Ok, with that I scroll up the command prompt window to another user that's in the same exact OU's and path, when comparing them both, there is absolutely nothing different about the user's, nothing at all.  I even went in as far as going into AD pulling both those user's property values up and bouncing them off each other, they are both in fact actual objects/users with nothing different about them in their canonical names.
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24237927
OK, that is odd!  Well, let's make the script ignore that error (but tell you about it at the DOS prompt), and continue...

Regards,

Rob.
If LCase(Right(Wscript.FullName, 11)) = "wscript.exe" Then
    strPath = Wscript.ScriptFullName
    strCommand = "%comspec% /k cscript  """ & strPath & """"
    Set objShell = CreateObject("Wscript.Shell")
    objShell.Run(strCommand), 1, True
    Wscript.Quit
End If
 
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open("Ads Provider")
 
Set rsUsers = CreateObject("ADODB.Recordset")                                        
 
Set objRootDSE = GetObject("LDAP://RootDSE")
 
strLogFile = "\\my-server\mylogs\AdminStatus.log"
 
Set objFSO = CreateObject("Scripting.FileSystemObject")
Const intForReading = 1
 
Set dictPrevious = CreateObject("Scripting.Dictionary")
Set dictCurrent = CreateObject("Scripting.Dictionary")
 
' First read the previous status
If objFSO.FileExists(strLogFile) = True Then
        Set objFile = objFSO.OpenTextFile(strLogFile, intForReading, False)
        While Not objFile.AtEndOfStream
                strLine = objFile.ReadLine
                If Trim(strLine) <> "" Then
                        dictPrevious.Add Split(strLine, ";")(0), Split(strLine, ";")(1)
                End If
        Wend
        objFile.Close
End If
 
strFilter = "(&(objectCategory=user))"
'strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
               
Const ADS_UF_ACCOUNTDISABLE = 2
 
Set rsUsers = objConnection.Execute(strCmd)
'Loop through recordset to check the status
rsUsers.MoveFirst
While Not rsUsers.EOF
	WScript.Echo "About to bind to: " & rsUsers.fields("adsPath") & VbCrLf
	Set objUser = GetObject(rsUsers.fields("adsPath"))
	If objUser.Class = "user" Then
		On Error Resume Next
		intUAC = objUser.Get("userAccountControl")
		If Err.Number = 0 Then
			On Error GoTo 0
			If intUAC And ADS_UF_ACCOUNTDISABLE Then
				strStatus = "Disabled"
			Else
				strStatus = "Enabled"
			End If
			dictCurrent.Add objUser.AdsPath, strStatus
			If objFSO.FileExists(strLogFile) = True Then
				If dictPrevious.Exists(objUser.adsPath) = True Then
					If dictPrevious(objUser.adsPath) <> strStatus Then
						WScript.Echo objUser.DisplayName & " has been changed from " & dictPrevious(objUser.adsPath) & " to " & strStatus
					End If
				End If
			End If
		Else
			Err.Clear
			On Error GoTo 0
			WScript.Echo "ERROR: Cannot read UserAccountControl for " & objUser.DisplayName
		End If
	End If
	rsUsers.MoveNext
Wend
 
rsUsers.Close
objConnection.Close
' Now write the current status back to the log file
Set objFile = objFSO.CreateTextFile(strLogFile, True)
For Each strUser In dictCurrent
        objFile.WriteLine strUser & ";" & dictCurrent(strUser)
Next
objFile.Close
WScript.Echo "Script has finished. You can check the account status in " & strLogFile

Open in new window

0
 

Author Comment

by:itsmevic
ID: 24261942
Yes that appeared to scan through much more now that we've set it to ignore that particular error.  Now it scans and stops on this:


About to bind to:  LDAP://CN=John Doe-ADM,OU=Biz1,OU=LocalServerAdmins,OU=Biz2,OU=Administrators,DC=test,DC=test,DC=com
C:\documents and settings\mypc\desktop\adminacctsOU.vbs(79, 1) Microsoft VBScript runtime error: Path not found

Looks like we've almost got it, (fingers crossed).  Hope you are well Rob, haven't spoken to you in a couple of days.

0
 

Author Comment

by:itsmevic
ID: 24262381
UPDATE:  Rob, I went back in to check the strLogFile = "\\myserver\logs\AdminStatus.log" path, I had a typo there and fixed it and now the script goes through the entire OU structure without erroring out...WAAAHOOOO!

It creates the log file fine and shows all the accounts.  

NEXT STAGE (hopefully last)

For it to send out an alert if it detects any disabled accts that have been set to enabled and by whom, if that is possible.  What I'll do is set a Windows Scheduled Task for this to run on an hourly basis that way it should be able to detect any status changes.  

GREAT WORK SO FAR Rob!
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24277086
OK, so if you change the email variables at the top of this....see if it sends you an email.

Regards,

Rob.
If LCase(Right(Wscript.FullName, 11)) = "wscript.exe" Then
    strPath = Wscript.ScriptFullName
    strCommand = "%comspec% /k cscript  """ & strPath & """"
    Set objShell = CreateObject("Wscript.Shell")
    objShell.Run(strCommand), 1, True
    Wscript.Quit
End If
 
' Email variables:
strServer = "smtpserver.domain.com"
strTo = "person.toalert@domain.com"
strFrom = "administrator@domain.com"
strSubject = "Change in User Status"
strBody = "Please see the change in user status accounts below:" & VbCrLf
 
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open("Ads Provider")
 
Set rsUsers = CreateObject("ADODB.Recordset")                                        
 
Set objRootDSE = GetObject("LDAP://RootDSE")
 
strLogFile = "\\my-server\mylogs\AdminStatus.log"
 
Set objFSO = CreateObject("Scripting.FileSystemObject")
Const intForReading = 1
 
Set dictPrevious = CreateObject("Scripting.Dictionary")
Set dictCurrent = CreateObject("Scripting.Dictionary")
 
' First read the previous status
If objFSO.FileExists(strLogFile) = True Then
        Set objFile = objFSO.OpenTextFile(strLogFile, intForReading, False)
        While Not objFile.AtEndOfStream
                strLine = objFile.ReadLine
                If Trim(strLine) <> "" Then
                        dictPrevious.Add Split(strLine, ";")(0), Split(strLine, ";")(1)
                End If
        Wend
        objFile.Close
End If
 
strFilter = "(&(objectCategory=user))"
'strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
               
Const ADS_UF_ACCOUNTDISABLE = 2
 
Set rsUsers = objConnection.Execute(strCmd)
'Loop through recordset to check the status
rsUsers.MoveFirst
While Not rsUsers.EOF
	WScript.Echo "About to bind to: " & rsUsers.fields("adsPath") & VbCrLf
	Set objUser = GetObject(rsUsers.fields("adsPath"))
	If objUser.Class = "user" Then
		On Error Resume Next
		intUAC = objUser.Get("userAccountControl")
		If Err.Number = 0 Then
			On Error GoTo 0
			If intUAC And ADS_UF_ACCOUNTDISABLE Then
				strStatus = "Disabled"
			Else
				strStatus = "Enabled"
			End If
			dictCurrent.Add objUser.AdsPath, strStatus
			If objFSO.FileExists(strLogFile) = True Then
				If dictPrevious.Exists(objUser.adsPath) = True Then
					If dictPrevious(objUser.adsPath) <> strStatus Then
						WScript.Echo objUser.DisplayName & " has been changed from " & dictPrevious(objUser.adsPath) & " to " & strStatus
						strBody = strBody & VbCrLf & objUser.DisplayName & " has been changed from " & dictPrevious(objUser.adsPath) & " to " & strStatus & vbCrLf
					End If
				End If
			End If
		Else
			Err.Clear
			On Error GoTo 0
			WScript.Echo "ERROR: Cannot read UserAccountControl for " & objUser.DisplayName
		End If
	End If
	rsUsers.MoveNext
Wend
 
' Now send the file
SendEmail strServer, strTo, strFrom, strSubject, strBody
WScript.Echo "Email has been sent."
 
rsUsers.Close
objConnection.Close
' Now write the current status back to the log file
Set objFile = objFSO.CreateTextFile(strLogFile, True)
For Each strUser In dictCurrent
        objFile.WriteLine strUser & ";" & dictCurrent(strUser)
Next
objFile.Close
WScript.Echo "Script has finished. You can check the account status in " & strLogFile
 
Sub SendEmail(strServer, strTo, strFrom, strSubject, strBody)
	Dim objMessage
	
	Set objMessage = CreateObject("CDO.Message")
	objMessage.To = strTo
	objMessage.From = strFrom
	objMessage.Subject = strSubject
	objMessage.TextBody = strBody
 
	'==This section provides the configuration information for the remote SMTP server.
	objMessage.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
	'Name or IP of Remote SMTP Server
	objMessage.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = strServer
	'Server port (typically 25)
	objMessage.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25	
	objMessage.Configuration.Fields.Update
	'==End remote SMTP server configuration section==
 
	objMessage.Send
	Set objMessage = Nothing
End Sub

Open in new window

0
 

Author Comment

by:itsmevic
ID: 24283713
Hi Rob,

   Here are the latest results.

I'm able to run the code, it cycles through the accounts fine and will then error out with the following:

\\myserver\logs\Scripts\Admin Accounts OU\AdminAcctsOU.vbs<109, 8> CDO.Message.1: The system cannot find the file specified.

To interprete this, I'm thinking that the script is unable to find the "AdminStatus.log" because it's not being written for some reason like it was before.

Ok, moving on.  If I manually create a AdminStatus.log file, it will run through the cycle of accounts and then produce this error:

\\myserver\logs\Scripts\Admin Accounts OU\AdminAcctsOU.vbs<120, 6> <null>: The message could not be sent to the SMTP server.  The transport error code was 0x80070057.  The server response was not available.

So close ( :
0
 

Author Comment

by:itsmevic
ID: 24283726
I have checked the smtp name and have verified that.  I have also verified the paths, they are correct and lastly, I have the appropriate permissions to the path...so i'm not sure what could be causing this?
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24292025
Hmmm, both errors seem to be SMTP transport errors....

The machine that you run the script from....is that machine configured on the mail server to allow relaying?  I'm pretty sure that's required.....try that.....Also, the strServer at the top should be set to the FQDN of the SMTP server.

Regards.

Rob.
0
 

Author Comment

by:itsmevic
ID: 24295441
Woohoo it works! You were right it was a relaying issue.  I ran the script directly from the log server which I know has relaying permissions turned on and it worked! Good news!  The text report that it sent did not contain any information though but I'm assuming your just approaching this step-by-step.  Very happy indeed thus far!
0
 

Author Comment

by:itsmevic
ID: 24295487
Belay that last comment Rob, the test file does contain information.  I guess our next step from here would be for it to detect and flag any changes.  Like I said, what I'll do is run it on a hourly basis, if JDOE was disabled at 10am and appears enabled at 11am when the script is ran, can we get it to flag that and show who made the changes?
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24302010
Hi, we can't see *who* made the changes......the AD doesn't hold that sort of information.  We can only see whether the property was changed since we last ran the script (and created the log file).

So, just to test, if you run it once to create the log file with the current status of users, that will record the status as of now.  Change one of the accounts to disabled, and run the script again. Hopfefully, the email that you get should tell that that account had been changed since the last run.

Regards,

Rob.
0
 

Author Comment

by:itsmevic
ID: 24304752
Ok so we are done basically then correct? I suppose, I could look at the Authorization logs for further information if in fact the script does pick up a user that has been enabled.
0
 

Author Comment

by:itsmevic
ID: 24306459
I guess the only draw back to this is that I will have to compare both text files side-by-side and detect any differences that way.  I wish there were a way to take a snap shot of the first txt file and for it to detect variances off the second text file or something to that effect.
0
 

Author Comment

by:itsmevic
ID: 24309293
Hi Rob, I just finished a meeting up with one of the Senior Engineers here, he says that what I'm needing is doable and mapped out the plan of attack to do it below.  I understand the logic by reading his statements but would need help in tieing it all together with the present code set that we have.

After speaking to one of the Senior Engineers that I work with here, he says this can be done and he basically mapped out what type of logic I would need for this script...I just need help putting this all together.  Below was his input on the logic of the script.  I've also attached my present script.

SCRIPT LOGIC
******************
1.  Create an ADODB Disconnected Recordset OldRS 'record set
     a.  Two columns(1) - OldAccount and (2) entire rcord line (Oldline)
     b.  Load old text file into OldRS
     c.  Sort OldRS by OldAccount
     d.  Position to 1st record

2. Create an ADODB Disconnected Recordset NewRS
      a.  Two columns(1) -newAccount and (2) entire record line (NewLine)
      b.  Load old text file into NewRS
      c.  Sort NewRS by NewAccount
      d.  Position to 1st record

3.  Read next OldRS record

4.  Read next New RS record

5. If OldAccount = NewAccount then

          if Old Line = NewLine
                 (nothing has changed for this account, so move on)
                  Read next OldRS
                  Read next NewRS

          Else
   
                 This account has changed so write it out NewLine & ("Changed") to email file
                 Read next OldRS
                 Read next NewRS

         Else

                 If oldAccount < NewAccount (means OldAccount has been deleted)
 
                           Old account has been deleted so write it out OldLine & ("Deleted") to email file
                  Read next OldRS

         Else (means OldAccount > NewAccount means NewAccount has been added)

                  New account has been added so write it out NewLine & ("Added") to email file
                 Read next New RS

5.           Loop until both OldRS and NewRS are EOF

6.     Mail out mail file

7.   Delete oldfile

8 .  Rename newfile to oldfile (preparation for next run)

9.  Finish
0
 
LVL 65

Accepted Solution

by:
RobSampson earned 2000 total points
ID: 24310380
Hi there,

The script that I provided does pretty much exactly that....(slightly different, but pretty close), by using dictionary object, instead of disconnected recordsets, but it still works.

You can see by the log file that it creates, that it stores the adsPath, and the Status, separated by a semi-colon.  This is the flat text file database that is created when you first run the script, and re-created on subsequent runs.

When the script starts it reads the log file into the "Previous accounts" dictionary object, then enumerates the OUs to read the "Current accounts" adsPaths, loading then into a "Current accounts" dictionary object at the same time.

Each current account is checked for existence in the previous list, and that determines whether an acount is now new, or has has its status modified.

After that, it compares the current list with the previous list, to determine whether an account has been deleted (ie. no longer exists in the current list).

These changes are appended to the body text of the email, and if no changes have been made, no email is sent.

I hope this helps....it should work the way you have asked....it appears to work fine in my environment....

Regards,

Rob.
If LCase(Right(Wscript.FullName, 11)) = "wscript.exe" Then
    strPath = Wscript.ScriptFullName
    strCommand = "%comspec% /k cscript  """ & strPath & """"
    Set objShell = CreateObject("Wscript.Shell")
    objShell.Run(strCommand), 1, True
    Wscript.Quit
End If
 
' Email variables:
strServer = "smtpserver.domain.com"
strTo = "person.toalert@domain.com"
strFrom = "administrator@domain.com"
strSubject = "Change in User Status"
strBody = "Please see the change in user status accounts below:" & VbCrLf
 
' Obtain local Time Zone bias from machine registry.
Set objShell = CreateObject("Wscript.Shell")
lngBiasKey = objShell.RegRead("HKLM\System\CurrentControlSet\Control\" _
    & "TimeZoneInformation\ActiveTimeBias")
If (UCase(TypeName(lngBiasKey)) = "LONG") Then
    lngBias = lngBiasKey
ElseIf (UCase(TypeName(lngBiasKey)) = "VARIANT()") Then
    lngBias = 0
    For k = 0 To UBound(lngBiasKey)
        lngBias = lngBias + (lngBiasKey(k) * 256^k)
    Next
End If
 
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open("Ads Provider")
 
Set rsUsers = CreateObject("ADODB.Recordset")                                        
 
Set objRootDSE = GetObject("LDAP://RootDSE")
 
strLogFile = "\\my-server\mylogs\AdminStatus.log"
 
Set objFSO = CreateObject("Scripting.FileSystemObject")
Const intForReading = 1
 
Set dictPrevious = CreateObject("Scripting.Dictionary")
Set dictCurrent = CreateObject("Scripting.Dictionary")
 
' First read the previous status
If objFSO.FileExists(strLogFile) = True Then
        Set objFile = objFSO.OpenTextFile(strLogFile, intForReading, False)
        While Not objFile.AtEndOfStream
                strLine = objFile.ReadLine
                If Trim(strLine) <> "" Then
                        dictPrevious.Add Split(strLine, ";")(0), Split(strLine, ";")(1)
                End If
        Wend
        objFile.Close
End If
 
strFilter = "(&(objectCategory=user))"
strCmd = "<LDAP://OU=Administrator," & objRootDSE.Get("DefaultNamingContext") & ">;" & strFilter & ";adsPath;subtree"
               
Const ADS_UF_ACCOUNTDISABLE = 2
 
Set rsUsers = objConnection.Execute(strCmd)
'Loop through recordset to check the status
boolChanges = False
rsUsers.MoveFirst
While Not rsUsers.EOF
	'WScript.Echo "About to bind to: " & rsUsers.fields("adsPath") & VbCrLf
	Set objUser = GetObject(rsUsers.fields("adsPath"))
	If objUser.Class = "user" Then
		On Error Resume Next
		intUAC = objUser.Get("userAccountControl")
		If Err.Number = 0 Then
			On Error GoTo 0
			If intUAC And ADS_UF_ACCOUNTDISABLE Then
				strStatus = "Disabled"
			Else
				strStatus = "Enabled"
			End If
			dictCurrent.Add objUser.AdsPath, strStatus
			If objFSO.FileExists(strLogFile) = True Then
				If dictPrevious.Exists(objUser.adsPath) = True Then
					If dictPrevious(objUser.adsPath) <> strStatus Then
						boolChanges = True
						strWhenChanged = DateAdd("n", -lngBias, objUser.whenChanged)
						strBody = strBody & VbCrLf & objUser.DisplayName & " has been changed from " & dictPrevious(objUser.adsPath) & " to " & strStatus & " at around " & strWhenChanged & VbCrLf
					End If
				Else
					boolChanges = True
					strWhenChanged = DateAdd("n", -lngBias, objUser.whenChanged)
					strBody = strBody & VbCrLf & objUser.DisplayName & " has been added at around " & strWhenChanged & VbCrLf
				End If
			End If
		Else
			Err.Clear
			On Error GoTo 0
			WScript.Echo "ERROR: Cannot read UserAccountControl for " & objUser.DisplayName
		End If
	End If
	rsUsers.MoveNext
Wend
For Each strUser In dictPrevious
	'WScript.Echo VbCrLf & "Checking if " & strUser & " has been deleted..."
	If dictCurrent.Exists(strUser) = False Then
		boolChanges = True
		strBody = strBody & VbCrLf & strUser & " has been deleted." & VbCrLf		
	End If
Next
 
' Now send the file
If boolChanges = True Then
	'SendEmail strServer, strTo, strFrom, strSubject, strBody
	WScript.Echo VbCrLf & VbCrLf & strBody & VbCrLf & VbCrLf
	WScript.Echo "Email has been sent."
Else
	WScript.Echo "No account changes have been made."
End If
 
rsUsers.Close
objConnection.Close
' Now write the current status back to the log file
Set objFile = objFSO.CreateTextFile(strLogFile, True)
For Each strUser In dictCurrent
        objFile.WriteLine strUser & ";" & dictCurrent(strUser)
Next
objFile.Close
WScript.Echo "Script has finished. You can check the account status in " & strLogFile
 
Sub SendEmail(strServer, strTo, strFrom, strSubject, strBody)
	Dim objMessage
	
	Set objMessage = CreateObject("CDO.Message")
	objMessage.To = strTo
	objMessage.From = strFrom
	objMessage.Subject = strSubject
	objMessage.TextBody = strBody
 
	'==This section provides the configuration information for the remote SMTP server.
	objMessage.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
	'Name or IP of Remote SMTP Server
	objMessage.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = strServer
	'Server port (typically 25)
	objMessage.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25	
	objMessage.Configuration.Fields.Update
	'==End remote SMTP server configuration section==
 
	objMessage.Send
	Set objMessage = Nothing
End Sub

Open in new window

0
 

Author Comment

by:itsmevic
ID: 24311337
Much thanks Rob, thanks for sticking with me on this long project to...I'm gonna test this in the A.M. for sure and will let you know.
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24311360
No problem. Some things do take a long time....especially when I'm as busy as I am at the moment!

Hopefully it works for you.

Regards,

Rob.
0
 

Author Closing Comment

by:itsmevic
ID: 31571202
flawless.  
0
 
LVL 65

Expert Comment

by:RobSampson
ID: 24333493
Thanks for the grade.  Sorry it took so long to have a working solution :-)

Rob.
0

Featured Post

Free Tool: Port Scanner

Check which ports are open to the outside world. Helps make sure that your firewall rules are working as intended.

One of a set of tools we are providing to everyone as a way of saying thank you for being a part of the community.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

Group policies can be applied selectively to specific devices with the help of groups. Utilising this, it is possible to phase-in group policies, over a period of time, by randomly adding non-members user or computers at a set interval, to a group f…
Here's a look at newsworthy articles and community happenings during the last month.
This tutorial will walk an individual through the process of transferring the five major, necessary Active Directory Roles, commonly referred to as the FSMO roles from a Windows Server 2008 domain controller to a Windows Server 2012 domain controlle…
There are cases when e.g. an IT administrator wants to have full access and view into selected mailboxes on Exchange server, directly from his own email account in Outlook or Outlook Web Access. This proves useful when for example administrator want…

839 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question