The other day I had to clean up the groups in our Domino Directory. Many groups still contained names of terminated users, they had not been cleaned/maintained properly. We also removed some groups over time, but some of them were still listed as members of other groups.
So I wrote a small Lotuscript agent to perform this cleanup. I am posting it below in case someone needs it. As always: no guarantees, use at your own risk, etc.
Update: As some commenters pointed out, there are some issues with the code. First of all, I was using a view my company added to name.nsf. The view PeopleByFirstName has the following selection formula: SELECT Type = “Person” & TerminationDate=””
The field TerminationDate is one we added to the person document. This field is either blank (for current employees) or contains the date they were terminated (and then they are filtered out of most views).
Also as Christian points out, any groups that contains external users would be clenaed out. In the past when I worked at a company that used mail groups with external users, those groups were kept in a separate (secondary) Domino Directory, names_ext.nsf. So this is something to keep in mind if you choose to use this code.
Finally I have updated the code with a few more lines. It will now ignore server groups, and also get a list of all servers and make sure they are included as group members when the groups are processed.
Thanks for pointing out the problems with the code!
Dim session As New NotesSession Dim db As NotesDatabase Dim view As NotesView Dim doc As NotesDocument Dim userNames List As String Dim groupNames List As String Dim serverNames List As String Dim groups List As NotesDocument Dim tmp As String Dim tmpname As NotesName Set db = New NotesDatabasesession.CurrentDatabase.Server,"names.nsf") '*** Get all existing groups and put in a list Set view = db.getView("Groups") Set doc = view.GetFirstDocument() Do Until doc Is Nothing Set groups(doc.UniversalID) = doc tmp = doc.GetItemValue("ListName")(0) groupNames(tmp) = tmp Set doc = view.GetNextDocument(doc) Loop '*** Get all active users and put in a list Set view = db.getView("PeopleByFirstName") Set doc = view.GetFirstDocument() Do Until doc Is Nothing tmp = doc.GetItemValue("FullName")(0) Set tmpname = New NotesName(tmp) userNames(tmpname.Common) = tmpname.Common Set doc = view.GetNextDocument(doc) Loop '*** Get all servers and put in a list Set view = db.getView("Servers") Set doc = view.GetFirstDocument() Do Until doc Is Nothing tmp = doc.GetItemValue("ServerName")(0) Set tmpname = New NotesName(tmp) serverNames(tmpsname.Common) = tmpname.Common Set doc = view.GetNextDocument(doc) Loop '*** Loop though list of groups and process each one ForAll g In groups '*** Check that not server group If g.GetItemValue("GroupType")(0)<>"4" Then tmp = "" '*** Store existing list of group members in a backup field Call g.ReplaceItemValue("MembersBackup",g.GetItemValue("Members")) '*** Loop through all members of the group ForAll m In g.GetItemValue("Members") Set tmpname = New NotesName(m) '*** If the name is an existing group or an active user, add to list If IsElement(userNames(tmpname.Common)) _ Or IsElement(groupNames(tmpname.Common)) _ Or IsElement(serverNames(tmpname.Common))Then tmp = tmp + tmpname.Canonical + ";" End If End ForAll '*** Write list back to members field Call g.ReplaceItemValue("Members", FullTrim(Split(tmp,";"))) Call g.Save(True,False) End If End ForAll MsgBox "Done!"
Several problems with the code:
It uses a nonstandard view which is not in a default NAB, ‘PeopleByFirstName’. If I use ($People), it will empty the server groups ‘LocalDomainServers’ and ‘TravelerServers’. Can you please provide the Selection formula for the view ‘PeopleByFirstName’?
Ah, I did not think about that, it’s a view we created internally. Will update the code tomorrow.
so, your script will delete any external users (from Mail lists) and any Notes users from other domains…
… and it is not able to deal with Wildcards (e.g in Access only lists), so they will be deleted, too
… and Servers are also gone from Access lists (assuming your internal view is persons only)
One thing I do when using Lists…. If I only care about the tag (like “userNames” and “serverNames” in your code), then I define the List with a data type of Byte and always set the value to 1. A Byte data type is 1 byte in size and a String data type is 2 bytes per character in size. This can save a lot of memory as the size of the List grows.
Nothing wrong with what you’re doing, but that’s just a thing I do when I only need the tag for “IsElement” purposes.
Very true. Often I use Boolean as the data type in that case.
Booleans are stored as 16bit. So if storage is of the essence, then byte is smallest.
Then again, if processing a NAB with a lot of groups, storing them all as NotesDocument objects can get you into trouble as well…
Thats why Bill Buchan and others recommend not keeping NotesDocuments (or other product objects) in memory but rather custom classes. In lists if theres more of them. Also much quicker when manipulating them, I’ve seen agents crawling along using NotesItem updates on NotesDocument objects, which became speed demons just reading the data needed, then do all processing outside the Notes* objects, only going back once all processing is done.
here’s code which ignores Servergroups, lets external users be, and also ignores group members with a wildcard (*):
%REM
Agent check groups for old users
Created Dec 16, 2015 by L. Berntrop-Bos/Alphen/Zeeman
Description: checks for groups containing domain users no longer active
%END REM
‘ Option Public ‘ deprecated
Option Declare
%REM
Class group
Description: holds group data
%END REM
Class group
Public listName As String
Public members As Variant
Public membersBackup As Variant
Public universalID As String
Public namesRemoved List As String
%REM
Sub New
%END REM
Sub New(ve As NotesViewEntry, tListName$)
listName = tListname
members = FullTrim(ve.Document.GetItemValue(“Members”))
membersBackup = members
universalID = ve.Universalid
End Sub
End Class
Sub Initialize
Dim session As New NotesSession
Dim dbList List As NotesDatabase
Dim db As NotesDatabase
Const test = True
If test Then
Set db = session.GetDatabase(“”, “namesZeeman.nsf”)
Set dbList(db.Server + “!!” + db.FilePath) = db
Else
ForAll pNAB In session.Addressbooks
Set db = pNAB
If db.IsPublicAddressBook Then
Call db.Open(“”, “”)
Set dbList(db.Server + “!!” + db.FilePath) = db
End If
End ForAll
End If
ForAll pubNAB In dbList
Call checkNAB(pubNAB)
End ForAll
Print “Done!”
End Sub
%REM
Sub checkNAB
Description: process one NAB
%END REM
Sub checkNAB(db As NotesDatabase)
Dim nServer As NotesName
Dim view As NotesView
Dim vec As NotesViewEntryCollection
Dim ve As NotesViewEntry
Dim doc As NotesDocument
Dim userNames List As String
Dim groupNames List As String
Dim groups List As group
Dim saveGroups List As group
Dim tmp As String
Dim uname As NotesName
Dim needsSave As Boolean
Dim savedOK As Boolean
Dim allOK As Boolean
Dim ctUpdate As Long
Dim v As Variant
Dim namesRemoved List As Long
Dim lowOrg As String
If db.Server = “” Then ‘ if running local, get organiztion from current user
Set nServer = New NotesName(db.Parent.EffectiveUsername)
Else ‘ get the org from the server
Set nServer = New NotesName(db.Server)
End If
lowOrg = LCase$(nServer.Organization)
‘*** Get all existing groups and put in a list
Set view = db.GetView(“Groups”) ‘ view ignores Deny Access groups
Set vec = view.AllEntries
Set ve = vec.GetfirstEntry
Do Until ve Is Nothing
If ve.IsValid And ve.IsDocument And Not ve.IsConflict Then
If Not CStr(ve.Document.GetItemValue(“GroupType”)(0))=”4″ Then ‘ ignore server groups
tmp = LCase$(ve.ColumnValues(1)) ‘ groupnames are case insensitive
groupNames(tmp) = tmp
Set groups(ve.UniversalID) = New group(ve, tmp)
End If
End If
Set ve = vec.GetNextEntry(ve)
Loop
‘*** Get all active users and put in a list
Set view = db.GetView(“($PeopleGroupsHier)”)
Set vec = view.AllEntries
Set ve = vec.GetfirstEntry
Do Until ve Is Nothing
If ve.IsValid And ve.IsDocument And Not ve.IsConflict Then
v = ve.ColumnValues(4)
If IsArray(v) Then
Set uname = New NotesName(v(0)) ‘ (“FullName”)(0)
Else
Set uname = New NotesName(v) ‘ (“FullName”)(0)
End If
tmp = LCase$(uName.Common)
userNames(tmp) = tmp
End If
Set ve = vec.GetNextEntry(ve)
Loop
allOK = True
‘*** Loop though list of groups and process each one
ForAll g In groups
needsSave = False
‘ *** Loop through all members of the group
If g.members(0) “” Then ‘ skip empty groups
ForAll m In g.Members
Set uname = New NotesName(m)
If LCase(uname.Organization) = lowOrg Then ‘ filter members in our organization (i.e. ignore external users)
tmp = LCase$(uName.Common)
‘*** If the name is not an existing group and not an active user, remove unless it has a wildcard
If Not IsElement(userNames(tmp)) And Not IsElement(groupNames(tmp)) And InStr(m, “*”) = 0 Then
tmp = uName.Abbreviated
g.namesRemoved(tmp) = tmp ‘ keep score in group
If IsElement(namesRemoved(tmp)) Then ‘ also keep global score
namesRemoved(tmp) = namesRemoved(tmp) + 1
Else
namesRemoved(tmp) = 1
End If
NeedsSave = True
m = “”
End If
End If
End ForAll
‘*** Write list back to members field
If needsSave Then
g.members = FullTrim(g.Members)
Set saveGroups(g.universalID) = g
ctUpdate = ctUpdate + 1
End If
End If
End ForAll
‘ Optionally, insert code to display the lists here
‘ for now, I send me a log of the proposed actions
Dim lg As New NotesLog(“log for group check”)
lg.OpenMailLog db.Parent.EffectiveUsername, “log for group check”
lg.LogAction “Nab on ” + db.Server + ” !! ” + db.FilePath
lg.LogAction “.”
lg.LogAction “.”
lg.LogAction “NamesRemoved (name: numTimes)”
lg.LogAction “.”
ForAll nm In namesremoved
lg.Logaction ListTag(nm) + “: ” + CStr(nm)
End ForAll
lg.LogAction “.”
lg.LogAction “.”
lg.LogAction “Groups affected (groupname: removedName(s))”
lg.LogAction “.”
ForAll sg In saveGroups
tmp = “”
ForAll nmr In sg.namesRemoved
tmp = tmp + nmr + “; ”
End ForAll
lg.Logaction sg.Listname + “: ” + tmp
End ForAll
lg.Close
If MsgBox(“Do you want to update ” + CStr(ctUpdate) + ” Group documents in the NAB?”, 4, “Group document update confirmation”) = 6 Then
ForAll g In saveGroups
Set doc = db.GetDocumentByUnid(g.universalID)
Call doc.ReplaceItemValue(“Members”, g.Members) ‘ clear out empties
Call doc.ReplaceItemValue(“MembersBackup”, g.membersBackup)
savedOK = doc.Save(False, False)
If Not savedOK Then
allOK = False
Print “Rerun needed to update group: ” + g.listName
End If
End ForAll
End If
If Not allOK Then MsgBox “One or more groups were not updated because they could not be saved. Please try again.”
End Sub