diff --git a/docs/GamUpdates.md b/docs/GamUpdates.md index de9576a3..4fabe3ea 100644 --- a/docs/GamUpdates.md +++ b/docs/GamUpdates.md @@ -12,6 +12,11 @@ See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Win ### 6.76.06 +Fixed bug in `gam print messages ... positivecountsonly` where message counts with value 0 were deiplayed. + +Added option `addcsvdata ` to `gam print|messages` that adds +additional columns of data to the CSV file output. + Added option `showusagebytes` to `gam print|show drivesettings` that displays the following fields in bytes ```usageBytes,usageInDriveBytes,usageInDriveTrashBytes``` in addition to the fields in their formatted form with units: ```usage,usageInDrive,usageInDriveTrash```. diff --git a/docs/Users-Gmail-Messages-Threads.md b/docs/Users-Gmail-Messages-Threads.md index 250d1771..6b8ed463 100644 --- a/docs/Users-Gmail-Messages-Threads.md +++ b/docs/Users-Gmail-Messages-Threads.md @@ -365,6 +365,10 @@ gam archive messages Messages are archived to the group specified by ``. +When `query` is specified: +* `max_to_archive 0` - All messages selected will be archived; this is the default +* `max_to_archive ` - No messages will be archived if the number messages selected is > `` + By default, the command results are displayed as indented keys and values. Use the `csv` option to display the command results in CSV form. ``` @@ -406,6 +410,10 @@ By default, when exporting a message, an existing local file will not be overwri * `overwrite true` - Overwite an existing file * `overwrite false` - Do not overwite an existing file; add a numeric prefix and create a new file +When `query` is specified: +* `max_to_export 0` - All messages selected will be exported +* `max_to_export ` - No messages will be exported if the number messages selected is > ``; `` defaults to 1. + See below for message selection. ## Forward messages/threads @@ -429,6 +437,10 @@ If `addorigfieldstosubject` is specified, GAM appends the original `from`, `to` Fwd: Ross to TestUser (Original From: Ross Scroggs To: testuser@domain.com Date: Thu, 23 Nov 2023 07:01:59 -0800) ``` +When `query` is specified: +* `max_to_forward 0` - All messages selected will be forwarded +* `max_to_forward ` - No messages will be forwarded if the number messages selected is > ``; `` defaults to 1. + See below for message selection. ## Manage messages/threads @@ -542,7 +554,8 @@ gam print messages|threads [todrive *] ``` ## Display all messages By default, Gam displays all messages. -* `max_to_xxx` - Limit the number of messages that will be displayed +* `max_to_print|max_to_show 0` - All messages will be displayed; this is the default +* `max_to_print|max_to_show ` - Limit the number of messages that will be displayed to `` * `includespamtrash` - Include messages in the Spam and Trash folders ## Display a specific set of messages @@ -550,7 +563,8 @@ By default, Gam displays all messages. ## Display a selected set of messages * `((query [querytime ]*) (matchlabel ) [or|and])+` - Criteria to select messages -* `max_to_xxx` - Limit the number of messages that will be displayed +* `max_to_print|max_to_show 0` - All selected messages will be displayed; this is the default +* `max_to_print|max_to_show ` - Limit the number of selected messages that will be displayed to `` * `includespamtrash` - Include messages in the Spam and Trash folders * `labelmatchpattern ` - Only display messages with some label that matches `` * `labelmatchpattern xyz` - Label must start with xyz diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 5ce3f9a1..16374088 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -7226,9 +7226,10 @@ gam print messages|threads [todrive *] [countsonly|positivecountsonly] [useronly] [headers all|] [dateheaderformat iso|rfc2822| [dateheaderconverttimezone []]] [showlabels] [delimiter ] [showbody] [showdate] [showsize] [showsnippet] + [convertcrnl] [delimiter ] [[attachmentnamepattern ] [showattachments [noshowtextplain]]] - [convertcrnl] + (addcsvdata )* # Users - Gmail - Profile diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 22c0c56d..e23f53f1 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -4,6 +4,11 @@ Merged GAM-Team version 6.76.06 +Fixed bug in `gam print messages ... positivecountsonly` where message counts with value 0 were deiplayed. + +Added option `addcsvdata ` to `gam print|messages` that adds +additional columns of data to the CSV file output. + Added option `showusagebytes` to `gam print|show drivesettings` that displays the following fields in bytes ```usageBytes,usageInDriveBytes,usageInDriveTrashBytes``` in addition to the fields in their formatted form with units: ```usage,usageInDrive,usageInDriveTrash```. diff --git a/src/gam/__init__.py b/src/gam/__init__.py index c4d6c1ea..19355ecf 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -4163,6 +4163,8 @@ def SetGlobalVariables(): GC.Values[GC.PRINT_CROS_OUS] = GM.Globals[GM.PRINT_CROS_OUS] if not GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]: GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN] = GM.Globals[GM.PRINT_CROS_OUS_AND_CHILDREN] + GC.Values[GC.SHOW_GETTINGS] = GM.Globals[GM.SHOW_GETTINGS] + GC.Values[GC.SHOW_GETTINGS_GOT_NL] = GM.Globals[GM.SHOW_GETTINGS_GOT_NL] # customer_id, domain and admin_email must be set when enable_dasa = true if GC.Values[GC.ENABLE_DASA]: errors = 0 @@ -9574,6 +9576,7 @@ def ProcessGAMCommandMulti(pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout, csvHeaderForce, csvRowFilter, csvRowFilterMode, csvRowDropFilter, csvRowDropFilterMode, csvRowLimit, + showGettings, showGettingsGotNL, args): global mplock @@ -9612,6 +9615,8 @@ def ProcessGAMCommandMulti(pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout, GM.Globals[GM.PRINT_CROS_OUS] = printCrosOUs GM.Globals[GM.PRINT_CROS_OUS_AND_CHILDREN] = printCrosOUsAndChildren GM.Globals[GM.SAVED_STDOUT] = None + GM.Globals[GM.SHOW_GETTINGS] = showGettings + GM.Globals[GM.SHOW_GETTINGS_GOT_NL] = showGettingsGotNL GM.Globals[GM.SYSEXITRC] = 0 GM.Globals[GM.PARSER] = None if mpQueueCSVFile: @@ -9818,6 +9823,7 @@ def MultiprocessGAMCommands(items, showCmds): GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE], GC.Values[GC.CSV_OUTPUT_ROW_LIMIT], + GC.Values[GC.SHOW_GETTINGS], GC.Values[GC.SHOW_GETTINGS_GOT_NL], item]) poolProcessResults[0] += 1 if parallelPoolProcesses > 0: @@ -12380,7 +12386,7 @@ def doProcessSvcAcctKeys(mode, iam=None, projectId=None, clientEmail=None, clien new_data['yubikey_serial_number'] = getInteger() else: unknownArgumentExit() - + def waitForCompletion(i): sleep_time = i*5 if i > 3: @@ -25470,6 +25476,11 @@ def _getChatPageMessage(entityType, user, i, count, pfilter): return getPageMessage() CHAT_PAGE_SIZE = 1000 +CHAT_SPACES_ADMIN_ORDERBY_CHOICE_MAP = { + 'createtime': 'createTime', + 'lastactivetime': 'lastActiveTime', + 'membershipcount': 'membershipCount.joined_direct_human_user_count' + } # gam [] show chatspaces # [types ] @@ -25477,15 +25488,34 @@ CHAT_PAGE_SIZE = 1000 # gam [] print chatspaces [todrive *] # [types ] # [formatjson [quotechar ]] +# gam [] show chatspaces adminaccess|asadmin +# [query ]] +# [orderby [ascending|descending]] +# [formatjson] +# gam [] print chatspaces adminaccess|asadmin [todrive *] +# [query ]] +# [orderby [ascending|descending]] +# [formatjson [quotechar ]] def printShowChatSpaces(users): csvPF = CSVPrintFile(['User', 'name'] if not isinstance(users, list) else ['name']) if Act.csvFormat() else None FJQC = FormatJSONQuoteChar(csvPF) + useAdminAccess = checkArgumentPresent(ADMIN_ACCESS_OPTIONS) + OBY = OrderBy(CHAT_SPACES_ADMIN_ORDERBY_CHOICE_MAP) pfilter = '' + kwargs = {} + if not useAdminAccess: + api = API.CHAT_SPACES + function = 'list' + else: + api = API.CHAT_SPACES_ADMIN + function = 'search' + kwargs['useAdminAccess'] = True + kwargs['query'] = 'customer = "customers/my_customer" AND spaceType = "SPACE"' while Cmd.ArgumentsRemaining(): myarg = getArgument() if csvPF and myarg == 'todrive': csvPF.GetTodriveParameters() - elif myarg in {'type', 'types'}: + elif not useAdminAccess and myarg in {'type', 'types'}: for ctype in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split(): if ctype in CHAT_SPACE_TYPE_MAP: if pfilter: @@ -25493,23 +25523,30 @@ def printShowChatSpaces(users): pfilter += f'spaceType = "{CHAT_SPACE_TYPE_MAP[ctype]}"' else: invalidChoiceExit(ctype, CHAT_SPACE_TYPE_MAP, True) + elif useAdminAccess and myarg == 'orderby': + OBY.GetChoice() + elif useAdminAccess and myarg == 'query': + kwargs['query'] += ' AND '+ getString(Cmd.OB_QUERY) else: FJQC.GetFormatJSONQuoteChar(myarg, True) - if not pfilter: - pfilter = None + if not useAdminAccess: + if pfilter: + kwargs['filter'] = pfilter + else: + kwargs['orderBy'] = OBY.orderBy i, count, users = getEntityArgument(users) for user in users: i += 1 - user, chat, kvList = buildChatServiceObject(API.CHAT_SPACES, user, i, count) + user, chat, kvList = buildChatServiceObject(api, user, i, count) if not chat: continue try: - spaces = callGAPIpages(chat.spaces(), 'list', 'spaces', + spaces = callGAPIpages(chat.spaces(), function, 'spaces', pageMessage=_getChatPageMessage(Ent.CHAT_SPACE, user, i, count, pfilter), bailOnInternalError=True, throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION], - pageSize=CHAT_PAGE_SIZE, filter=pfilter) + pageSize=CHAT_PAGE_SIZE, **kwargs) except (GAPI.notFound, GAPI.invalidArgument, GAPI.internalError, GAPI.permissionDenied, GAPI.failedPrecondition) as e: exitIfChatNotConfigured(chat, kvList, str(e), i, count) @@ -43446,7 +43483,7 @@ def doPrintUserList(entityList): def doPrintUserCountsByOrgUnit(): def _printUserCounts(title, v): csvPF.WriteRow({'orgUnitPath': title, 'archived': v['archived'], 'active': v['active'], 'suspended': v['suspended'], 'total': v['total']}) - + USER_COUNTS_FIELDS = ['archived', 'active', 'suspended', 'total'] USER_COUNTS_ZERO_FIELDS = {'archived': 0, 'active': 0, 'suspended': 0, 'total': 0} cd = buildGAPIObject(API.DIRECTORY) @@ -68552,6 +68589,7 @@ def printShowMessagesThreads(users, entityType): dateHeaderFormat = '' dateHeaderConvertTimezone = False uploadAttachmentBody = {} + addCSVData = {} parentParms = initDriveFileAttributes() while Cmd.ArgumentsRemaining(): myarg = getArgument() @@ -68610,6 +68648,9 @@ def printShowMessagesThreads(users, entityType): dateHeaderConvertTimezone = getBoolean() if not dateHeaderFormat: dateHeaderFormat = RFC2822_TIME_FORMAT + elif myarg == 'addcsvdata': + k = getString(Cmd.OB_STRING) + addCSVData[k] = getString(Cmd.OB_STRING, minLen=0) else: unknownArgumentExit() labelMatchPattern = parameters['labelMatchPattern'] @@ -68634,11 +68675,15 @@ def printShowMessagesThreads(users, entityType): _callbacks = {'batch': _callbackCountLabels, 'process': _countMessages if entityType == Ent.MESSAGE else _countThreads} if show_size: sortTitles.append('size') + if addCSVData: + sortTitles.extend(sorted(addCSVData.keys())) csvPF.SetTitles(sortTitles) else: sortTitles = ['User', 'threadId', 'id'] csvPF.SetTitles(sortTitles) sortTitles.extend(defaultHeaders) + if addCSVData: + sortTitles.extend(sorted(addCSVData.keys())) _callbacks = {'batch': _callbackPrint, 'process': _printMessage if entityType == Ent.MESSAGE else _printThread} csvPF.SetSortTitles(sortTitles) else: @@ -68670,6 +68715,8 @@ def printShowMessagesThreads(users, entityType): if not senderMatchPattern: _initSenderLabelsMap(user) messageThreadCounts = {'User': user, parameters['listType']: 0, 'size': 0} + if addCSVData: + messageThreadCounts.update(addCSVData) senderCounts = {} if save_attachments: _, userName, _ = splitEmailAddressOrUID(user) @@ -68700,14 +68747,14 @@ def printShowMessagesThreads(users, entityType): if jcount == 0: setSysExitRC(NO_ENTITIES_FOUND_RC) if countsOnly and not show_labels and not senderMatchPattern and not show_size: - if not csvPF: - printEntityKVList([Ent.USER, user], [parameters['listType'], jcount], i, count) - else: - csvPF.WriteRow({'User': user, parameters['listType']: jcount}) - continue - if jcount == 0: - if not csvPF: - entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count) + if not positiveCountsOnly or jcount > 0: + if not csvPF: + printEntityKVList([Ent.USER, user], [parameters['listType'], jcount], i, count) + else: + row = {'User': user, parameters['listType']: jcount} + if addCSVData: + row.update(addCSVData) + csvPF.WriteRow(row) continue if not csvPF and not countsOnly: if (parameters['messageEntity'] is not None or @@ -68763,32 +68810,46 @@ def printShowMessagesThreads(users, entityType): if not show_size: for label in labelsMap.values(): label.pop('size', None) + if addCSVData: + row.update(addCSVData) csvPF.WriteRowTitles(flattenJSON({'Labels': sorted(iter(labelsMap.values()), key=lambda k: k['name'])}, flattened=row)) elif not senderMatchPattern: - if not csvPF: - if not show_size: - printEntityKVList([Ent.USER, user], [parameters['listType'], messageThreadCounts[parameters['listType']]], i, count) + v = messageThreadCounts[parameters['listType']] + if not positiveCountsOnly or v > 0: + if not csvPF: + if not show_size: + printEntityKVList([Ent.USER, user], [parameters['listType'], v], i, count) + else: + printEntityKVList([Ent.USER, user], [parameters['listType'], v, 'size', messageThreadCounts['size']], i, count) else: - printEntityKVList([Ent.USER, user], [parameters['listType'], messageThreadCounts[parameters['listType']], 'size', messageThreadCounts['size']], i, count) - else: - if not show_size: - messageThreadCounts.pop('size', None) - csvPF.WriteRow(messageThreadCounts) + if not show_size: + messageThreadCounts.pop('size', None) + csvPF.WriteRow(messageThreadCounts) else: if not show_size: if not csvPF: for k, v in sorted(iter(senderCounts.items())): - printEntityKVList([Ent.USER, user, Ent.SENDER, k], [parameters['listType'], v['count']], i, count) + if not positiveCountsOnly or v['count'] > 0: + printEntityKVList([Ent.USER, user, Ent.SENDER, k], [parameters['listType'], v['count']], i, count) else: for k, v in sorted(iter(senderCounts.items())): - csvPF.WriteRow({'User': user, 'Sender': k, parameters['listType']: v['count']}) + if not positiveCountsOnly or v['count'] > 0: + row = {'User': user, 'Sender': k, parameters['listType']: v['count']} + if addCSVData: + row.update(addCSVData) + csvPF.WriteRow(row) else: if not csvPF: for k, v in sorted(iter(senderCounts.items())): - printEntityKVList([Ent.USER, user, Ent.SENDER, k], [parameters['listType'], v['count'], 'size', v['size']], i, count) + if not positiveCountsOnly or v['count'] > 0: + printEntityKVList([Ent.USER, user, Ent.SENDER, k], [parameters['listType'], v['count'], 'size', v['size']], i, count) else: for k, v in sorted(iter(senderCounts.items())): - csvPF.WriteRow({'User': user, 'Sender': k, parameters['listType']: v['count'], 'size': v['size']}) + if not positiveCountsOnly or v['count'] > 0: + row = {'User': user, 'Sender': k, parameters['listType']: v['count'], 'size': v['size']} + if addCSVData: + row.update(addCSVData) + csvPF.WriteRow(row) if csvPF: if not countsOnly: csvPF.RemoveTitles(['SizeEstimate', 'LabelsCount', 'Labels', 'Snippet', 'Body']) @@ -68805,15 +68866,16 @@ def printShowMessagesThreads(users, entityType): else: csvPF.writeCSVfile('Message Counts' if not show_labels else 'Message Label Counts') -# gam print message|messages +# gam print message|messages [todrive *] # (((query [querytime ]*) (matchlabel ) [or|and])* [quick|notquick] [max_to_print ] [includespamtrash])|(ids ) # [labelmatchpattern ] [sendermatchpattern ] # [headers all|] [dateheaderformat iso|rfc2822|] [dateheaderconverttimezone []] # [showlabels] [showbody] [showdate] [showsize] [showsnippet] -# [convertcrnl] [delimiter ] [todrive *] +# [convertcrnl] [delimiter ] # [countsonly|positivecountsonly] [useronly] # [[attachmentnamepattern ] # [showattachments [noshowtextplain]]] +# (addcsvdata )* # gam show message|messages # (((query [querytime ]*) (matchlabel ) [or|and])* [quick|notquick] [max_to_show ] [includespamtrash])|(ids ) # [labelmatchpattern ] [sendermatchpattern ] @@ -68827,15 +68889,16 @@ def printShowMessagesThreads(users, entityType): def printShowMessages(users): printShowMessagesThreads(users, Ent.MESSAGE) -# gam print thread|threads +# gam print thread|threads [todrive *] # (((query [querytime ]*) (matchlabel ) [or|and])* [quick|notquick] [max_to_print ] [includespamtrash])|(ids ) # [labelmatchpattern ] # [headers all|] [dateheaderformat iso|rfc2822|] [dateheaderconverttimezone []] # [showlabels] [showbody] [showdate] [showsize] [showsnippet] -# [convertcrnl] [delimiter ] [todrive *] +# [convertcrnl] [delimiter ] # [countsonly|positivecountsonly] [useronly] # [[attachmentnamepattern ] # [showattachments [noshowtextplain]]] +# (addcsvdata )* # gam show thread|threads # (((query [querytime ]*) (matchlabel ) [or|and])* [quick|notquick] [max_to_show ] [includespamtrash])|(ids ) # [labelmatchpattern ] diff --git a/src/gam/gamlib/glapi.py b/src/gam/gamlib/glapi.py index 520fc31d..c90a8bcf 100644 --- a/src/gam/gamlib/glapi.py +++ b/src/gam/gamlib/glapi.py @@ -29,8 +29,10 @@ CBCM = 'cbcm' CHAT = 'chat' CHAT_EVENTS = 'chatevents' CHAT_MEMBERSHIPS = 'chatmemberships' +CHAT_MEMBERSHIPS_ADMIN = 'chatmembershipsadmin' CHAT_MESSAGES = 'chatmessages' CHAT_SPACES = 'chatspaces' +CHAT_SPACES_ADMIN = 'chatspacesadmin' CHAT_SPACES_DELETE = 'chatspacesdelete' CHROMEMANAGEMENT = 'chromemanagement' CHROMEMANAGEMENT_APPDETAILS = 'chromemanagementappdetails' @@ -197,8 +199,10 @@ _INFO = { CHAT: {'name': 'Chat API', 'version': 'v1', 'v2discovery': True}, CHAT_EVENTS: {'name': 'Chat API - Events', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, CHAT_MEMBERSHIPS: {'name': 'Chat API - Memberships', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, + CHAT_MEMBERSHIPS_ADMIN: {'name': 'Chat API - Admin Memberships', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, CHAT_MESSAGES: {'name': 'Chat API - Messages', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, CHAT_SPACES: {'name': 'Chat API - Spaces', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, + CHAT_SPACES_ADMIN: {'name': 'Chat API - Admin Spaces', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, CHAT_SPACES_DELETE: {'name': 'Chat API - Spaces Delete', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHAT}, CLASSROOM: {'name': 'Classroom API', 'version': 'v1', 'v2discovery': True}, CHROMEMANAGEMENT: {'name': 'Chrome Management API', 'version': 'v1', 'v2discovery': True}, @@ -514,6 +518,10 @@ _SVCACCT_SCOPES = [ 'api': CHAT_MEMBERSHIPS, 'subscopes': READONLY, 'scope': 'https://www.googleapis.com/auth/chat.memberships'}, + {'name': 'Chat API - Admin Memberships', + 'api': CHAT_MEMBERSHIPS_ADMIN, + 'subscopes': READONLY, + 'scope': 'https://www.googleapis.com/auth/chat.admin.memberships'}, {'name': 'Chat API - Messages', 'api': CHAT_MESSAGES, 'subscopes': READONLY, @@ -522,6 +530,10 @@ _SVCACCT_SCOPES = [ 'api': CHAT_SPACES, 'subscopes': READONLY, 'scope': 'https://www.googleapis.com/auth/chat.spaces'}, + {'name': 'Chat API - Admin Spaces', + 'api': CHAT_SPACES_ADMIN, + 'subscopes': READONLY, + 'scope': 'https://www.googleapis.com/auth/chat.admin.spaces'}, {'name': 'Chat API - Spaces Delete', 'api': CHAT_SPACES_DELETE, 'subscopes': [], diff --git a/src/gam/gamlib/glglobals.py b/src/gam/gamlib/glglobals.py index 4e42a37b..6be29ce4 100644 --- a/src/gam/gamlib/glglobals.py +++ b/src/gam/gamlib/glglobals.py @@ -171,6 +171,10 @@ RATE_CHECK_COUNT = 'rccn' RATE_CHECK_START = 'rcst' # Section name from outer gam, passed to inner gams SECTION = 'sect' +# Enable/disable "Getting ... " messages +SHOW_GETTINGS = 'shog' +# Enable/disable NL at end of "Got ..." messages +SHOW_GETTINGS_GOT_NL = 'shgn' # redirected files SAVED_STDOUT = 'svso' STDERR = 'stde' @@ -287,6 +291,8 @@ Globals = { RATE_CHECK_COUNT: 0, RATE_CHECK_START: 0, SECTION: None, + SHOW_GETTINGS: True, + SHOW_GETTINGS_GOT_NL: False, SAVED_STDOUT: None, STDERR: {}, STDOUT: {},