Multiple updates

This commit is contained in:
Ross Scroggs
2023-11-03 08:52:05 -07:00
parent 3facd05a94
commit 4a199c7b6f
12 changed files with 341 additions and 250 deletions

View File

@ -137,7 +137,7 @@ gam csv UpdateBrowsers.csv gam update browser ~deviceId updatenotes "~~notes~~\n
``` ```
gam move browsers ou|org|orgunit <OrgUnitPath> gam move browsers ou|org|orgunit <OrgUnitPath>
((ids <DeviceIDList>) | ((ids <DeviceIDList>) |
(queries <QueryBrowserList> [querytime.* <Time>]) | (queries <QueryBrowserList> [querytime<String> <Time>]) |
(browserou <OrgUnitItem>) | (browserous <OrgUnitList>) | (browserou <OrgUnitItem>) | (browserous <OrgUnitList>) |
<FileSelector> | <CSVFileSelector>) <FileSelector> | <CSVFileSelector>)
[batchsize <Integer>] [batchsize <Integer>]
@ -178,7 +178,7 @@ By default, Gam displays the information as an indented list of keys and values:
``` ```
gam show browsers gam show browsers
([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser>)|(queries <QueryBrowserList>))|(select <BrowserEntity>)) ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser>)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
[querytime.* <Time>] [querytime<String> <Time>]
[orderby <BrowserOrderByFieldName> [ascending|descending]] [orderby <BrowserOrderByFieldName> [ascending|descending]]
[basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>] [basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>]
[formatjson] [formatjson]
@ -205,7 +205,7 @@ The characters following `querytime` can be any combination of lowercase letters
``` ```
gam print browsers [todrive <ToDriveAttribute>*] gam print browsers [todrive <ToDriveAttribute>*]
([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser>)|(queries <QueryBrowserList>))|(select <BrowserEntity>)) ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser>)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
[querytime.* <Time>] [querytime<String> <Time>]
[orderby <BrowserOrderByFieldName> [ascending|descending]] [orderby <BrowserOrderByFieldName> [ascending|descending]]
[basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>] [basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>]
[sortheaders] [formatjson [quotechar <Character>]] [sortheaders] [formatjson [quotechar <Character>]]
@ -372,7 +372,7 @@ gam revoke browsertoken <BrowserTokenPermanentID>
``` ```
gam show browsertokens gam show browsertokens
([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>))) ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>)))
[querytime.* <Time>] [querytime<String> <Time>]
[orderby <BrowserTokenFieldName> [ascending|descending]] [orderby <BrowserTokenFieldName> [ascending|descending]]
[allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>] [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
[formatjson] [formatjson]
@ -395,7 +395,7 @@ By default, Gam displays the information as an indented list of keys and values:
``` ```
gam print browsertokens [todrive <ToDriveAttribute>*] gam print browsertokens [todrive <ToDriveAttribute>*]
([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>))) ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>)))
[querytime.* <Time>] [querytime<String> <Time>]
[orderby <BrowserTokenFieldName> [ascending|descending]] [orderby <BrowserTokenFieldName> [ascending|descending]]
[allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>] [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
[sortheaders] [formatjson [quotechar <Character>]] [sortheaders] [formatjson [quotechar <Character>]]

View File

@ -538,7 +538,7 @@ gam <CrOSTypeEntity> print cros
``` ```
gam print cros [todrive <ToDriveAttribute>*] gam print cros [todrive <ToDriveAttribute>*]
[(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime.* <Time>] [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
[(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)| [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
(cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]] (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
[orderby <CrOSOrderByFieldName> [ascending|descending]] [orderby <CrOSOrderByFieldName> [ascending|descending]]
@ -691,7 +691,7 @@ gam <CrOSTypeEntity> show count
``` ```
gam print crosactivity [todrive <ToDriveAttribute>*] gam print crosactivity [todrive <ToDriveAttribute>*]
[(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime.* <Time>] [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
[(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)| [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
(cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]] (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
[orderby <CrOSOrderByFieldName> [ascending|descending]] [orderby <CrOSOrderByFieldName> [ascending|descending]]

View File

@ -167,7 +167,7 @@ These two/three columns are used to match current company devices against the CS
If `preview` is specified, the operations that would be performed are previewed but are not performed; use this to test. If `preview` is specified, the operations that would be performed are previewed but are not performed; use this to test.
``` ```
gam sync devices gam sync devices
[(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime.* <Time>)*] [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
csvfile <FileName> csvfile <FileName>
(devicetype_column <String>)|(static_devicetype <DeviceType>) (devicetype_column <String>)|(static_devicetype <DeviceType>)
(serialnumber_column <String>) (serialnumber_column <String>)
@ -190,7 +190,7 @@ By default, Gam displays the information as an indented list of keys and values.
## Print devices ## Print devices
``` ```
gam print devices [todrive <ToDriveAttribute>*] gam print devices [todrive <ToDriveAttribute>*]
[(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime.* <Time>)*] [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
<DeviceFieldName>* [fields <DeviceFieldNameList>] [userfields <DeviceUserFieldNameList>] <DeviceFieldName>* [fields <DeviceFieldNameList>] [userfields <DeviceUserFieldNameList>]
[orderby <DeviceOrderByFieldName> [ascending|descending]] [orderby <DeviceOrderByFieldName> [ascending|descending]]
[all|company|personal|nocompanydevices|nopersonaldevices] [all|company|personal|nocompanydevices|nopersonaldevices]
@ -266,7 +266,7 @@ gam info deviceuser <DeviceUserEntity>
``` ```
gam print deviceusers [todrive <ToDriveAttribute>*] gam print deviceusers [todrive <ToDriveAttribute>*]
[select <DeviceID>] [select <DeviceID>]
[(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime.* <Time>)*] [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
<DeviceUserFieldName>* [fields <DeviceUserFieldNameList>] <DeviceUserFieldName>* [fields <DeviceUserFieldNameList>]
[orderby <DeviceOrderByFieldName> [ascending|descending]] [orderby <DeviceOrderByFieldName> [ascending|descending]]
[formatjson [quotechar <Character>]] [formatjson [quotechar <Character>]]

View File

@ -10,6 +10,27 @@ Add the `-s` option to the end of the above commands to suppress creating the `g
See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Windows or other options, including manual installation See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Windows or other options, including manual installation
Updated `gam <UserTypeEntity> import|insert message` to allow `replace <Tag> <UserReplacement>` as documented.
### 6.65.05
Updated `gam info users <UserTypeEntity>` to make option `grouptree` effective when used
with option `formatjson`.
Added option `[formatjson [quotechar <Character>]]]`
to these commands so that event details are displayed in CSV format.
```
gam print|show grouptree <GroupEntity>
gam <UserTypeEntity> print|show grouptree
```
Added option `querytime<String> <Date>` to all commands that process messages.
For example, you can identify all messages within a particular time period, in this case, all messages unread
in the last 30 days.
```
gam user user@domain.com print messages querytime30d -30d query "after:#querytime30d# is:unread"
```
### 6.65.04 ### 6.65.04
Fixed bug where license SKU `1010020031` (Google Workspace Frontline Standard) was improperly entered making it unusable; Fixed bug where license SKU `1010020031` (Google Workspace Frontline Standard) was improperly entered making it unusable;

View File

@ -334,7 +334,7 @@ writes the credentials into the file oauth2.txt.
admin@server:/Users/admin/bin/gamadv-xtd3$ rm -f /Users/admin/GAMConfig/oauth2.txt admin@server:/Users/admin/bin/gamadv-xtd3$ rm -f /Users/admin/GAMConfig/oauth2.txt
admin@server:/Users/admin/bin/gamadv-xtd3$ ./gam version admin@server:/Users/admin/bin/gamadv-xtd3$ ./gam version
WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found
GAMADV-XTD3 6.65.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.65.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.10.8 64-bit final Python 3.10.8 64-bit final
MacOS High Sierra 10.13.6 x86_64 MacOS High Sierra 10.13.6 x86_64
@ -1002,7 +1002,7 @@ writes the credentials into the file oauth2.txt.
C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt
C:\GAMADV-XTD3>gam version C:\GAMADV-XTD3>gam version
WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found
GAMADV-XTD3 6.65.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.65.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.0 64-bit final Python 3.12.0 64-bit final
Windows-10-10.0.17134 AMD64 Windows-10-10.0.17134 AMD64

View File

@ -100,7 +100,7 @@ By default, Gam displays the information as an indented list of keys and values.
## Print mobile devices ## Print mobile devices
``` ```
gam print mobile [todrive <ToDriveAttribute>*] gam print mobile [todrive <ToDriveAttribute>*]
[(query <QueryMobile>)|(queries <QueryMobileList>) (querytime.* <Time>)*] [(query <QueryMobile>)|(queries <QueryMobileList>) (querytime<String> <Time>)*]
[orderby <MobileOrderByFieldName> [ascending|descending]] [orderby <MobileOrderByFieldName> [ascending|descending]]
[basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>] [basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>]
[delimiter <Character>] [appslimit <Number>] [oneappperrow] [listlimit <Number>] [delimiter <Character>] [appslimit <Number>] [oneappperrow] [listlimit <Number>]

View File

@ -173,6 +173,22 @@
(gcsdoc|gcshtml <StorageBucketObjectName>)| (gcsdoc|gcshtml <StorageBucketObjectName>)|
(emlfile <FileName>) (emlfile <FileName>)
``` ```
## Message queries with dates
```
query <QueryGmail> [querytime<String> <Date>]*
```
* `query "xxx"` - ` xxx` is appended to the current query; you can repeat the query argument to build up a longer query.
Use the `querytime<String> <Date>` option to allow dates, usually relative, to be substituted into the `query <QueryGmail>` option.
The `querytime<String> <Date>` value replaces the string `#querytime<String>#` in any queries.
The characters following `querytime` can be any combination of lowercase letters and numbers. This is most useful in scripts
where you can specify a relative date without having to change the script.
For example, query for messages from moree than 5 years ago:
```
querytime5years -5y query "before:#querytime5years#"
```
## Subject and label queries ## Subject and label queries
Using a query to select messages by subject or label requires some attention in order to achieve the desired effect. Using a query to select messages by subject or label requires some attention in order to achieve the desired effect.
* https://support.google.com/mail/answer/7190 * https://support.google.com/mail/answer/7190
@ -316,7 +332,7 @@ Your command line will have: `embedimage file1.jpg image1` embedimage file2.jpg
## Archive messages ## Archive messages
``` ```
gam <UserTypeEntity> archive messages <GroupItem> gam <UserTypeEntity> archive messages <GroupItem>
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_archive <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_archive <Number>])|(ids <MessageIDEntity>)
``` ```
@ -328,10 +344,10 @@ See below for message selection.
Export messages in EML format. Export messages in EML format.
``` ```
gam <UserTypeEntity> export message|messages gam <UserTypeEntity> export message|messages
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <MessageIDEntity>) (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <MessageIDEntity>)
[targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]] [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
gam <UserTypeEntity> export thread|threads gam <UserTypeEntity> export thread|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <ThreadIDEntity>) (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <ThreadIDEntity>)
[targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]] [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
``` ```
@ -354,11 +370,11 @@ See below for message selection.
## Forward messages/threads ## Forward messages/threads
``` ```
gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity> gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity>
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>)
[subject <String>] [subject <String>]
gam <UserTypeEntity> forward thread|threads recipient|to <RecipientEntity> gam <UserTypeEntity> forward thread|threads recipient|to <RecipientEntity>
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>) [quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>)
[subject <String>] [subject <String>]
``` ```
@ -372,27 +388,27 @@ See below for message selection.
## Manage messages/threads ## Manage messages/threads
``` ```
gam <UserTypeEntity> delete messages|threads gam <UserTypeEntity> delete messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_delete <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_delete <Number>])|(ids <MessageIDEntity>)
gam <UserTypeEntity> modify messages|threads gam <UserTypeEntity> modify messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_modify <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_modify <Number>])|(ids <MessageIDEntity>)
(addlabel <LabelName>)* (removelabel <LabelName>)* (addlabel <LabelName>)* (removelabel <LabelName>)*
gam <UserTypeEntity> spam messages|threads gam <UserTypeEntity> spam messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_spam <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_spam <Number>])|(ids <MessageIDEntity>)
gam <UserTypeEntity> trash messages|threads gam <UserTypeEntity> trash messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>)
gam <UserTypeEntity> untrash messages|threads gam <UserTypeEntity> untrash messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_untrash <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <MessageIDEntity>)
``` ```
### Manage a specific set of messages ### Manage a specific set of messages
* `ids <MessageIDEntity>` - A list of message ids * `ids <MessageIDEntity>` - A list of message ids
### Manage a selected set of messages ### Manage a selected set of messages
* `((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+` - Criteria to select messages * `((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+` - Criteria to select messages
* `max_to_xxx` - Limit the number of messages that will be processed; use a value of 0 for no limit * `max_to_xxx` - Limit the number of messages that will be processed; use a value of 0 for no limit
* `doit` - No messages are processed unless you specify `doit`. By not specifying `doit`, you can preview the messages selected to verify that the results match your expectations. * `doit` - No messages are processed unless you specify `doit`. By not specifying `doit`, you can preview the messages selected to verify that the results match your expectations.
@ -439,7 +455,7 @@ gam config auto_batch_min 1 groups_inde EastOffice delete message query "rfc822m
## Display messages/threads ## Display messages/threads
``` ```
gam <UserTypeEntity> show messages|threads gam <UserTypeEntity> show messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])* (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])*
[quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>) [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>)
[labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>] [labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>]
[countsonly|positivecountsonly] [useronly] [countsonly|positivecountsonly] [useronly]
@ -449,7 +465,7 @@ gam <UserTypeEntity> show messages|threads
[saveattachments [attachmentnamepattern <RegularExpression>]] [saveattachments [attachmentnamepattern <RegularExpression>]]
[targetfolder <FilePath>] [overwrite [<Boolean>]] [targetfolder <FilePath>] [overwrite [<Boolean>]]
gam <UserTypeEntity> print messages|threads [todrive <ToDriveAttribute>*] gam <UserTypeEntity> print messages|threads [todrive <ToDriveAttribute>*]
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])* (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])*
[quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>) [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>)
[labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>] [labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>]
[countsonly|positivecountsonly] [useronly] [countsonly|positivecountsonly] [useronly]
@ -467,7 +483,7 @@ By default, Gam displays all messages.
* `ids <MessageIDEntity>` - A list of message ids * `ids <MessageIDEntity>` - A list of message ids
## Display a selected set of messages ## Display a selected set of messages
* `((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+` - Criteria to select messages * `((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+` - Criteria to select messages
* `max_to_xxx` - Limit the number of messages that will be displayed * `max_to_xxx` - Limit the number of messages that will be displayed
* `includespamtrash` - Include messages in the Spam and Trash folders * `includespamtrash` - Include messages in the Spam and Trash folders
* `labelmatchpattern <RegularExpression>` - Only display messages with some label that matches `<RegularExpression>` * `labelmatchpattern <RegularExpression>` - Only display messages with some label that matches `<RegularExpression>`

View File

@ -4,7 +4,7 @@
Print the current version of Gam with details Print the current version of Gam with details
``` ```
gam version gam version
GAMADV-XTD3 6.65.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.65.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.0 64-bit final Python 3.12.0 64-bit final
MacOS Monterey 12.7 x86_64 MacOS Monterey 12.7 x86_64
@ -16,7 +16,7 @@ Time: 2023-06-02T21:10:00-07:00
Print the current version of Gam with details and time offset information Print the current version of Gam with details and time offset information
``` ```
gam version timeoffset gam version timeoffset
GAMADV-XTD3 6.65.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.65.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.0 64-bit final Python 3.12.0 64-bit final
MacOS Monterey 12.7 x86_64 MacOS Monterey 12.7 x86_64
@ -28,7 +28,7 @@ Your system time differs from www.googleapis.com by less than 1 second
Print the current version of Gam with extended details and SSL information Print the current version of Gam with extended details and SSL information
``` ```
gam version extended gam version extended
GAMADV-XTD3 6.65.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource GAMADV-XTD3 6.65.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.0 64-bit final Python 3.12.0 64-bit final
MacOS Monterey 12.7 x86_64 MacOS Monterey 12.7 x86_64
@ -65,7 +65,7 @@ MacOS High Sierra 10.13.6 x86_64
Path: /Users/Admin/bin/gamadv-xtd3 Path: /Users/Admin/bin/gamadv-xtd3
Version Check: Version Check:
Current: 5.35.08 Current: 5.35.08
Latest: 6.65.04 Latest: 6.65.05
echo $? echo $?
1 1
``` ```
@ -73,7 +73,7 @@ echo $?
Print the current version number without details Print the current version number without details
``` ```
gam version simple gam version simple
6.65.04 6.65.05
``` ```
In Linux/MacOS you can do: In Linux/MacOS you can do:
``` ```
@ -83,7 +83,7 @@ echo $VER
Print the current version of Gam and address of this Wiki Print the current version of Gam and address of this Wiki
``` ```
gam help gam help
GAM 6.65.04 - https://github.com/taers232c/GAMADV-XTD3 GAM 6.65.05 - https://github.com/taers232c/GAMADV-XTD3
Ross Scroggs <ross.scroggs@gmail.com> Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.0 64-bit final Python 3.12.0 64-bit final
MacOS Monterey 12.7 x86_64 MacOS Monterey 12.7 x86_64

View File

@ -1906,7 +1906,7 @@ gam delete browser <DeviceID>
gam update browser <BrowserEntity> <BrowserAttibute>+ gam update browser <BrowserEntity> <BrowserAttibute>+
gam move browsers ou|org|orgunit <OrgUnitPath> gam move browsers ou|org|orgunit <OrgUnitPath>
((ids <DeviceIDList>) | ((ids <DeviceIDList>) |
(queries <QueryBrowserList> [querytime.* <Time>]) | (queries <QueryBrowserList> [querytime<String> <Time>]) |
(browserou <OrgUnitItem>) | (browserous <OrgUnitList>) | (browserou <OrgUnitItem>) | (browserous <OrgUnitList>) |
<FileSelector> | <CSVFileSelector>) <FileSelector> | <CSVFileSelector>)
[batchsize <Integer>] [batchsize <Integer>]
@ -1916,13 +1916,13 @@ gam info browser <DeviceID>
[formatjson] [formatjson]
gam show browsers gam show browsers
([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser>)|(queries <QueryBrowserList>))|(select <BrowserEntity>)) ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser>)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
[querytime.* <Time>] [querytime<String> <Time>]
[orderby <BrowserOrderByFieldName> [ascending|descending]] [orderby <BrowserOrderByFieldName> [ascending|descending]]
[basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>] [basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>]
[formatjson] [formatjson]
gam print browsers [todrive <ToDriveAttribute>*] gam print browsers [todrive <ToDriveAttribute>*]
([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser>)|(queries <QueryBrowserList>))|(select <BrowserEntity>)) ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser>)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
[querytime.* <Time>] [querytime<String> <Time>]
[orderby <BrowserOrderByFieldName> [ascending|descending]] [orderby <BrowserOrderByFieldName> [ascending|descending]]
[basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>] [basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>]
[sortheaders] [sortheaders]
@ -1950,13 +1950,13 @@ gam revoke browsertoken <BrowserTokenPermanentID>
gam show browsertokens gam show browsertokens
([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken>)|(queries <QueryBrowserTokenList>))) ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken>)|(queries <QueryBrowserTokenList>)))
[querytime.* <Time>] [querytime<String> <Time>]
[orderby <BrowserTokenFieldName> [ascending|descending]] [orderby <BrowserTokenFieldName> [ascending|descending]]
[allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>] [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
[formatjson] [formatjson]
gam print browsertokens [todrive <ToDriveAttribute>*] gam print browsertokens [todrive <ToDriveAttribute>*]
([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken>)|(queries <QueryBrowserTokenList>))) ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken>)|(queries <QueryBrowserTokenList>)))
[querytime.* <Time>] [querytime<String> <Time>]
[orderby <BrowserTokenFieldName> [ascending|descending]] [orderby <BrowserTokenFieldName> [ascending|descending]]
[allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>] [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
[sortheaders] [sortheaders]
@ -2253,13 +2253,13 @@ gam <CrOSTypeEntity> info
[formatjson] [formatjson]
Print fields for selected CrOS devices; use these options to select CrOS devices: Print fields for selected CrOS devices; use these options to select CrOS devices:
[(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime.* <Time>] [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
[(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)| [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
(cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]] (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
If none of these options are chosen, all CrOS devices are selected. If none of these options are chosen, all CrOS devices are selected.
gam print cros [todrive <ToDriveAttribute>*] gam print cros [todrive <ToDriveAttribute>*]
[(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime.* <Time>] [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
[(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)| [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
(cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]] (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
[orderby <CrOSOrderByFieldName> [ascending|descending]] [orderby <CrOSOrderByFieldName> [ascending|descending]]
@ -2315,13 +2315,13 @@ Show count of CrOS devices
gam <CrOSTypeEntity> show count gam <CrOSTypeEntity> show count
Print activity for selected CrOS devices; use these options to select CrOS devices: Print activity for selected CrOS devices; use these options to select CrOS devices:
[(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime.* <Time>] [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
[(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)| [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
(cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]] (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
If none of these options are chosen, all CrOS devices are selected. If none of these options are chosen, all CrOS devices are selected.
gam print crosactivity [todrive <ToDriveAttribute>*] gam print crosactivity [todrive <ToDriveAttribute>*]
[(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime.* <Time>] [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
[(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)| [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
(cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]] (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
[orderby <CrOSOrderByFieldName> [ascending|descending]] [orderby <CrOSOrderByFieldName> [ascending|descending]]
@ -3593,7 +3593,9 @@ gam print groups [todrive <ToDriveAttribute>*]
gam print grouptree <GroupEntity> [todrive <ToDriveAttribute>*] gam print grouptree <GroupEntity> [todrive <ToDriveAttribute>*]
[showparentsaslist [<Boolean>]] [delimiter <Character>] [showparentsaslist [<Boolean>]] [delimiter <Character>]
[formatjson [quotechar <Character>]]
gam show grouptree <GroupEntity> gam show grouptree <GroupEntity>
[formatjson]
<MembersFieldName> ::= <MembersFieldName> ::=
delivery|deliverysettings| delivery|deliverysettings|
@ -3807,7 +3809,7 @@ gam wipe device <DeviceEntity> [removeresetlock] [doit]
gam sync devices gam sync devices
<CSVFileSelector> <CSVFileSelector>
[(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime.* <Time>)*] [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
(devicetype_column <String>)|(static_devicetype <DeviceType>) (devicetype_column <String>)|(static_devicetype <DeviceType>)
(serialnumber_column <String>) (serialnumber_column <String>)
[assettag_column <String>] [assettag_column <String>]
@ -3820,7 +3822,7 @@ gam info device <DeviceEntity>
[nodeviceusers] [nodeviceusers]
[formatjson] [formatjson]
gam print devices [todrive <ToDriveAttribute>*] gam print devices [todrive <ToDriveAttribute>*]
[(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime.* <Time>)*] [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
<DeviceFieldName>* [fields <DeviceFieldNameList>] [userfields <DeviceUserFieldNameList>] <DeviceFieldName>* [fields <DeviceFieldNameList>] [userfields <DeviceUserFieldNameList>]
[orderby <DeviceOrderByFieldName> [ascending|descending]] [orderby <DeviceOrderByFieldName> [ascending|descending]]
[all|company|personal|nocompanydevices|nopersonaldevices] [all|company|personal|nocompanydevices|nopersonaldevices]
@ -3845,7 +3847,7 @@ gam info deviceuser <DeviceUserEntity>
[formatjson] [formatjson]
gam print deviceusers [todrive <ToDriveAttribute>*] gam print deviceusers [todrive <ToDriveAttribute>*]
[select <DeviceID>] [select <DeviceID>]
[(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime.* <Time>)*] [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
<DeviceUserFieldName>* [fields <DeviceUserFieldNameList>] <DeviceUserFieldName>* [fields <DeviceUserFieldNameList>]
[orderby <DeviceOrderByFieldName> [ascending|descending]] [orderby <DeviceOrderByFieldName> [ascending|descending]]
[formatjson [quotechar <Character>]] [formatjson [quotechar <Character>]]
@ -3986,7 +3988,7 @@ gam info mobile <MobileEntity>
[basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>] [basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>]
[formatjson] [formatjson]
gam print mobile [todrive <ToDriveAttribute>*] gam print mobile [todrive <ToDriveAttribute>*]
[(query <QueryMobile>)|(queries <QueryMobileList>) (querytime.* <Time>)*] [(query <QueryMobile>)|(queries <QueryMobileList>) (querytime<String> <Time>)*]
[orderby <MobileOrderByFieldName> [ascending|descending]] [orderby <MobileOrderByFieldName> [ascending|descending]]
[basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>] [basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>]
[delimiter <Character>] [appslimit <Number>] [oneappperrow] [listlimit <Number>] [delimiter <Character>] [appslimit <Number>] [oneappperrow] [listlimit <Number>]
@ -6865,43 +6867,43 @@ gam <UserTypeEntity> insert message
[deleted [<Boolean>]] [deleted [<Boolean>]]
gam <UserTypeEntity> archive messages <GroupItem> gam <UserTypeEntity> archive messages <GroupItem>
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_archive <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_archive <Number>])|(ids <MessageIDEntity>)
gam <UserTypeEntity> delete messages|threads gam <UserTypeEntity> delete messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_delete <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_delete <Number>])|(ids <MessageIDEntity>)
gam <UserTypeEntity> modify messages|threads gam <UserTypeEntity> modify messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_modify <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_modify <Number>])|(ids <MessageIDEntity>)
(addlabel <LabelName>)* (removelabel <LabelName>)* (addlabel <LabelName>)* (removelabel <LabelName>)*
gam <UserTypeEntity> spam messages|threads gam <UserTypeEntity> spam messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_spam <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_spam <Number>])|(ids <MessageIDEntity>)
gam <UserTypeEntity> trash messages|threads gam <UserTypeEntity> trash messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>)
gam <UserTypeEntity> untrash messages|threads gam <UserTypeEntity> untrash messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_untrash <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <MessageIDEntity>)
gam <UserTypeEntity> export message|messages gam <UserTypeEntity> export message|messages
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <MessageIDEntity>) (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <MessageIDEntity>)
[targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]] [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
gam <UserTypeEntity> export thread|threads gam <UserTypeEntity> export thread|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <ThreadIDEntity>) (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <ThreadIDEntity>)
[targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]] [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity> gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity>
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
[quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>) [quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>)
[subject <String>] [altcharset <String>] [subject <String>] [altcharset <String>]
gam <UserTypeEntity> forward thread|thtreads recipient|to <RecipientEntity> gam <UserTypeEntity> forward thread|thtreads recipient|to <RecipientEntity>
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+
quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>) quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>)
[subject <String>] [altcharset <String>] [subject <String>] [altcharset <String>]
gam <UserTypeEntity> show messages|threads gam <UserTypeEntity> show messages|threads
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])* (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])*
[quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>) [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>)
[labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>] [labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>]
[countsonly|positivecountsonly] [useronly] [countsonly|positivecountsonly] [useronly]
@ -6911,7 +6913,7 @@ gam <UserTypeEntity> show messages|threads
[saveattachments [attachmentnamepattern <RegularExpression>]] [saveattachments [attachmentnamepattern <RegularExpression>]]
[targetfolder <FilePath>] [overwrite [<Boolean>]] [targetfolder <FilePath>] [overwrite [<Boolean>]]
gam <UserTypeEntity> print messages|threads [todrive <ToDriveAttribute>*] gam <UserTypeEntity> print messages|threads [todrive <ToDriveAttribute>*]
(((query <QueryGmail>) (matchlabel <LabelName>) [or|and])* (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])*
[quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>) [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>)
[labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>] [labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>]
[countsonly|positivecountsonly] [useronly] [countsonly|positivecountsonly] [useronly]
@ -7014,9 +7016,11 @@ gam <UserTypeEntity> print grouptree [todrive <ToDriveAttribute>*]
[(domain <DomainName>)|(customerid <CustomerID>)] [(domain <DomainName>)|(customerid <CustomerID>)]
[roles <GroupRoleList>] [roles <GroupRoleList>]
[showparentsaslist [<Boolean>]] [delimiter <Character>] [showparentsaslist [<Boolean>]] [delimiter <Character>]
[formatjson [quotechar <Character>]]
gam <UserTypeEntity> show grouptree gam <UserTypeEntity> show grouptree
[(domain <DomainName>)|(customerid <CustomerID>)] [(domain <DomainName>)|(customerid <CustomerID>)]
[roles <GroupRoleList>] [roles <GroupRoleList>]
[formatjson]
gam <UserTypeEntity> print groupslist [todrive <ToDriveAttribute>*] gam <UserTypeEntity> print groupslist [todrive <ToDriveAttribute>*]
[(domain <DomainName>)|(customerid <CustomerID>)] [(domain <DomainName>)|(customerid <CustomerID>)]
[delimiter <Character>] [quotechar <Character>] [delimiter <Character>] [quotechar <Character>]

View File

@ -1,3 +1,30 @@
7.00.00
Merged GAM-Team version
6.65.05
Updated `gam info users <UserTypeEntity>` to make option `grouptree` effective when used
with option `formatjson`.
Added option `[formatjson [quotechar <Character>]]]`
to these commands so that event details are displayed in CSV format.
```
gam print|show grouptree <GroupEntity>
gam <UserTypeEntity> print|show grouptree
```
Added option `querytime<String> <Date>` to all commands that process messages.
For example, you can identify all messages within a particular time period, in this case, all messages unread
in the last 30 days.
```
gam user user@domain.com print messages querytime30d -30d query "after:#querytime30d# is:unread"
```
Updated `gam <UserTypeEntity> import|insert message` to allow `replace <Tag> <UserReplacement>` as documented.
Updated non-owner permission handling in `gam <UserTypeEntity> copy|move drivefile`.
6.65.04 6.65.04
Fixed bug where license SKU `1010020031` (Google Workspace Frontline Standard) was improperly entered making it unusable; Fixed bug where license SKU `1010020031` (Google Workspace Frontline Standard) was improperly entered making it unusable;
@ -7,10 +34,6 @@ Added support for Google Workspace Additional Storage.
* ProductID - 101043 * ProductID - 101043
* SKUID - 1010430001 | gwas | plusstorage * SKUID - 1010430001 | gwas | plusstorage
7.00.00
Merged GAM-Team version
6.65.03 6.65.03
Fixed bug in commands that display calendar events where event start and end times were not properly displayed Fixed bug in commands that display calendar events where event start and end times were not properly displayed

View File

@ -2191,6 +2191,7 @@ def getJSON(deleteFields):
if not Cmd.ArgumentsRemaining(): if not Cmd.ArgumentsRemaining():
missingArgumentExit(Cmd.OB_JSON_DATA) missingArgumentExit(Cmd.OB_JSON_DATA)
argstr = Cmd.Current() argstr = Cmd.Current()
# argstr = Cmd.Current().replace(r'\\"', r'\"')
Cmd.Advance() Cmd.Advance()
try: try:
if encoding == UTF8: if encoding == UTF8:
@ -4509,17 +4510,26 @@ def runSqliteQuery(db_file, query):
return curr.fetchone()[0] return curr.fetchone()[0]
def refreshCredentialsWithReauth(credentials): def refreshCredentialsWithReauth(credentials):
def gcloudError():
writeStderr(f'Failed to run gcloud as {admin_email}. Please make sure it\'s setup')
e = Msg.REAUTHENTICATION_IS_NEEDED
handleOAuthTokenError(e, False)
writeStderr(Msg.CALLING_GCLOUD_FOR_REAUTH) writeStderr(Msg.CALLING_GCLOUD_FOR_REAUTH)
if 'termios' in sys.modules: if 'termios' in sys.modules:
old_settings = termios.tcgetattr(sys.stdin) old_settings = termios.tcgetattr(sys.stdin)
admin_email = _getAdminEmail()
# First makes sure gcloud has a valid access token and thus # First makes sure gcloud has a valid access token and thus
# should also have a valid RAPT token # should also have a valid RAPT token
try: try:
devnull = open(os.devnull, 'w', encoding=UTF8)
subprocess.run(['gcloud', subprocess.run(['gcloud',
'auth', 'auth',
'print-identity-token', 'print-identity-token',
'--no-user-output-enabled'], '--no-user-output-enabled'],
stderr=devnull,
check=False) check=False)
devnull.close()
# now determine gcloud's config path and token file # now determine gcloud's config path and token file
gcloud_path_result = subprocess.run(['gcloud', gcloud_path_result = subprocess.run(['gcloud',
'info', 'info',
@ -4532,14 +4542,14 @@ def refreshCredentialsWithReauth(credentials):
printBlankLine() printBlankLine()
raise KeyboardInterrupt from e raise KeyboardInterrupt from e
token_path = gcloud_path_result.stdout.decode().strip() token_path = gcloud_path_result.stdout.decode().strip()
if not token_path:
gcloudError()
token_file = f'{token_path}/access_tokens.db' token_file = f'{token_path}/access_tokens.db'
admin_email = _getAdminEmail()
try: try:
credentials._rapt_token = runSqliteQuery(token_file, credentials._rapt_token = runSqliteQuery(token_file,
f'SELECT rapt_token FROM access_tokens WHERE account_id = "{admin_email}"') f'SELECT rapt_token FROM access_tokens WHERE account_id = "{admin_email}"')
except TypeError: except TypeError:
systemErrorExit(SYSTEM_ERROR_RC, gcloudError()
f'Failed to run gcloud as {admin_email}. Please make sure it\'s setup')
if not credentials._rapt_token: if not credentials._rapt_token:
systemErrorExit(SYSTEM_ERROR_RC, systemErrorExit(SYSTEM_ERROR_RC,
'Failed to retrieve reauth token from gcloud. You may need to wait until gcloud is also prompted for reauth.') 'Failed to retrieve reauth token from gcloud. You may need to wait until gcloud is also prompted for reauth.')
@ -4792,7 +4802,7 @@ def checkGDataError(e, service):
reason = error[0].get('reason', '') reason = error[0].get('reason', '')
body = error[0].get('body', '').decode(UTF8) body = error[0].get('body', '').decode(UTF8)
# First check for errors that need special handling # First check for errors that need special handling
if reason in ['Token invalid - Invalid token: Stateless token expired', 'Token invalid - Invalid token: Token not found']: if reason in ['Token invalid - Invalid token: Stateless token expired', 'Token invalid - Invalid token: Token not found', 'gone']:
keep_domain = service.domain keep_domain = service.domain
getGDataOAuthToken(service) getGDataOAuthToken(service)
service.domain = keep_domain service.domain = keep_domain
@ -23302,7 +23312,7 @@ CROS_INDEXED_TITLES = ['activeTimeRanges', 'recentUsers', 'deviceFiles',
'cpuStatusReports', 'diskVolumeReports', 'lastKnownNetwork', 'screenshotFiles', 'systemRamFreeReports'] 'cpuStatusReports', 'diskVolumeReports', 'lastKnownNetwork', 'screenshotFiles', 'systemRamFreeReports']
# gam print cros [todrive <ToDriveAttribute>*] # gam print cros [todrive <ToDriveAttribute>*]
# [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime.* <Time>] # [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
# [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)| # [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
# (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]] # (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
# gam print cros [todrive <ToDriveAttribute>*] select <CrOSTypeEntity> # gam print cros [todrive <ToDriveAttribute>*] select <CrOSTypeEntity>
@ -23628,7 +23638,7 @@ CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP = {
CROS_ACTIVITY_TIME_OBJECTS = {'createTime'} CROS_ACTIVITY_TIME_OBJECTS = {'createTime'}
# gam print crosactivity [todrive <ToDriveAttribute>*] # gam print crosactivity [todrive <ToDriveAttribute>*]
# [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime.* <Time>] # [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
# [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)| # [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
# (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]] # (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
# gam print crosactivity [todrive <ToDriveAttribute>*] select <CrOSTypeEntity> # gam print crosactivity [todrive <ToDriveAttribute>*] select <CrOSTypeEntity>
@ -24215,7 +24225,7 @@ def doInfoBrowsers():
# gam move browsers ou|org|orgunit <OrgUnitPath> # gam move browsers ou|org|orgunit <OrgUnitPath>
# ((ids <DeviceIDList>) | # ((ids <DeviceIDList>) |
# (queries <QueryBrowserList> [querytime.* <Time>]) | # (queries <QueryBrowserList> [querytime<String> <Time>]) |
# (browserou <OrgUnitItem>) | (browserous <OrgUnitList>) | # (browserou <OrgUnitItem>) | (browserous <OrgUnitList>) |
# <FileSelector> | <CSVFileSelector>) # <FileSelector> | <CSVFileSelector>)
# [batchsize <Integer>] # [batchsize <Integer>]
@ -24354,13 +24364,13 @@ BROWSER_ORDERBY_CHOICE_MAP = {
# gam show browsers # gam show browsers
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser)|(queries <QueryBrowserList>))|(select <BrowserEntity>)) # ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
# [querytime.* <Time>] # [querytime<String> <Time>]
# [orderby <BrowserOrderByFieldName> [ascending|descending]] # [orderby <BrowserOrderByFieldName> [ascending|descending]]
# [basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>] # [basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>]
# [formatjson] # [formatjson]
# gam print browsers [todrive <ToDriveAttribute>*] # gam print browsers [todrive <ToDriveAttribute>*]
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser)|(queries <QueryBrowserList>))|(select <BrowserEntity>)) # ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
# [querytime.* <Time>] # [querytime<String> <Time>]
# [orderby <BrowserOrderByFieldName> [ascending|descending]] # [orderby <BrowserOrderByFieldName> [ascending|descending]]
# [basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>] # [basic|full|allfields|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>]
# [sortheaders] [formatjson [quotechar <Character>]] # [sortheaders] [formatjson [quotechar <Character>]]
@ -24556,13 +24566,13 @@ BROWSER_TOKEN_FIELDS_CHOICE_MAP = {
# gam show browsertokens # gam show browsertokens
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>))) # ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>)))
# [querytime.* <Time>] # [querytime<String> <Time>]
# [orderby <BrowserTokenFieldName> [ascending|descending]] # [orderby <BrowserTokenFieldName> [ascending|descending]]
# [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>] # [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
# [formatjson] # [formatjson]
# gam print browsertokens [todrive <ToDriveAttribute>*] # gam print browsertokens [todrive <ToDriveAttribute>*]
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>))) # ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>)))
# [querytime.* <Time>] # [querytime<String> <Time>]
# [orderby <BrowserTokenFieldName> [ascending|descending]] # [orderby <BrowserTokenFieldName> [ascending|descending]]
# [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>] # [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
# [sortheaders] [formatjson [quotechar <Character>]] # [sortheaders] [formatjson [quotechar <Character>]]
@ -26677,7 +26687,7 @@ DEVICE_MISSING_ACTION_MAP = {
# gam sync devices # gam sync devices
# <CSVFileSelector> # <CSVFileSelector>
# [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime.* <Time>)*] # [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
# (devicetype_column <String>)|(static_devicetype <DeviceType>) # (devicetype_column <String>)|(static_devicetype <DeviceType>)
# (serialnumber_column <String>) # (serialnumber_column <String>)
# [assettag_column <String>] # [assettag_column <String>]
@ -26976,7 +26986,7 @@ DEVICE_ORDERBY_CHOICE_MAP = {
} }
# gam print devices [todrive <ToDriveAttribute>*] # gam print devices [todrive <ToDriveAttribute>*]
# [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime.* <Time>)*] # [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
# <DeviceFieldName>* [fields <DeviceFieldNameList>] [userfields <DeviceUserFieldNameList>] # <DeviceFieldName>* [fields <DeviceFieldNameList>] [userfields <DeviceUserFieldNameList>]
# [orderby <DeviceOrderByFieldName> [ascending|descending]] # [orderby <DeviceOrderByFieldName> [ascending|descending]]
# [all|company|personal|nocompanydevices|nopersonaldevices] # [all|company|personal|nocompanydevices|nopersonaldevices]
@ -27163,7 +27173,7 @@ def doInfoCIDeviceUser():
# gam print deviceusers [todrive <ToDriveAttribute>*] # gam print deviceusers [todrive <ToDriveAttribute>*]
# [select <DeviceID>] # [select <DeviceID>]
# [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime.* <Time>)*] # [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
# <DeviceUserFieldName>* [fields <DevieUserFieldNameList>] # <DeviceUserFieldName>* [fields <DevieUserFieldNameList>]
# [orderby <DeviceOrderByFieldName> [ascending|descending]] # [orderby <DeviceOrderByFieldName> [ascending|descending]]
# [formatjson [quotechar <Character>]] # [formatjson [quotechar <Character>]]
@ -28948,7 +28958,7 @@ MOBILE_ORDERBY_CHOICE_MAP = {
} }
# gam print mobile [todrive <ToDriveAttribute>*] # gam print mobile [todrive <ToDriveAttribute>*]
# [(query <QueryMobile>)|(queries <QueryMobileList>) [querytime.* <Time>]] # [(query <QueryMobile>)|(queries <QueryMobileList>) [querytime<String> <Time>]]
# [orderby <MobileOrderByFieldName> [ascending|descending]] # [orderby <MobileOrderByFieldName> [ascending|descending]]
# [basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>] # [basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>]
# [delimiter <Character>] [appslimit <Number>] [oneappperrow] [listlimit <Number>] # [delimiter <Character>] [appslimit <Number>] [oneappperrow] [listlimit <Number>]
@ -32006,53 +32016,68 @@ def doShowGroupMembers():
if checkGroupMatchPatterns(groupEmail, group, matchPatterns): if checkGroupMatchPatterns(groupEmail, group, matchPatterns):
_showGroup(groupEmail, 0) _showGroup(groupEmail, 0)
def getGroupParents(cd, groupParents, groupEmail, groupName, kwargs):
groupParents[groupEmail] = {'name': groupName, 'parents': []}
_setUserGroupArgs(groupEmail, kwargs)
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email,name)', **kwargs)
for parentGroup in entityList:
groupParents[groupEmail]['parents'].append(parentGroup['email'])
if parentGroup['email'] not in groupParents:
getGroupParents(cd, groupParents, parentGroup['email'], parentGroup['name'], kwargs)
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, groupEmail)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
def showGroupParents(groupParents, groupEmail, role, i, count):
kvList = [groupEmail, f'{groupParents[groupEmail]["name"]}']
if role:
kvList.extend([Ent.Singular(Ent.ROLE), role])
printKeyValueListWithCount(kvList, i, count)
Ind.Increment()
for parentEmail in groupParents[groupEmail]['parents']:
showGroupParents(groupParents, parentEmail, None, 0, 0)
Ind.Decrement()
def addJsonGroupParents(groupParents, userGroup, groupEmail):
userGroup.setdefault('parents', [])
for parentEmail in groupParents[groupEmail]['parents']:
userGroup['parents'].append({'email': parentEmail, 'name': groupParents[parentEmail]['name'], 'parents': []})
addJsonGroupParents(groupParents, userGroup['parents'][-1], parentEmail)
def printGroupParents(groupParents, groupEmail, row, csvPF, delimiter, showParentsAsList):
if groupParents[groupEmail]['parents']:
for parentEmail in groupParents[groupEmail]['parents']:
row['parents'].append({'email': parentEmail, 'name': groupParents[parentEmail]['name']})
printGroupParents(groupParents, parentEmail, row, csvPF, delimiter, showParentsAsList)
del row['parents'][-1]
else:
if not showParentsAsList:
csvPF.WriteRowTitles(flattenJSON(row))
else:
crow = row.copy()
if 'Role' in row:
crow['Role'] = row['Role']
parents = crow.pop('parents')
crow['ParentsCount'] = len(parents)
crow['Parents'] = delimiter.join([parent['email'] for parent in parents])
crow['ParentsName'] = delimiter.join([parent['name'] for parent in parents])
csvPF.WriteRow(flattenJSON(crow))
# gam print grouptree <GroupEntity> [todrive <ToDriveAttribute>*] # gam print grouptree <GroupEntity> [todrive <ToDriveAttribute>*]
# [showparentsaslist [<Boolean>]] [delimiter <Character>] # [showparentsaslist [<Boolean>]] [delimiter <Character>]
# [formatjson [quotechar <Character>]]
# gam show grouptree <GroupEntity> # gam show grouptree <GroupEntity>
# [formatjson]
def doPrintShowGroupTree(): def doPrintShowGroupTree():
def getGroupParents(groupEmail, groupName):
groupParents[groupEmail] = {'name': groupName, 'parents': []}
_setUserGroupArgs(groupEmail, kwargs)
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email,name)', **kwargs)
for parentGroup in entityList:
groupParents[groupEmail]['parents'].append(parentGroup['email'])
if parentGroup['email'] not in groupParents:
getGroupParents(parentGroup['email'], parentGroup['name'])
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, groupEmail)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
def showGroupParents(groupEmail, i, count):
printKeyValueListWithCount([f'{groupEmail}: {groupParents[groupEmail]["name"]}'], i, count)
Ind.Increment()
for parentEmail in groupParents[groupEmail]['parents']:
showGroupParents(parentEmail, 0, 0)
Ind.Decrement()
def printGroupParents(groupEmail, row):
if groupParents[groupEmail]['parents']:
for parentEmail in groupParents[groupEmail]['parents']:
row['parents'].append({'email': parentEmail, 'name': groupParents[parentEmail]['name']})
printGroupParents(parentEmail, row)
del row['parents'][-1]
else:
if not showParentsAsList:
csvPF.WriteRowTitles(flattenJSON(row))
else:
crow = {'Group': row['Group'], 'Name': row['Name']}
crow['ParentsCount'] = len(row['parents'])
crow['Parents'] = delimiter.join([parent['email'] for parent in row['parents']])
crow['ParentsName'] = delimiter.join([parent['name'] for parent in row['parents']])
csvPF.WriteRow(flattenJSON(crow))
cd = buildGAPIObject(API.DIRECTORY) cd = buildGAPIObject(API.DIRECTORY)
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]} kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
csvPF = CSVPrintFile(['Group', 'Name']) if Act.csvFormat() else None csvPF = CSVPrintFile(['Group', 'Name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER] delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
showParentsAsList = False showParentsAsList = False
entityList = getEntityList(Cmd.OB_GROUP_ENTITY) entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
@ -32065,8 +32090,8 @@ def doPrintShowGroupTree():
elif csvPF and myarg == 'showparentsaslist': elif csvPF and myarg == 'showparentsaslist':
showParentsAsList = getBoolean() showParentsAsList = getBoolean()
else: else:
unknownArgumentExit() FJQC.GetFormatJSONQuoteChar(myarg, True)
if csvPF: if csvPF and not FJQC.formatJSON:
if not showParentsAsList: if not showParentsAsList:
csvPF.SetIndexedTitles(['parents']) csvPF.SetIndexedTitles(['parents'])
else: else:
@ -32074,7 +32099,7 @@ def doPrintShowGroupTree():
groupParents = {} groupParents = {}
i = 0 i = 0
count = len(entityList) count = len(entityList)
if not csvPF: if not csvPF and not FJQC.formatJSON:
performActionNumItems(count, Ent.GROUP_TREE) performActionNumItems(count, Ent.GROUP_TREE)
for group in entityList: for group in entityList:
i += 1 i += 1
@ -32088,12 +32113,24 @@ def doPrintShowGroupTree():
GAPI.invalid, GAPI.systemError) as e: GAPI.invalid, GAPI.systemError) as e:
entityActionFailedWarning([Ent.GROUP, groupEmail], str(e), i, count) entityActionFailedWarning([Ent.GROUP, groupEmail], str(e), i, count)
continue continue
getGroupParents(groupEmail, groupName) getGroupParents(cd, groupParents, groupEmail, groupName, kwargs)
if not csvPF: if not FJQC.formatJSON:
showGroupParents(groupEmail, i, count) if not csvPF:
showGroupParents(groupParents, groupEmail, None, i, count)
else:
row = {'Group': groupEmail, 'Name': groupParents[groupEmail]['name'], 'parents': []}
printGroupParents(groupParents, groupEmail, row, csvPF, delimiter, showParentsAsList)
else: else:
row = {'Group': groupEmail, 'Name': groupParents[groupEmail]['name'], 'parents': []} groupInfo = {'email': groupEmail, 'name': groupParents[groupEmail]['name'], 'parents': []}
printGroupParents(groupEmail, row) addJsonGroupParents(groupParents, groupInfo, groupEmail)
if not csvPF:
printLine(json.dumps(cleanJSON(groupInfo), ensure_ascii=False, sort_keys=True))
else:
row = flattenJSON(groupInfo)
if csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'Group': groupEmail, 'Name': groupParents[groupEmail]['name'],
'JSON': json.dumps(cleanJSON(groupInfo),
ensure_ascii=False, sort_keys=True)})
if csvPF: if csvPF:
csvPF.writeCSVfile('Group Tree') csvPF.writeCSVfile('Group Tree')
@ -41513,29 +41550,6 @@ def _formatLanguagesList(propertyValue, delimiter):
return delimiter.join(languages) return delimiter.join(languages)
def infoUsers(entityList): def infoUsers(entityList):
def getGroupParents(groupEmail, groupName):
groupParents[groupEmail] = {'name': groupName, 'parents': []}
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
userKey=groupEmail, orderBy='email', fields='nextPageToken,groups(email,name)')
for parentGroup in entityList:
groupParents[groupEmail]['parents'].append(parentGroup['email'])
if parentGroup['email'] not in groupParents:
getGroupParents(parentGroup['email'], parentGroup['name'])
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, groupEmail)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
def showGroupParents(groupEmail):
printKeyValueList([groupEmail, f'{groupParents[groupEmail]["name"]}'])
Ind.Increment()
for parentEmail in groupParents[groupEmail]['parents']:
showGroupParents(parentEmail)
Ind.Decrement()
def printUserCIGroupMap(parent, group_name_mappings, seen_group_count, edges, direction): def printUserCIGroupMap(parent, group_name_mappings, seen_group_count, edges, direction):
for a_parent, a_child in edges: for a_parent, a_child in edges:
if a_parent == parent: if a_parent == parent:
@ -41662,8 +41676,14 @@ def infoUsers(entityList):
getCIGroupsTree = False getCIGroupsTree = False
licenses = getUserLicenses(lic, user, skus) if getLicenses else [] licenses = getUserLicenses(lic, user, skus) if getLicenses else []
if FJQC.formatJSON: if FJQC.formatJSON:
if getGroups: if getGroups or getGroupsTree:
user['groups'] = groups user['groups'] = groups
if getGroupsTree:
for group in user['groups']:
groupEmail = group['email']
if groupEmail not in groupParents:
getGroupParents(cd, groupParents, groupEmail, group['name'], {})
addJsonGroupParents(groupParents, group, groupEmail)
if getLicenses: if getLicenses:
user['licenses'] = [SKU.formatSKUIdDisplayName(u_license) for u_license in licenses] user['licenses'] = [SKU.formatSKUIdDisplayName(u_license) for u_license in licenses]
if not getAliases: if not getAliases:
@ -41901,8 +41921,8 @@ def infoUsers(entityList):
for group in groups: for group in groups:
groupEmail = group['email'] groupEmail = group['email']
if groupEmail not in groupParents: if groupEmail not in groupParents:
getGroupParents(groupEmail, group['name']) getGroupParents(cd, groupParents, groupEmail, group['name'], {})
showGroupParents(groupEmail) showGroupParents(groupParents, groupEmail, None, 0, 0)
Ind.Decrement() Ind.Decrement()
elif getCIGroupsTree: elif getCIGroupsTree:
printEntity([Ent.GROUP_MEMBERSHIP_TREE, '']) printEntity([Ent.GROUP_MEMBERSHIP_TREE, ''])
@ -52092,7 +52112,7 @@ FILECOUNT_SUMMARY_CHOICE_MAP = {
FILECOUNT_SUMMARY_USER = 'Summary' FILECOUNT_SUMMARY_USER = 'Summary'
# gam <UserTypeEntity> print filelist [todrive <ToDriveAttribute>*] # gam <UserTypeEntity> print filelist [todrive <ToDriveAttribute>*]
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime.* <Time>)*] # [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
# [choose <DriveFileNameEntity>|<DriveFileEntityShortcut>] # [choose <DriveFileNameEntity>|<DriveFileEntityShortcut>]
# [corpora <CorporaAttribute>] # [corpora <CorporaAttribute>]
# [select <DriveFileEntity> [selectsubquery <QueryDriveFile>] # [select <DriveFileEntity> [selectsubquery <QueryDriveFile>]
@ -52822,7 +52842,7 @@ def printShowFilePaths(users):
csvPF.writeCSVfile('Drive File Paths') csvPF.writeCSVfile('Drive File Paths')
# gam <UserTypeEntity> print filecounts [todrive <ToDriveAttribute>*] # gam <UserTypeEntity> print filecounts [todrive <ToDriveAttribute>*]
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime.* <Time>)*] # [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
# [corpora <CorporaAttribute>] # [corpora <CorporaAttribute>]
# [select <SharedDriveEntity>] # [select <SharedDriveEntity>]
# [anyowner|(showownedby any|me|others)] # [anyowner|(showownedby any|me|others)]
@ -52832,7 +52852,7 @@ def printShowFilePaths(users):
# [excludetrashed] # [excludetrashed]
# [summary none|only|plus] [summaryuser <String>] [showsize] # [summary none|only|plus] [summaryuser <String>] [showsize]
# gam <UserTypeEntity> show filecounts # gam <UserTypeEntity> show filecounts
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime.* <Time>)*] # [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
# [corpora <CorporaAttribute>] # [corpora <CorporaAttribute>]
# [select <SharedDriveEntity>] # [select <SharedDriveEntity>]
# [anyowner|(showownedby any|me|others)] # [anyowner|(showownedby any|me|others)]
@ -54765,7 +54785,8 @@ def _getUniqueFilename(destFilename, mimeType, targetChildren):
def _copyPermissions(drive, user, i, count, j, jcount, def _copyPermissions(drive, user, i, count, j, jcount,
entityType, fileId, fileTitle, newFileId, newFileTitle, entityType, fileId, fileTitle, newFileId, newFileTitle,
statistics, stat, copyMoveOptions, atTop, copyInherited, copyNonInherited): statistics, stat, copyMoveOptions, atTop, copyInherited, copyNonInherited,
updateOwner):
def getPermissions(fid): def getPermissions(fid):
permissions = {} permissions = {}
try: try:
@ -54800,12 +54821,14 @@ def _copyPermissions(drive, user, i, count, j, jcount,
def isPermissionCopyable(kvList, permission): def isPermissionCopyable(kvList, permission):
role = permission['role'] role = permission['role']
if permission['type'] in {'group', 'user'}:
emailAddress = permission.get('emailAddress', '')
domain = '' domain = ''
if copyMoveOptions['excludePermissionsFromDomains'] or copyMoveOptions['includePermissionsFromDomains']: if copyMoveOptions['excludePermissionsFromDomains'] or copyMoveOptions['includePermissionsFromDomains']:
if permission['type'] in {'group', 'user'}: if permission['type'] in {'group', 'user'}:
atLoc = permission.get('emailAddress', '').find('@') atLoc = emailAddress.find('@')
if atLoc > 0: if atLoc > 0:
domain = permission['emailAddress'][atLoc+1:] domain = emailAddress[atLoc+1:]
elif permission['type'] == 'domain': elif permission['type'] == 'domain':
domain = permission.get('domain', '') domain = permission.get('domain', '')
if permission['inherited'] and not copyMoveOptions[copyInherited]: if permission['inherited'] and not copyMoveOptions[copyInherited]:
@ -54813,7 +54836,13 @@ def _copyPermissions(drive, user, i, count, j, jcount,
elif not permission['inherited'] and copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_NEVER: elif not permission['inherited'] and copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_NEVER:
notCopiedMessage = 'noninherited not selected' notCopiedMessage = 'noninherited not selected'
elif role == 'owner': elif role == 'owner':
notCopiedMessage = f'role {role} copy not required/appropriate' if emailAddress == user or copyMoveOptions['destDriveId'] or not updateOwner:
notCopiedMessage = f'role {role} copy not required/appropriate'
else:
permission['role'] = 'writer'
return True
elif updateOwner and emailAddress == user:
notCopiedMessage = 'user is now owner'
elif domain and domain in copyMoveOptions['excludePermissionsFromDomains']: elif domain and domain in copyMoveOptions['excludePermissionsFromDomains']:
notCopiedMessage = f'domain {domain} excluded' notCopiedMessage = f'domain {domain} excluded'
elif domain and copyMoveOptions['includePermissionsFromDomains'] and domain not in copyMoveOptions['includePermissionsFromDomains']: elif domain and copyMoveOptions['includePermissionsFromDomains'] and domain not in copyMoveOptions['includePermissionsFromDomains']:
@ -55301,7 +55330,8 @@ def copyDriveFile(users):
statistics, STAT_FOLDER_PERMISSIONS_FAILED, statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, True, copyMoveOptions, True,
'copyTopFolderInheritedPermissions', 'copyTopFolderInheritedPermissions',
copyFolderNonInheritedPermissions) copyFolderNonInheritedPermissions,
True)
return (newParentId, newParentName, True) return (newParentId, newParentName, True)
# Merge parent folders # Merge parent folders
if copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE: if copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE:
@ -55330,7 +55360,8 @@ def copyDriveFile(users):
statistics, STAT_FOLDER_PERMISSIONS_FAILED, statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, atTop, copyMoveOptions, atTop,
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop], ['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
copyFolderNonInheritedPermissions) copyFolderNonInheritedPermissions,
False)
return (newFolderId, newFolderName, True) return (newFolderId, newFolderName, True)
entityActionFailedWarning(kvList+[Ent.DRIVE_FOLDER, newParentNameId], Msg.NOT_WRITABLE, j, jcount) entityActionFailedWarning(kvList+[Ent.DRIVE_FOLDER, newParentNameId], Msg.NOT_WRITABLE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_NOT_WRITABLE) _incrStatistic(statistics, STAT_FOLDER_NOT_WRITABLE)
@ -55375,7 +55406,8 @@ def copyDriveFile(users):
statistics, STAT_FOLDER_PERMISSIONS_FAILED, statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, False, copyMoveOptions, False,
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop], ['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop]) ['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
True)
return (newFolderId, newFolderName, False) return (newFolderId, newFolderName, False)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.internalError, GAPI.storageQuotaExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.badRequest) as e: GAPI.internalError, GAPI.storageQuotaExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.badRequest) as e:
@ -55570,7 +55602,8 @@ def copyDriveFile(users):
statistics, STAT_FILE_PERMISSIONS_FAILED, statistics, STAT_FILE_PERMISSIONS_FAILED,
copyMoveOptions, False, copyMoveOptions, False,
'copySheetProtectedRangesInheritedPermissions', 'copySheetProtectedRangesInheritedPermissions',
'copySheetProtectedRangesNonInheritedPermissions') 'copySheetProtectedRangesNonInheritedPermissions',
True)
_updateSheetProtectedRanges(sheet, user, i, count, k, kcount, result['id'], result['name'], protectedSheetRanges, _updateSheetProtectedRanges(sheet, user, i, count, k, kcount, result['id'], result['name'], protectedSheetRanges,
statistics, STAT_FILE_PROTECTEDRANGES_FAILED) statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
elif copyMoveOptions['copyFilePermissions']: elif copyMoveOptions['copyFilePermissions']:
@ -55579,7 +55612,8 @@ def copyDriveFile(users):
statistics, STAT_FILE_PERMISSIONS_FAILED, statistics, STAT_FILE_PERMISSIONS_FAILED,
copyMoveOptions, False, copyMoveOptions, False,
'copyFileInheritedPermissions', 'copyFileInheritedPermissions',
'copyFileNonInheritedPermissions') 'copyFileNonInheritedPermissions',
True)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
GAPI.insufficientParentPermissions, GAPI.unknownError, GAPI.insufficientParentPermissions, GAPI.unknownError,
GAPI.invalid, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable, GAPI.invalid, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
@ -55830,7 +55864,8 @@ def copyDriveFile(users):
statistics, STAT_FILE_PERMISSIONS_FAILED, statistics, STAT_FILE_PERMISSIONS_FAILED,
copyMoveOptions, False, copyMoveOptions, False,
'copySheetProtectedRangesInheritedPermissions', 'copySheetProtectedRangesInheritedPermissions',
'copySheetProtectedRangesNonInheritedPermissions') 'copySheetProtectedRangesNonInheritedPermissions',
True)
_updateSheetProtectedRanges(sheet, user, i, count, j, jcount, result['id'], result['name'], protectedSheetRanges, _updateSheetProtectedRanges(sheet, user, i, count, j, jcount, result['id'], result['name'], protectedSheetRanges,
statistics, STAT_FILE_PROTECTEDRANGES_FAILED) statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
elif copyMoveOptions['copyFilePermissions']: elif copyMoveOptions['copyFilePermissions']:
@ -55839,7 +55874,8 @@ def copyDriveFile(users):
statistics, STAT_FILE_PERMISSIONS_FAILED, statistics, STAT_FILE_PERMISSIONS_FAILED,
copyMoveOptions, False, copyMoveOptions, False,
'copyFileInheritedPermissions', 'copyFileInheritedPermissions',
'copyFileNonInheritedPermissions') 'copyFileNonInheritedPermissions',
True)
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
GAPI.insufficientParentPermissions, GAPI.unknownError, GAPI.insufficientParentPermissions, GAPI.unknownError,
GAPI.invalid, GAPI.badRequest, GAPI.cannotCopyFile, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable, GAPI.invalid, GAPI.badRequest, GAPI.cannotCopyFile, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
@ -56072,7 +56108,8 @@ def moveDriveFile(users):
statistics, STAT_FOLDER_PERMISSIONS_FAILED, statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, True, copyMoveOptions, True,
'copyTopFolderInheritedPermissions', 'copyTopFolderInheritedPermissions',
copyFolderNonInheritedPermissions) copyFolderNonInheritedPermissions,
False)
source.pop('oldparents', None) source.pop('oldparents', None)
return (newParentId, newParentName, True) return (newParentId, newParentName, True)
# Merge parent folders # Merge parent folders
@ -56101,7 +56138,8 @@ def moveDriveFile(users):
statistics, STAT_FOLDER_PERMISSIONS_FAILED, statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, atTop, copyMoveOptions, atTop,
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop], ['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
copyFolderNonInheritedPermissions) copyFolderNonInheritedPermissions,
False)
return (newFolderId, newFolderName, True) return (newFolderId, newFolderName, True)
entityActionFailedWarning(kvList+[Ent.DRIVE_FOLDER, newParentNameId], Msg.NOT_WRITABLE, j, jcount) entityActionFailedWarning(kvList+[Ent.DRIVE_FOLDER, newParentNameId], Msg.NOT_WRITABLE, j, jcount)
_incrStatistic(statistics, STAT_FOLDER_NOT_WRITABLE) _incrStatistic(statistics, STAT_FOLDER_NOT_WRITABLE)
@ -56187,7 +56225,8 @@ def moveDriveFile(users):
statistics, STAT_FOLDER_PERMISSIONS_FAILED, statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, False, copyMoveOptions, False,
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop], ['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop]) ['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
True)
return (newFolderId, newFolderName, False) return (newFolderId, newFolderName, False)
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
GAPI.internalError, GAPI.storageQuotaExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.internalError, GAPI.storageQuotaExceeded, GAPI.teamDriveHierarchyTooDeep,
@ -59575,7 +59614,7 @@ def doInfoDriveFileACLs():
infoDriveFileACLs([_getAdminEmail()], True) infoDriveFileACLs([_getAdminEmail()], True)
DRIVEFILE_BASIC_PERMISSION_FIELDS = [ DRIVEFILE_BASIC_PERMISSION_FIELDS = [
'id', 'emailAddress', 'domain', 'role', 'type', 'displayName', 'id', 'emailAddress', 'domain', 'role', 'type',
'allowFileDiscovery', 'expirationTime', 'deleted' 'allowFileDiscovery', 'expirationTime', 'deleted'
] ]
@ -60929,7 +60968,8 @@ def copySyncSharedDriveACLs(users, useDomainAdminAccess=False):
statistics, STAT_FOLDER_PERMISSIONS_FAILED, statistics, STAT_FOLDER_PERMISSIONS_FAILED,
copyMoveOptions, True, copyMoveOptions, True,
'copyTopFolderInheritedPermissions', 'copyTopFolderInheritedPermissions',
'copyTopFolderNonInheritedPermissions') 'copyTopFolderNonInheritedPermissions',
False)
def doCopySyncSharedDriveACLs(): def doCopySyncSharedDriveACLs():
copySyncSharedDriveACLs([_getAdminEmail()], True) copySyncSharedDriveACLs([_getAdminEmail()], True)
@ -62162,58 +62202,16 @@ def printShowUserGroups(users):
# [(domain <DomainName>)|(customerid <CustomerID>)] # [(domain <DomainName>)|(customerid <CustomerID>)]
# [roles <GroupRoleList>] # [roles <GroupRoleList>]
# [showparentsaslist [<Boolean>]] [delimiter <Character>] # [showparentsaslist [<Boolean>]] [delimiter <Character>]
# [formatjson [quotechar <Character>]]
# gam <UserTypeEntity> show grouptree # gam <UserTypeEntity> show grouptree
# [(domain <DomainName>)|(customerid <CustomerID>)] # [(domain <DomainName>)|(customerid <CustomerID>)]
# [roles <GroupRoleList>] # [roles <GroupRoleList>]
# [formatjson]
def printShowGroupTree(users): def printShowGroupTree(users):
def getGroupParents(groupEmail, groupName):
groupParents[groupEmail] = {'name': groupName, 'parents': []}
_setUserGroupArgs(groupEmail, kwargs)
try:
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
orderBy='email', fields='nextPageToken,groups(email,name)', **kwargs)
for parentGroup in entityList:
groupParents[groupEmail]['parents'].append(parentGroup['email'])
if parentGroup['email'] not in groupParents:
getGroupParents(parentGroup['email'], parentGroup['name'])
except (GAPI.invalidMember, GAPI.invalidInput):
badRequestWarning(Ent.GROUP, Ent.MEMBER, groupEmail)
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
accessErrorExit(cd)
def showGroupParents(groupEmail, role, i, count):
kvList = [groupEmail, f'{groupParents[groupEmail]["name"]}']
if role:
kvList.extend([Ent.Singular(Ent.ROLE), role])
printKeyValueListWithCount(kvList, i, count)
Ind.Increment()
for parentEmail in groupParents[groupEmail]['parents']:
showGroupParents(parentEmail, None, 0, 0)
Ind.Decrement()
def printGroupParents(groupEmail, row):
if groupParents[groupEmail]['parents']:
for parentEmail in groupParents[groupEmail]['parents']:
row['parents'].append({'email': parentEmail, 'name': groupParents[parentEmail]['name']})
printGroupParents(parentEmail, row)
del row['parents'][-1]
else:
if not showParentsAsList:
csvPF.WriteRowTitles(flattenJSON(row))
else:
crow = {'User': row['User'], 'Group': row['Group'], 'Name': row['Name']}
if rolesSet:
crow['Role'] = row['Role']
crow['ParentsCount'] = len(row['parents'])
crow['Parents'] = delimiter.join([parent['email'] for parent in row['parents']])
crow['ParentsName'] = delimiter.join([parent['name'] for parent in row['parents']])
csvPF.WriteRow(flattenJSON(crow))
cd = buildGAPIObject(API.DIRECTORY) cd = buildGAPIObject(API.DIRECTORY)
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]} kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
csvPF = CSVPrintFile(['User', 'Group', 'Name']) if Act.csvFormat() else None csvPF = CSVPrintFile(['User', 'Group', 'Name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER] delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
showParentsAsList = False showParentsAsList = False
rolesSet = set() rolesSet = set()
@ -62234,14 +62232,19 @@ def printShowGroupTree(users):
elif csvPF and myarg == 'showparentsaslist': elif csvPF and myarg == 'showparentsaslist':
showParentsAsList = getBoolean() showParentsAsList = getBoolean()
else: else:
unknownArgumentExit() FJQC.GetFormatJSONQuoteChar(myarg, False)
if csvPF: if csvPF:
if rolesSet: if rolesSet:
csvPF.AddTitles('Role') csvPF.AddTitles('Role')
if not showParentsAsList: if not FJQC.formatJSON:
csvPF.SetIndexedTitles(['parents']) if not showParentsAsList:
csvPF.SetIndexedTitles(['parents'])
else:
csvPF.AddTitles(['ParentsCount', 'Parents', 'ParentsName'])
else: else:
csvPF.AddTitles(['ParentsCount', 'Parents', 'ParentsName']) if rolesSet:
csvPF.AddJSONTitles('Role')
csvPF.AddJSONTitles('JSON')
allRoles = rolesSet == ALL_GROUP_ROLES allRoles = rolesSet == ALL_GROUP_ROLES
groupParents = {} groupParents = {}
i, count, users = getEntityArgument(users) i, count, users = getEntityArgument(users)
@ -62259,7 +62262,7 @@ def printShowGroupTree(users):
continue continue
j = 0 j = 0
jcount = len(groups) jcount = len(groups)
if not csvPF: if not csvPF and not FJQC.formatJSON:
if allRoles: if allRoles:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.GROUP_TREE, i, count) entityPerformActionNumItems([Ent.USER, user], jcount, Ent.GROUP_TREE, i, count)
else: else:
@ -62269,7 +62272,7 @@ def printShowGroupTree(users):
j += 1 j += 1
groupEmail = group['email'] groupEmail = group['email']
if groupEmail not in groupParents: if groupEmail not in groupParents:
getGroupParents(groupEmail, group['name']) getGroupParents(cd, groupParents, groupEmail, group['name'], kwargs)
if rolesSet: if rolesSet:
try: try:
result = callGAPI(cd.members(), 'get', result = callGAPI(cd.members(), 'get',
@ -62287,13 +62290,30 @@ def printShowGroupTree(users):
continue continue
else: else:
role = None role = None
if not csvPF: if not FJQC.formatJSON:
showGroupParents(groupEmail, role, j, jcount) if not csvPF:
showGroupParents(groupParents, groupEmail, role, j, jcount)
else:
row = {'User': user, 'Group': groupEmail, 'Name': group['name'], 'parents': []}
if role is not None:
row['Role'] = role
printGroupParents(groupParents, groupEmail, row, csvPF, delimiter, showParentsAsList)
else: else:
row = {'User': user, 'Group': groupEmail, 'Name': group['name'], 'parents': []} groupInfo = {'email': groupEmail, 'name': group['name'], 'parents': []}
if rolesSet: if role is not None:
row['Role'] = role groupInfo['role'] = role
printGroupParents(groupEmail, row) addJsonGroupParents(groupParents, groupInfo, groupEmail)
if not csvPF:
printLine(json.dumps(cleanJSON(groupInfo), ensure_ascii=False, sort_keys=True))
else:
row = flattenJSON(groupInfo)
if csvPF.CheckRowTitles(row):
row = {'User': user, 'Group': groupEmail, 'Name': group['name']}
if rolesSet:
row['Role'] = role
row['JSON'] = json.dumps(cleanJSON(groupInfo),
ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
Ind.Decrement() Ind.Decrement()
if csvPF: if csvPF:
csvPF.writeCSVfile('User Group Trees') csvPF.writeCSVfile('User Group Trees')
@ -64599,7 +64619,8 @@ MESSAGES_MAX_TO_KEYWORDS = {
def _initMessageThreadParameters(entityType, doIt, maxToProcess): def _initMessageThreadParameters(entityType, doIt, maxToProcess):
listType = 'messages' if entityType == Ent.MESSAGE else 'threads' listType = 'messages' if entityType == Ent.MESSAGE else 'threads'
return {'currLabelOp': 'and', 'prevLabelOp': 'and', 'labelGroupOpen': False, 'query': '', return {'currLabelOp': 'and', 'prevLabelOp': 'and', 'labelGroupOpen': False,
'query': '', 'queryTimes': {},
'entityType': entityType, 'messageEntity': None, 'doIt': doIt, 'quick': True, 'entityType': entityType, 'messageEntity': None, 'doIt': doIt, 'quick': True,
'labelMatchPattern': None, 'senderMatchPattern': None, 'labelMatchPattern': None, 'senderMatchPattern': None,
'maxToProcess': maxToProcess, 'maxItems': 0, 'maxToProcess': maxToProcess, 'maxItems': 0,
@ -64611,6 +64632,8 @@ LABEL_QUERY_REPLACEMENT_CHARACTERS = ' &()"|{}/'
def _getMessageSelectParameters(myarg, parameters): def _getMessageSelectParameters(myarg, parameters):
if myarg == 'query': if myarg == 'query':
parameters['query'] += f' ({getString(Cmd.OB_QUERY)})' parameters['query'] += f' ({getString(Cmd.OB_QUERY)})'
elif myarg.startswith('querytime'):
parameters['queryTimes'][myarg] = getDateOrDeltaFromNow().replace('-', '/')
elif myarg == 'matchlabel': elif myarg == 'matchlabel':
labelTemp = getString(Cmd.OB_LABEL_NAME).lower() labelTemp = getString(Cmd.OB_LABEL_NAME).lower()
labelName = '' labelName = ''
@ -64667,6 +64690,9 @@ def _finalizeMessageSelectParameters(parameters, queryOrIdsRequired):
if parameters['query']: if parameters['query']:
if parameters['labelGroupOpen']: if parameters['labelGroupOpen']:
parameters['query'] += ')' parameters['query'] += ')'
if parameters['queryTimes']:
for queryTimeName, queryTimeValue in iter(parameters['queryTimes'].items()):
parameters['query'] = parameters['query'].replace(f'#{queryTimeName}#', queryTimeValue)
_mapMessageQueryDates(parameters) _mapMessageQueryDates(parameters)
elif queryOrIdsRequired and parameters['messageEntity'] is None: elif queryOrIdsRequired and parameters['messageEntity'] is None:
missingArgumentExit('query|matchlabel|ids') missingArgumentExit('query|matchlabel|ids')
@ -64675,7 +64701,7 @@ def _finalizeMessageSelectParameters(parameters, queryOrIdsRequired):
parameters['maxItems'] = parameters['maxToProcess'] if parameters['quick'] and not parameters['labelMatchPattern'] else 0 parameters['maxItems'] = parameters['maxToProcess'] if parameters['quick'] and not parameters['labelMatchPattern'] else 0
# gam <UserTypeEntity> archive messages <GroupItem> # gam <UserTypeEntity> archive messages <GroupItem>
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_archive <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_archive <Number>])|(ids <MessageIDEntity>)
def archiveMessages(users): def archiveMessages(users):
entityType = Ent.MESSAGE entityType = Ent.MESSAGE
parameters = _initMessageThreadParameters(entityType, False, 0) parameters = _initMessageThreadParameters(entityType, False, 0)
@ -64905,30 +64931,30 @@ def _processMessagesThreads(users, entityType):
Ind.Decrement() Ind.Decrement()
# gam <UserTypeEntity> delete message|messages # gam <UserTypeEntity> delete message|messages
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_delete <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_delete <Number>])|(ids <MessageIDEntity>)
# gam <UserTypeEntity> modify message|messages # gam <UserTypeEntity> modify message|messages
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_modify <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_modify <Number>])|(ids <MessageIDEntity>)
# (addlabel <LabelName>)* (removelabel <LabelName>)* # (addlabel <LabelName>)* (removelabel <LabelName>)*
# gam <UserTypeEntity> spam message|messages # gam <UserTypeEntity> spam message|messages
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_spam <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_spam <Number>])|(ids <MessageIDEntity>)
# gam <UserTypeEntity> trash message|messages # gam <UserTypeEntity> trash message|messages
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>)
# gam <UserTypeEntity> untrash message|messages # gam <UserTypeEntity> untrash message|messages
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <MessageIDEntity>)
def processMessages(users): def processMessages(users):
_processMessagesThreads(users, Ent.MESSAGE) _processMessagesThreads(users, Ent.MESSAGE)
# gam <UserTypeEntity> delete thread|threads # gam <UserTypeEntity> delete thread|threads
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_delete <Number>])|(ids <ThreadIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_delete <Number>])|(ids <ThreadIDEntity>)
# gam <UserTypeEntity> modify thread|threads # gam <UserTypeEntity> modify thread|threads
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_modify <Number>])|(ids <ThreadIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_modify <Number>])|(ids <ThreadIDEntity>)
# (addlabel <LabelName>)* (removelabel <LabelName>)* # (addlabel <LabelName>)* (removelabel <LabelName>)*
# gam <UserTypeEntity> spam thread|threads # gam <UserTypeEntity> spam thread|threads
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_spam <Number>])|(ids <ThreadIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_spam <Number>])|(ids <ThreadIDEntity>)
# gam <UserTypeEntity> trash thread|threads # gam <UserTypeEntity> trash thread|threads
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>)
# gam <UserTypeEntity> untrash thread|threads # gam <UserTypeEntity> untrash thread|threads
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <ThreadIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <ThreadIDEntity>)
def processThreads(users): def processThreads(users):
_processMessagesThreads(users, Ent.THREAD) _processMessagesThreads(users, Ent.THREAD)
@ -65038,13 +65064,13 @@ def exportMessagesThreads(users, entityType):
Ind.Decrement() Ind.Decrement()
# gam <UserTypeEntity> export message|messages # gam <UserTypeEntity> export message|messages
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <MessageIDEntity>)
# [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]] # [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
def exportMessages(users): def exportMessages(users):
exportMessagesThreads(users, Ent.MESSAGE) exportMessagesThreads(users, Ent.MESSAGE)
# gam <UserTypeEntity> export thread|threads # gam <UserTypeEntity> export thread|threads
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <ThreadIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_export <Number>])|(ids <ThreadIDEntity>)
# [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]] # [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
def exportThreads(users): def exportThreads(users):
exportMessagesThreads(users, Ent.THREAD) exportMessagesThreads(users, Ent.THREAD)
@ -65064,10 +65090,10 @@ def _decodeHeader(header):
return header return header
# gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity> # gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity>
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>)
# [subject <String>] [altcharset <String>] # [subject <String>] [altcharset <String>]
# gam <UserTypeEntity> forward thread|threads recipient|to <RecipientEntity> # gam <UserTypeEntity> forward thread|threads recipient|to <RecipientEntity>
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>)
# [subject <String>] [altcharset <String>] # [subject <String>] [altcharset <String>]
def forwardMessagesThreads(users, entityType): def forwardMessagesThreads(users, entityType):
def getRecipients(): def getRecipients():
@ -65371,7 +65397,7 @@ def _draftImportInsertMessage(users, operation):
emlFile = True emlFile = True
internalDateSource = 'dateHeader' internalDateSource = 'dateHeader'
elif myarg == 'replace': elif myarg == 'replace':
_getTagReplacement(tagReplacements, False) _getTagReplacement(tagReplacements, True)
elif operation in IMPORT_INSERT and myarg == 'addlabel': elif operation in IMPORT_INSERT and myarg == 'addlabel':
addLabelNames.append(getString(Cmd.OB_LABEL_NAME, minLen=1)) addLabelNames.append(getString(Cmd.OB_LABEL_NAME, minLen=1))
elif operation in IMPORT_INSERT and myarg == 'labels': elif operation in IMPORT_INSERT and myarg == 'labels':
@ -66184,7 +66210,7 @@ def printShowMessagesThreads(users, entityType):
csvPF.writeCSVfile('Message Counts' if not show_labels else 'Message Label Counts') csvPF.writeCSVfile('Message Counts' if not show_labels else 'Message Label Counts')
# gam <UserTypeEntity> print message|messages # gam <UserTypeEntity> print message|messages
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>)
# [labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>] # [labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>]
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]] # [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
# [showlabels] [showbody] [showdate] [showsize] [showsnippet] # [showlabels] [showbody] [showdate] [showsize] [showsnippet]
@ -66192,7 +66218,7 @@ def printShowMessagesThreads(users, entityType):
# [convertcrnl] [delimiter <Character>] [todrive <ToDriveAttribute>*] # [convertcrnl] [delimiter <Character>] [todrive <ToDriveAttribute>*]
# [countsonly|positivecountsonly] [useronly] # [countsonly|positivecountsonly] [useronly]
# gam <UserTypeEntity> show message|messages # gam <UserTypeEntity> show message|messages
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>)
# [labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>] # [labelmatchpattern <RegularExpression>] [sendermatchpattern <RegularExpression>]
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]] # [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
# [showlabels] [showbody] [showdate] [showsize] [showsnippet] # [showlabels] [showbody] [showdate] [showsize] [showsnippet]
@ -66203,7 +66229,7 @@ def printShowMessages(users):
printShowMessagesThreads(users, Ent.MESSAGE) printShowMessagesThreads(users, Ent.MESSAGE)
# gam <UserTypeEntity> print thread|threads # gam <UserTypeEntity> print thread|threads
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <ThreadIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <ThreadIDEntity>)
# [labelmatchpattern <RegularExpression>] # [labelmatchpattern <RegularExpression>]
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]] # [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
# [showlabels] [showbody] [showdate] [showsize] [showsnippet] # [showlabels] [showbody] [showdate] [showsize] [showsnippet]
@ -66211,7 +66237,7 @@ def printShowMessages(users):
# [convertcrnl] [delimiter <Character>] [todrive <ToDriveAttribute>*] # [convertcrnl] [delimiter <Character>] [todrive <ToDriveAttribute>*]
# [countsonly|positivecountsonly] [useronly] # [countsonly|positivecountsonly] [useronly]
# gam <UserTypeEntity> show thread|threads # gam <UserTypeEntity> show thread|threads
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <ThreadIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <ThreadIDEntity>)
# [labelmatchpattern <RegularExpression>] # [labelmatchpattern <RegularExpression>]
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]] # [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
# [showlabels] [showbody] [showdate] [showsize] [showsnippet] # [showlabels] [showbody] [showdate] [showsize] [showsnippet]
@ -67169,10 +67195,10 @@ EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP = {
} }
# gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity> # gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity>
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>)
# [subject <String>] # [subject <String>]
# gam <UserTypeEntity> forward thread|threads recipient|to <RecipientEntity> # gam <UserTypeEntity> forward thread|threads recipient|to <RecipientEntity>
# (((query <QueryGmail>) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>) # (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>)
# [subject <String>] # [subject <String>]
# gam <UserTypeEntity> forward <FalseValues> # gam <UserTypeEntity> forward <FalseValues>
# gam <UserTypeEntity> forward <TrueValues> keep|leaveininbox|archive|delete|trash|markread <EmailAddress> # gam <UserTypeEntity> forward <TrueValues> keep|leaveininbox|archive|delete|trash|markread <EmailAddress>

View File

@ -438,6 +438,7 @@ STRING_LENGTH = 'string length'
SUBKEY_FIELD_MISMATCH = 'subkeyfield {0} does not match saved subkeyfield {1}' SUBKEY_FIELD_MISMATCH = 'subkeyfield {0} does not match saved subkeyfield {1}'
SUBSCRIPTION_NOT_FOUND = 'Could not find subscription' SUBSCRIPTION_NOT_FOUND = 'Could not find subscription'
SUFFIX_NOT_ALLOWED_WITH_CUSTOMLANGUAGE = 'Suffix {0} not allowed with customLanguage {1}' SUFFIX_NOT_ALLOWED_WITH_CUSTOMLANGUAGE = 'Suffix {0} not allowed with customLanguage {1}'
TASKLIST_TITLE_NOT_FOUND = 'Task list title not found'
THREAD = 'thread' THREAD = 'thread'
THREADS = 'threads' THREADS = 'threads'
TO = 'To' TO = 'To'