Compare commits

...

7 Commits

Author SHA1 Message Date
Jay Lee
0bc44582af BCE > CEP to match admin console product name change 2024-04-17 14:13:51 -04:00
Ross Scroggs
baf0c7863f Upgraded to Python 3.12.3 where possible. 2024-04-16 19:52:03 -07:00
Ross Scroggs
b00077151b Added the following option to <EventMatchProperty>
```
matchfield attendeesonlydomainlist <DomainNameList>
```
2024-04-16 18:34:13 -07:00
Ross Scroggs
842e46d060 Added the following options to <EventMatchProperty>
```
matchfield attendeesdomainlist <DomainNameList>
matchfield attendeesnotdomainlist <DomainNameList>
```
2024-04-16 15:32:25 -07:00
Ross Scroggs
bad4866bf7 Added option oneitemperrow to 'gam print vaultholds` 2024-04-15 15:59:57 -07:00
Ross Scroggs
3f5d96e13b Added timeofdayrange=<HH:MM>/<HH:MM> and timeofdayrange!=<HH:MM>/<HH:MM> to <RowValueFilter> 2024-04-12 11:55:50 -07:00
Ross Scroggs
a0dc04e7b0 Updated countsonly option of gam <UserTypeEntity> print|show notes to additionally display the total number of notes. 2024-04-04 17:57:55 -07:00
17 changed files with 352 additions and 99 deletions

View File

@@ -118,7 +118,7 @@ jobs:
with:
path: |
cache.tar.xz
key: gam-${{ matrix.jid }}-20240210
key: gam-${{ matrix.jid }}-20240416
- name: Untar Cache archive
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'

View File

@@ -3,6 +3,10 @@
- [Definitions](#definitions)
- [Quoting rules](#quoting-rules)
- [Column row filtering](#column-row-filtering)
- [Field names](#field-names)
- [Inclusive filters](#inclusive-filters)
- [Exclusive filters](#exclusive-filters)
- [Matches](#matches)
- [Column row limiting](#column-row-limiting)
- [Saving filters in gam.cfg](#saving-filters-in-gamcfg)
- [Validate filters](#validate-filters)
@@ -39,28 +43,30 @@ These filters can be used alone or in conjunction with the `matchfield|skipfield
<FieldNameFilter> :: = <RegularExpression>
<RowValueFilter> ::=
[(any|all):]count<Operator><Number>|
[(any|all):]countrange=<Number>/<Number>|
[(any|all):]countrange!=<Number>/<Number>|
[(any|all):]date<Operator><Date>|
[(any|all):]daterange=<Date>/<Date>|
[(any|all):]daterange!=<Date>/<Date>|
[(any|all):]length<Operator><Number>|
[(any|all):]lengthrange=<Number>/<Number>|
[(any|all):]lengthrange!=<Number>/<Number>|
[(any|all):]text<Operator><String>|
[(any|all):]textrange=<String>/<String>|
[(any|all):]textrange!=<String>/<String>|
[(any|all):]time<Operator><Time>|
[(any|all):]timerange=<Time>/<Time>|
[(any|all):]timerange!=<Time>/<Time>|
[(any|all):]boolean:<Boolean>|
[(any|all):]regex:<RegularExpression>|
[(any|all):]regexcs:<RegularExpression>|
[(any|all):]count<Operator><Number>|
[(any|all):]countrange!=<Number>/<Number>|
[(any|all):]countrange=<Number>/<Number>|
[(any|all):]data:<DataSelector>|
[(any|all):]date<Operator><Date>|
[(any|all):]daterange!=<Date>/<Date>|
[(any|all):]daterange=<Date>/<Date>|
[(any|all):]length<Operator><Number>|
[(any|all):]lengthrange!=<Number>/<Number>|
[(any|all):]lengthrange=<Number>/<Number>|
[(any|all):]notdata:<DataSelector>|
[(any|all):]notregex:<RegularExpression>|
[(any|all):]notregexcs:<RegularExpression>|
[(any|all):]data:<DataSelector>|
[(any|all):]notdata:<DataSelector>|
[(any|all):]regex:<RegularExpression>|
[(any|all):]regexcs:<RegularExpression>|
[(any|all):]text<Operator><String>|
[(any|all):]textrange!=<String>/<String>|
[(any|all):]textrange=<String>/<String>|
[(any|all):]time<Operator><Time>|
[(any|all):]timeofdayrange!=<Hour>:<Minute>/<Hour>:<Minute>|
[(any|all):]timeofdayrange=<Hour>:<Minute>/<Hour>:<Minute>|
[(any|all):]timerange!=<Time>/<Time>|
[(any|all):]timerange=<Time>/<Time>|
<RowValueFilterList> ::=
"'<FieldNameFilter>:<RowValueFilter>'(,'<FieldNameFilter>:<RowValueFilter>')*"
<RowValueFilterJSONList> ::=
@@ -156,11 +162,13 @@ In the case of `notregex|notregexcs|notdata`, the filter matches if some (not al
If neither `any` or `all` is explicitly specified, `any` is the default.
These are the row value filter types:
* `boolean:<Boolean>` - Used on fields with Boolean values; a blank field is considered False
* `count<Operator><Number>` - Used on fields with numbers; a blank field will not match
* `countrange=<Number>/<Number>` - Used on fields with numbers; a blank field will not match
* The field value must be `>=` the left `<Number>` and `<=` the right `<Number>`
* `countrange!=<Number>/<Number>` - Used on fields with numbers; a blank field will not match
* The field value must be `<` the left `<Number>` or `>` the right `<Number>`
* `data:<DataSelector>` - Used on fields with text; field value must match some value in `<DataSelector>`; case sensitive
* `date<Operator><Date>` - Used on fields with dates or times; only the date portion of a time field is compared; a blank field will not match
* `daterange=<Date>/<Date>` - Used on fields with dates or times; only the date portion of a time field is compared; a blank field will not match
* The field value must be `>=` the left `<Date>` and `<=` the right `<Date>`
@@ -171,23 +179,25 @@ These are the row value filter types:
* The field length must be `>=` the left `<Number>` and `<=` the right `<Number>`
* `lengthrange!=<Number>/<Number>` - Used on fields with strings; non string fields will not match
* The field length must be `<` the left `<Number>` or `>` the right `<Number>`
* `notdata:<DataSelector>` - Used on fields with text; field value must not match any value in `<DataSelector>`; case sensitive
* `notregex:<RegularExpression>` - Used on fields with text; field value must not match `<RegularExpression>`; case insensitive
* `notregexcs:<RegularExpression>` - Used on fields with text; field value must not match `<RegularExpression>`; case sensitive
* `regex:<RegularExpression>` - Used on fields with text; field value must match `<RegularExpression>`; case insensitive
* `regexcs:<RegularExpression>` - Used on fields with text; field value must match `<RegularExpression>`; case sensitive
* `text<Operator><String>` - Used on fields with text
* `textrange=<String>/<String>` - Used on fields with strings
* The field value must be `>=` the left `<String>` and `<=` the right `<String>`
* `textrange!=<String>/<String>` - Used on fields with strings
* The field value must be `<` the left `<String>` or `>` the right `<String>`
* `time<Operator><Time>` - Used on fields with times; a blank field will not match
* `timeofdayrange=<Hour>:<Minute>/<Hour>:<Minute>` - Used on fields with times; a blank field will not match
* The field value must be `>=` the left `<Hour>:<Minute>` and `<=` the right `<Hour>:<Minute>`
* `timeofdayrange!=<Hour>:<Minute>/<Hour>:<Minute>` - Used on fields with times; a blank field will not match
* The field value must be `<` the left `<Hour>:<Minute>` or `>` the right `<Hour>:<Minute>`
* `timerange=<Time>/<Time>` - Used on fields with times; a blank field will not match
* The field value must be `>=` the left `<Time>` and `<=` the right `<Time>`
* `timerange!=<Time>/<Time>` - Used on fields with times; a blank field will not match
* The field value must be `<` the left `<Time>` or `>` the right `<Time>`
* `boolean:<Boolean>` - Used on fields with Boolean values; a blank field is considered False
* `regex:<RegularExpression>` - Used on fields with text; field value must match `<RegularExpression>`; case insensitive
* `regexcs:<RegularExpression>` - Used on fields with text; field value must match `<RegularExpression>`; case sensitive
* `notregex:<RegularExpression>` - Used on fields with text; field value must not match `<RegularExpression>`; case insensitive
* `notregexcs:<RegularExpression>` - Used on fields with text; field value must not match `<RegularExpression>`; case sensitive
* `data:<DataSelector>` - Used on fields with text; field value must match some value in `<DataSelector>`; case sensitive
* `notdata:<DataSelector>` - Used on fields with text; field value must not match any value in `<DataSelector>`; case sensitive
### **Change in behavior.**
In versions prior to `5.12.00`, `regex:<RegularExpression>` and `notregex:<RegularExpression>` were processed in a case sensitive manner;

View File

@@ -4,6 +4,10 @@
- [Quoting rules](#quoting-rules)
- [Column header filtering](#column-header-filtering)
- [Column row filtering](#column-row-filtering)
- [Field names](#field-names)
- [Inclusive filters](#inclusive-filters)
- [Exclusive filters](#exclusive-filters)
- [Matches](#matches)
- [Column row limiting](#column-row-limiting)
- [Saving filters in gam.cfg](#saving-filters-in-gamcfg)
@@ -44,28 +48,30 @@ on all platforms.
<FieldNameFilter> :: = <RegularExpression>
<ColumnFieldNameFilterList> ::= "<FieldNameFilter>(,<FieldNameFilter>)*"
<RowValueFilter> ::=
[(any|all):]count<Operator><Number>|
[(any|all):]countrange=<Number>/<Number>|
[(any|all):]countrange!=<Number>/<Number>|
[(any|all):]date<Operator><Date>|
[(any|all):]textrange=<String>/<String>|
[(any|all):]textrange!=<String>/<String>|
[(any|all):]daterange=<Date>/<Date>|
[(any|all):]daterange!=<Date>/<Date>|
[(any|all):]length<Operator><Number>|
[(any|all):]lengthrange=<Number>/<Number>|
[(any|all):]lengthrange!=<Number>/<Number>|
[(any|all):]text<Operator><String>|
[(any|all):]time<Operator><Time>|
[(any|all):]timerange=<Time>/<Time>|
[(any|all):]timerange!=<Time>/<Time>|
[(any|all):]boolean:<Boolean>|
[(any|all):]regex:<RegularExpression>|
[(any|all):]regexcs:<RegularExpression>|
[(any|all):]count<Operator><Number>|
[(any|all):]countrange!=<Number>/<Number>|
[(any|all):]countrange=<Number>/<Number>|
[(any|all):]data:<DataSelector>|
[(any|all):]date<Operator><Date>|
[(any|all):]daterange!=<Date>/<Date>|
[(any|all):]daterange=<Date>/<Date>|
[(any|all):]length<Operator><Number>|
[(any|all):]lengthrange!=<Number>/<Number>|
[(any|all):]lengthrange=<Number>/<Number>|
[(any|all):]notdata:<DataSelector>
[(any|all):]notregex:<RegularExpression>|
[(any|all):]notregexcs:<RegularExpression>|
[(any|all):]data:<DataSelector>|
[(any|all):]notdata:<DataSelector>
[(any|all):]regex:<RegularExpression>|
[(any|all):]regexcs:<RegularExpression>|
[(any|all):]text<Operator><String>|
[(any|all):]textrange!=<String>/<String>|
[(any|all):]textrange=<String>/<String>|
[(any|all):]time<Operator><Time>|
[(any|all):]timeofdayrange!=<Hour>:<Minute>/<Hour>:<Minute>|
[(any|all):]timeofdayrange=<Hour>:<Minute>/<Hour>:<Minute>|
[(any|all):]timerange!=<Time>/<Time>|
[(any|all):]timerange=<Time>/<Time>|
<RowValueFilterList> ::=
"'<FieldNameFilter>:<RowValueFilter>'(,'<FieldNameFilter>:<RowValueFilter>')*"
<RowValueFilterJSONList> ::=
@@ -214,11 +220,13 @@ In the case of `notregex|notregexcs|notdata`, the filter matches if some (not al
If neither `any` or `all` is explicitly specified, `any` is the default.
These are the row value filter types:
* `boolean:<Boolean>` - Used on fields with Boolean values; a blank field is considered False
* `count<Operator><Number>` - Used on fields with numbers; a blank field will not match
* `countrange=<Number>/<Number>` - Used on fields with numbers; a blank field will not match
* The field value must be `>=` the left `<Number>` and `<=` the right `<Number>`
* `countrange!=<Number>/<Number>` - Used on fields with numbers; a blank field will not match
* The field value must be `<` the left `<Number>` or `>` the right `<Number>`
* `data:<DataSelector>` - Used on fields with text; field value must match some value in `<DataSelector>`; case sensitive
* `date<Operator><Date>` - Used on fields with dates or times; only the date portion of a time field is compared; a blank field will not match
* `daterange=<Date>/<Date>` - Used on fields with dates or times; only the date portion of a time field is compared; a blank field will not match
* The field value must be `>=` the left `<Date>` and `<=` the right `<Date>`
@@ -229,23 +237,25 @@ These are the row value filter types:
* The field length must be `>=` the left `<Number>` and `<=` the right `<Number>`
* `lengthrange!=<Number>/<Number>` - Used on fields with strings; non string fields will not match
* The field length must be `<` the left `<Number>` or `>` the right `<Number>`
* `notdata:<DataSelector>` - Used on fields with text; field value must not match any value in `<DataSelector>`; case sensitive
* `notregex:<RegularExpression>` - Used on fields with text; field value must not match `<RegularExpression>`; case insensitive
* `notregexcs:<RegularExpression>` - Used on fields with text; field value must not match `<RegularExpression>`; case sensitive
* `regex:<RegularExpression>` - Used on fields with text; field value must match `<RegularExpression>`; case insensitive
* `regexcs:<RegularExpression>` - Used on fields with text; field value must match `<RegularExpression>`; case sensitive
* `text<Operator><String>` - Used on fields with text
* `textrange=<String>/<String>` - Used on fields with strings
* The field value must be `>=` the left `<String>` and `<=` the right `<String>`
* `textrange!=<String>/<String>` - Used on fields with strings
* The field value must be `<` the left `<String>` or `>` the right `<String>`
* `time<Operator><Time>` - Used on fields with times; a blank field will not match
* `timeofdayrange=<Hour>:<Minute>/<Hour>:<Minute>` - Used on fields with times; a blank field will not match
* The field value must be `>=` the left `<Hour>:<Minute>` and `<=` the right `<Hour>:<Minute>`
* `timeofdayrange!=<Hour>:<Minute>/<Hour>:<Minute>` - Used on fields with times; a blank field will not match
* The field value must be `<` the left `<Hour>:<Minute>` or `>` the right `<Hour>:<Minute>`
* `timerange=<Time>/<Time>` - Used on fields with times; a blank field will not match
* The field value must be `>=` the left `<Time>` and `<=` the right `<Time>`
* `timerange!=<Time>/<Time>` - Used on fields with times; a blank field will not match
* The field value must be `<` the left `<Time>` or `>` the right `<Time>`
* `boolean:<Boolean>` - Used on fields with Boolean values; a blank field is considered False
* `regex:<RegularExpression>` - Used on fields with text; field value must match `<RegularExpression>`; case insensitive
* `regexcs:<RegularExpression>` - Used on fields with text; field value must match `<RegularExpression>`; case sensitive
* `notregex:<RegularExpression>` - Used on fields with text; field value must not match `<RegularExpression>`; case insensitive
* `notregexcs:<RegularExpression>` - Used on fields with text; field value must not match `<RegularExpression>`; case sensitive
* `data:<DataSelector>` - Used on fields with text; field value must match some value in `<DataSelector>`; case sensitive
* `notdata:<DataSelector>` - Used on fields with text; field value must not match any value in `<DataSelector>`; case sensitive
### **Change in behavior.**
In versions prior to `5.12.00`, `regex:<RegularExpression>` and `notregex:<RegularExpression>` were processed in a case sensitive manner;

View File

@@ -196,6 +196,9 @@ Client access works when accessing Resource calendars.
<EventMatchProperty> ::=
(matchfield attendees <EmailAddressEntity>)|
(matchfield attendeesonlydomainlist <DomainNameList>)|
(matchfield attendeesdomainlist <DomainNameList>)|
(matchfield attendeesnotdomainlist <DomainNameList>)|
(matchfield attendeespattern <RegularExpression>)|
(matchfield attendeesstatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddressEntity>)|
(matchfield creatoremail <RegularExpression>)|
@@ -216,7 +219,6 @@ Client access works when accessing Resource calendars.
(event|events <EventIdList> |
<FileSelector> | <CSVFileSelector> | <CSVkmdSelector> | <CSVSubkeySelector> | <CSVDataSelector>)
See: https://github.com/taers232c/GAMADV-XTD3/wiki/Collections-of-Items
<EventSelectEntity> ::=
(<EventSelectProperty>+ <EventMatchProperty>*)
@@ -229,6 +231,7 @@ Client access works when accessing Resource calendars.
lavender|peacock|sage|tangerine|tomato
<PropertyKey> ::= <String>
<PropertyValue> ::= <String>
<TimeZone> ::= <String>
<EventAttribute> ::=
(allday <Date>)|
@@ -356,7 +359,13 @@ The Google Calendar API processes `<EventSelectProperty>*`; you may specify none
GAM processes `<EventMatchProperty>*`; you may specify none or multiple properties.
* `matchfield attendees <EmailAddressEntity>` - All of the attendees in `<EmailAddressEntity>` must be present
* `matchfield attendeespattern <RegularExpression>` - Some attendee must match `<RegularExpression>`
* `matchfield attendeesonlydomainlist <DomainNameList>` - All attendee's email addresses must be in a domain in `<DomainNameList>`
* For example, this lets you look for events with all attendees in your internal domains
* `matchfield attendeesdomainlist <DomainNameList>` - Some attendee's email address must be in a domain in `<DomainNameList>`
* For example, this lets you look for events with attendees in specific external domains
* `matchfield attendeesnotdomainlist <DomainNameList>` - Some attendee's email address must be in a domain not in `<DomainNameList>`
* For example, this lets you look for events with attendees not in your internal domains
* `matchfield attendeespattern <RegularExpression>` - Some attendee's email address must match `<RegularExpression>`
* `matchfield attendeesstatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddressEntity>` - All of the attendees in `<EmailAddressEntity>` must be present
and must have the specified values.
* `<AttendeeAttendance>` - Default is `required`

View File

@@ -10,10 +10,52 @@ 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
### 6.72.14
Upgraded to Python 3.12.3 where possible.
### 6.72.13
Added the following option to `<EventMatchProperty>` that can be used to select
events based on the domains of the attendees.
```
matchfield attendeesonlydomainlist <DomainNameList>
```
This returns true if all attendee's email addresses are in a domain in `<DomainNameList>`;
for example this lets you look for events with attendees only in your internal domains.
### 6.72.12
Added the following options to `<EventMatchProperty>` that can be used to select
events based on the domains of the attendees.
```
matchfield attendeesdomainlist <DomainNameList>
matchfield attendeesnotdomainlist <DomainNameList>
```
The first returns true if any attendee's email address is in a domain in `<DomainNameList>`;
for example this lets you look for events with attendees in specific external domains.
The second returns true if any attendee's email address is in a domain other than those in `<DomainNameList>`;
for example this lets you look for events with attendees not in your internal domains.
### 6.72.11
Added option `oneitemperrow` to 'gam print vaultholds` to have each of a
hold's accounts displayed on a separate row with all of the other hold fields.
### 6.72.10
Added `timeofdayrange=<HH:MM>/<HH:MM>` and `timeofdayrange!=<HH:MM>/<HH:MM>` to `<RowValueFilter>` that allows
CSV row filtering based on time-of-day.
### 6.72.09
Updated `countsonly` option of `gam <UserTypeEntity> print|show notes` to additionally display the total number of notes.
### 6.72.08
Added option `countsonly` to `gam <UserTypeEntity> print|show notes` that displays
the number of notes a user owns and the number of notes of user can edit.
the number of notes a user owns and the number of notes a user can edit.
### 6.72.07

View File

@@ -334,10 +334,10 @@ 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$ ./gam version
WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found
GAMADV-XTD3 6.72.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.72.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64
Python 3.12.3 64-bit final
MacOS Sonoma 14.4.1 x86_64
Path: /Users/admin/bin/gamadv-xtd3
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
@@ -1006,9 +1006,9 @@ writes the credentials into the file oauth2.txt.
C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt
C:\GAMADV-XTD3>gam version
WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found
GAMADV-XTD3 6.72.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.72.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
Python 3.12.3 64-bit final
Windows-10-10.0.17134 AMD64
Path: C:\GAMADV-XTD3
Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com

View File

@@ -261,6 +261,9 @@
<EventMatchProperty> ::=
(matchfield attendees <EmailAddressEntity>)|
(matchfield attendeesonlydomainlist <DomainNameList>)|
(matchfield attendeesdomainlist <DomainNameList>)|
(matchfield attendeesnotdomainlist <DomainNameList>)|
(matchfield attendeespattern <RegularExpression>)|
(matchfield attendeesstatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddressEntity>)|
(matchfield creatoremail <RegularExpression>)|
@@ -438,7 +441,13 @@ The Google Calendar API processes `<EventSelectProperty>*`; you may specify none
GAM processes `<EventMatchProperty>*`; you may specify none or multiple properties.
* `matchfield attendees <EmailAddressEntity>` - All of the attendees in `<EmailAddressEntity>` must be present
* `matchfield attendeespattern <RegularExpression>` - Some attendee must match `<RegularExpression>`
* `matchfield attendeesonlydomainlist <DomainNameList>` - All attendee's email addresses must be in a domain in `<DomainNameList>`
* For example, this lets you look for events with all attendees in your internal domains
* `matchfield attendeesdomainlist <DomainNameList>` - Some attendee's email address must be in a domain in `<DomainNameList>`
* For example, this lets you look for events with attendees in specific external domains
* `matchfield attendeesnotdomainlist <DomainNameList>` - Some attendee's email address must be in a domain not in `<DomainNameList>`
* For example, this lets you look for events with attendees not in your internal domains
* `matchfield attendeespattern <RegularExpression>` - Some attendee's email address must match `<RegularExpression>`
* `matchfield attendeesstatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddressEntity>` - All of the attendees in `<EmailAddressEntity>` must be present
and must have the specified values.
* `<AttendeeAttendance>` - Default is `required`

View File

@@ -9,6 +9,7 @@
- [Manage vacation](#manage-vacation)
- [Display vacation](#display-vacation)
- [User attribute `replace <Tag> <UserReplacement>` processing](Tag-Replace)
- [Standardize user signatures](#standardize-user-signatures)
## API documentation
* https://developers.google.com/gmail/api/reference/rest/v1/users.settings.sendAs
@@ -245,3 +246,37 @@ Gam displays the information in CSV form.
* `compact` - Strip carriage returns and newlines in original HTML; this makes these values easier to process in the CSV file
and can be used as input to GAM.
* `enabledonly` - Do not display users with vacation autoreply disabled.
## Standardize user signatures
You can standardize user signatures by creating a signature template and a CSV file with data for each user.
You can create a signature template by defining the signature in the Gmail Settings GUI of a test user.
You must use the default signature `My signature`.
Use text like `{FirstName}` and `{Email}` in the locations where the actual values will go.
Once you're created the template signature, do the following:
```
$ gam user testuser@domain.com show signature compact > SimpleSig.html
$ more SimpleSig.html
SendAs Address: <testuser@domain.com>
IsPrimary: True
Default: True
Signature: <div dir="ltr">--<div>Name: {FirstName} {LastName}<div>Phone: {Phone}</div><div>Email: {Email}</div></div><div><br></div><div>Company Name</div><div>Company Address</div><div><br></div></div>\n
```
Edit SimpleSig.html and delete all text from `SendAs ` through `Signature: `.
The result should be:
```
<div dir="ltr">--<div>Name: {FirstName} {LastName}<div>Phone: {Phone}</div><div>Email: {Email}</div></div><div><br></div><div>Company Name</div><div>Company Address</div><div><br></div></div>\n
```
This is a sample Users.csv file.
```
email,first,last,phone
bsmith@domain.com,Bob,Smith,510-555-1212 x 123
mjones@domain.com,Mary,Jones,510-555-1212 x 456
```
This command will update the user's signatures.
```
gam csv Users.csv gam user "~email" signature htmlfile SimpleSig.html replace FirstName "~first" replace LastName "~last" replace Phone "~phone" replace Email "~email"
```

View File

@@ -139,8 +139,8 @@ By default, GAM displays all non-trashed notes:
* `filter trashed` - Display notes in the trash
* `role owner|writer` - Display notes where the user has the specified role
When option `countsonly` is specified, the number of notes a user owns and the number of notes of user can edit
if displayed.
When option `countsonly` is specified, the number of notes a user owns, the number of notes of user can edit
and the total number of notes is displayed.
By default, Gam displays the information as an indented list of keys and values; the note text is displayed as individual lines.
* `compact` - Display the note text with escaped carriage returns as \r and newlines as \n
@@ -158,8 +158,8 @@ By default, GAM displays all non-trashed notes:
* `filter trashed` - Display notes in the trash
* `role owner|writer` - Display notes where the user has the specified role
When option `countsonly` is specified, the number of notes a user owns and the number of notes of user can edit
if displayed.
When option `countsonly` is specified, the number of notes a user owns, the number of notes of user can edit
and the total number of notes is displayed.
By default, when writing CSV files, Gam uses a quote character of double quote `"`. The quote character is used to enclose columns that contain
the quote character itself, the column delimiter (comma by default) and new-line characters. Any quote characters within the column are doubled.

View File

@@ -657,7 +657,11 @@ By default, Gam displays the information as an indented list of keys and values.
gam print vaultholds|holds [todrive <ToDriveAttributes>*] [matters <MatterItemList>]
[fields <VaultHoldFieldNameList>] [shownames]
[formatjson [quotechar <Character>]]
[oneitemperrow]
```
By default, all accounts for a hold are displayed on a single row;
use `oneitemperrow` to have each account displayed on a separate row.
By default, Gam displays the information as columns of fields; the following option causes the output to be in JSON format,
* `formatjson` - Display the fields in JSON format.

View File

@@ -3,10 +3,10 @@
Print the current version of Gam with details
```
gam version
GAMADV-XTD3 6.72.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.72.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64
Python 3.12.3 64-bit final
MacOS Sonoma 14.4.1 x86_64
Path: /Users/Admin/bin/gamadv-xtd3
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
Time: 2023-06-02T21:10:00-07:00
@@ -15,10 +15,10 @@ Time: 2023-06-02T21:10:00-07:00
Print the current version of Gam with details and time offset information
```
gam version timeoffset
GAMADV-XTD3 6.72.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.72.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64
Python 3.12.3 64-bit final
MacOS Sonoma 14.4.1 x86_64
Path: /Users/Admin/bin/gamadv-xtd3
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
Your system time differs from www.googleapis.com by less than 1 second
@@ -27,17 +27,17 @@ 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
```
gam version extended
GAMADV-XTD3 6.72.08 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.72.14 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64
Python 3.12.3 64-bit final
MacOS Sonoma 14.4.1 x86_64
Path: /Users/Admin/bin/gamadv-xtd3
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
Time: 2023-06-02T21:10:00-07:00
Your system time differs from admin.googleapis.com by less than 1 second
OpenSSL 3.1.1 30 May 2023
cryptography 41.0.1
filelock 3.12.2
filelock 3.12.3
google-api-python-client 2.88.0
google-auth-httplib2 0.1.0
google-auth-oauthlib 1.0.0
@@ -64,7 +64,7 @@ MacOS High Sierra 10.13.6 x86_64
Path: /Users/Admin/bin/gamadv-xtd3
Version Check:
Current: 5.35.08
Latest: 6.72.08
Latest: 6.72.14
echo $?
1
```
@@ -72,7 +72,7 @@ echo $?
Print the current version number without details
```
gam version simple
6.72.08
6.72.14
```
In Linux/MacOS you can do:
```
@@ -82,10 +82,10 @@ echo $VER
Print the current version of Gam and address of this Wiki
```
gam help
GAM 6.72.08 - https://github.com/taers232c/GAMADV-XTD3
GAM 6.72.14 - https://github.com/taers232c/GAMADV-XTD3
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.2 64-bit final
MacOS Sonoma 14.2.1 x86_64
Python 3.12.3 64-bit final
MacOS Sonoma 14.4.1 x86_64
Path: /Users/Admin/bin/gamadv-xtd3
Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, customer_id: my_customer, domain: domain.com
Time: 2023-06-02T21:10:00-07:00

View File

@@ -123,7 +123,9 @@ clock_skew_in_seconds
Default: 10
Range: 10 - 3600
cmdlog
Path to GAM Log file; there is no logging if cmdlog is empty
Path to GAM Log file; there is no logging if cmdlog is empty.
If cmdlog specifies a relative path, e.g., just a filename, it is appended to GamConfigDir.
If cmdlog specifies a full path, it is used as is.
Default: ''
cmdlog_max_backups
Maximum number of backup log files

View File

@@ -1597,6 +1597,9 @@ gam calendar <CalendarEntity> printacl [todrive <ToDriveAttribute>*]
<EventMatchProperty> ::=
(matchfield attendees <EmailAddressEntity>)|
(matchfield attendeesonlydomainlist <DomainNameList>)|
(matchfield attendeesdomainlist <DomainNameList>)|
(matchfield attendeesnotdomainlist <DomainNameList>)|
(matchfield attendeespattern <RegularExpression>)|
(matchfield attendeesstatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddressEntity>)|
(matchfield creatoremail <RegularExpression>)|
@@ -5137,6 +5140,7 @@ gam info vaulthold|hold <MatterItem> <HoldItem>
gam print vaultholds|holds [todrive <ToDriveAttribute>*] [matters <MatterItemList>]
[fields <VaultHoldFieldNameList>] [shownames]
[formatjson [quotechar <Character>]]
[oneitemperrow]
gam show vaultholds|holds [matters <MatterItemList>]
[fields <VaultHoldFieldNameList>] [shownames]
[formatjson]

View File

@@ -2,10 +2,52 @@
Merged GAM-Team version
6.72.14
Upgraded to Python 3.12.3 where possible.
6.72.13
Added the following option to `<EventMatchProperty>` that can be used to select
events based on the domains of the attendees.
```
matchfield attendeesonlydomainlist <DomainNameList>
```
This returns true if all attendee's email addresses are in a domain in `<DomainNameList>`;
for example this lets you look for events with attendees only in your internal domains.
6.72.12
Added the following options to `<EventMatchProperty>` that can be used to select
events based on the domains of the attendees.
```
matchfield attendeesdomainlist <DomainNameList>
matchfield attendeesnotdomainlist <DomainNameList>
```
The first returns true if any attendee's email address is in a domain in `<DomainNameList>`;
for example this lets you look for events with attendees in specific external domains.
The second returns true if any attendee's email address is in a domain other than those in `<DomainNameList>`;
for example this lets you look for events with attendees not in your internal domains.
6.72.11
Added option `oneitemperrow` to 'gam print vaultholds` to have each of a
hold's accounts displayed on a separate row with all of the other hold fields.
6.72.10
Added `timeofdayrange=<HH:MM>/<HH:MM>` and `timeofdayrange!=<HH:MM>/<HH:MM>` to `<RowValueFilter>` that allows
CSV row filtering based on time-of-day.
6.72.09
Updated `countsonly` option of `gam <UserTypeEntity> print|show notes` to additionally display the total number of notes.
6.72.08
Added option `countsonly` to `gam <UserTypeEntity> print|show notes` that displays
the number of notes a user owns and the number of notes of user can edit.
the number of notes a user owns and the number of notes a user can edit.
6.72.07

View File

@@ -3420,6 +3420,7 @@ def SetGlobalVariables():
ROW_FILTER_ANY_ALL_PATTERN = re.compile(r'^(any:|all:)(.+)$', re.IGNORECASE)
ROW_FILTER_COMP_PATTERN = re.compile(r'^(date|time|count|length)\s*([<>]=?|=|!=)(.+)$', re.IGNORECASE)
ROW_FILTER_RANGE_PATTERN = re.compile(r'^(daterange|timerange|countrange|lengthrange)(=|!=)(\S+)/(\S+)$', re.IGNORECASE)
ROW_FILTER_TIMEOFDAYRANGE_PATTERN = re.compile(r'^(timeofdayrange)(=|!=)(\d\d):(\d\d)/(\d\d):(\d\d)$', re.IGNORECASE)
ROW_FILTER_BOOL_PATTERN = re.compile(r'^(boolean):(.+)$', re.IGNORECASE)
ROW_FILTER_TEXT_PATTERN = re.compile(r'^(text)([<>]=?|=|!=)(.*)$', re.IGNORECASE)
ROW_FILTER_TEXTRANGE_PATTERN = re.compile(r'^(textrange)(=|!=)(.*)/(.*)$', re.IGNORECASE)
@@ -3520,6 +3521,19 @@ def SetGlobalVariables():
else:
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <Number>/<Number>')
continue
mg = ROW_FILTER_TIMEOFDAYRANGE_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
startHour = int(mg.group(3))
startMinute = int(mg.group(4))
endHour = int(mg.group(5))
endMinute = int(mg.group(6))
if startHour > 23 or startMinute > 59 or endHour > 23 or endMinute > 59 or \
endHour < startHour or (endHour == startHour and endMinute < startMinute):
Cmd.Backup()
usageErrorExit(Msg.INVALID_TIMEOFDAY_RANGE.format(f'{startHour:02d}:{startMinute:02d}', f'{endHour:02d}:{endMinute:02d}'))
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), f'{startHour:02d}:{startMinute:02d}', f'{endHour:02d}:{endMinute:02d}'))
continue
mg = ROW_FILTER_BOOL_PATTERN.match(filterStr)
if mg:
filterType = mg.group(1).lower()
@@ -3558,7 +3572,7 @@ def SetGlobalVariables():
rowFilters.append((columnPat, anyMatch, filterType, getEntitiesFromCSVFile(False, returnSet=True)))
Cmd.RestoreArguments()
continue
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: date|time|count|text<Operator><Value> or boolean:<Boolean> or regex:<RegularExpression> or data:<DataSelector>')
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <RowValueFilter>')
return rowFilters
def _getCfgSection(sectionName, itemName):
@@ -7307,6 +7321,36 @@ def RowFilterMatch(row, titlesList, rowFilter, rowFilterModeAll, rowDropFilter,
return False
return True
def getHourMinuteFromDateTime(rowDate):
if YYYYMMDD_PATTERN.match(rowDate):
return None
try:
rowTime, _ = iso8601.parse_date(rowDate)
except (iso8601.ParseError, OverflowError):
return None
return f'{rowTime.hour:02d}:{rowTime.minute:02d}'
def rowTimeOfDayRangeFilterMatch(op, startHourMinute, endHourMinute):
def checkMatch(rowDate):
if not rowDate or not isinstance(rowDate, str) or rowDate == GC.Values[GC.NEVER_TIME]:
return False
rowHourMinute = getHourMinuteFromDateTime(rowDate)
if not rowHourMinute:
return False
if op == '!=':
return not startHourMinute <= rowHourMinute <= endHourMinute
return startHourMinute <= rowHourMinute <= endHourMinute
if anyMatch:
for column in columns:
if checkMatch(row.get(column, '')):
return True
return False
for column in columns:
if not checkMatch(row.get(column, '')):
return False
return True
def rowCountFilterMatch(op, filterCount):
def checkMatch(rowCount):
if isinstance(rowCount, str):
@@ -7509,6 +7553,9 @@ def RowFilterMatch(row, titlesList, rowFilter, rowFilterModeAll, rowDropFilter,
elif filterVal[2] in {'daterange', 'timerange'}:
if rowDateTimeRangeFilterMatch(filterVal[2] == 'date', filterVal[3], filterVal[4], filterVal[5]):
return True
elif filterVal[2] == 'timeofdayrange':
if rowTimeOfDayRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
return True
elif filterVal[2] == 'count':
if rowCountFilterMatch(filterVal[3], filterVal[4]):
return True
@@ -36117,6 +36164,9 @@ LIST_EVENTS_SELECT_PROPERTIES = {
LIST_EVENTS_MATCH_FIELDS = {
'attendees': ['attendees', 'email'],
'attendeesonlydomainlist': ['attendees', 'onlydomainlist'],
'attendeesdomainlist': ['attendees', 'domainlist'],
'attendeesnotdomainlist': ['attendees', 'notdomainlist'],
'attendeespattern': ['attendees', 'match'],
'attendeesstatus': ['attendees', 'status'],
'description': ['description'],
@@ -36188,6 +36238,8 @@ def getCalendarEventEntity():
calendarEventEntity['matches'].append((matchField, getBoolean()))
elif matchField[0] != 'attendees' or matchField[1] == 'match':
calendarEventEntity['matches'].append((matchField, getREPattern(re.IGNORECASE)))
elif matchField[0] == 'attendees' and matchField[1] in {'onlydomainlist', 'domainlist', 'notdomainlist'}:
calendarEventEntity['matches'].append((matchField, set(getString(Cmd.OB_DOMAIN_NAME_LIST).replace(',', ' ').split())))
elif matchField[1] == 'email':
calendarEventEntity['matches'].append((matchField, getNormalizedEmailAddressEntity()))
else: #status
@@ -36462,6 +36514,24 @@ def _eventMatches(event, match):
if match[1].search(attendee) is not None:
return True
return False
if match[0][1] == 'onlydomainlist':
for attendee in attendees:
_, domain = attendee.lower().split('@', 1)
if domain not in match[1]:
return False
return True
if match[0][1] == 'domainlist':
for attendee in attendees:
_, domain = attendee.lower().split('@', 1)
if domain in match[1]:
return True
return False
if match[0][1] == 'notdomainlist':
for attendee in attendees:
_, domain = attendee.lower().split('@', 1)
if domain not in match[1]:
return True
return False
# status
for matchEmail in match[3]:
for attendee in event['attendees']:
@@ -39269,16 +39339,27 @@ PRINT_VAULT_HOLDS_TITLES = ['matterId', 'matterName', 'holdId', 'name', 'updateT
# gam print vaultholds|holds [todrive <ToDriveAttribute>*] [matters <MatterItemList>]
# [fields <VaultHoldFieldNameList>] [shownames]
# [formatjson [quotechar <Character>]]
# [oneitemperrow]
# gam show vaultholds|holds [matters <MatterItemList>]
# [fields <VaultHoldFieldNameList>] [shownames]
# [formatjson]
def doPrintShowVaultHolds():
def _printVaultHold(hold):
row = flattenJSON(hold, flattened={'matterId': matterId, 'matterName': matterName}, timeObjects=VAULT_HOLD_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'matterId': matterId, 'matterName': matterName,
'holdId': hold['holdId'], 'name': hold['name'],
'JSON': json.dumps(cleanJSON(hold, timeObjects=VAULT_HOLD_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
v = buildGAPIObject(API.VAULT)
csvPF = CSVPrintFile(PRINT_VAULT_HOLDS_TITLES, 'sortall') if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar()
matters = []
cd = None
fieldsList = []
oneItemPerRow = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
@@ -39289,6 +39370,8 @@ def doPrintShowVaultHolds():
cd = buildGAPIObject(API.DIRECTORY)
elif getFieldsList(myarg, VAULT_HOLD_FIELDS_CHOICE_MAP, fieldsList, initialField=['holdId', 'name']):
pass
elif csvPF and myarg == 'oneitemperrow':
oneItemPerRow = True
else:
FJQC.GetFormatJSONQuoteChar(myarg, False)
fields = getItemFieldsFromFieldsList('holds', fieldsList)
@@ -39352,13 +39435,12 @@ def doPrintShowVaultHolds():
else:
for hold in holds:
_cleanVaultHold(hold, cd)
row = flattenJSON(hold, flattened={'matterId': matterId, 'matterName': matterName}, timeObjects=VAULT_HOLD_TIME_OBJECTS)
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
csvPF.WriteRowNoFilter({'matterId': matterId, 'matterName': matterName,
'holdId': hold['holdId'], 'name': hold['name'],
'JSON': json.dumps(cleanJSON(hold, timeObjects=VAULT_HOLD_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
if not oneItemPerRow or not hold.get('accounts', []):
_printVaultHold(hold)
else:
for account in hold.pop('accounts'):
hold['account'] = account
_printVaultHold(hold)
if csvPF:
csvPF.writeCSVfile('Vault Holds')
@@ -71349,7 +71431,7 @@ def printShowNotes(users):
csvPF.RemoveJSONTitles(['owner', 'ownedByMe'])
if countsOnly and csvPF:
if not FJQC.formatJSON:
csvPF.SetTitles(['User', 'noteOwner', 'noteWriter'])
csvPF.SetTitles(['User', 'noteOwner', 'noteWriter', 'totalNotes'])
else:
csvPF.SetJSONTitles(['User', 'JSON'])
fields = getItemFieldsFromFieldsList('notes', fieldsList, returnItemIfNoneList=False)
@@ -71370,27 +71452,30 @@ def printShowNotes(users):
throwReasons=GAPI.KEEP_THROW_REASONS,
filter=noteFilter, fields=fields)
if countsOnly:
noteCounts = {'User': user, 'noteOwner': 0, 'noteWriter': 0}
noteCounts = {'User': user, 'noteOwner': 0, 'noteWriter': 0, 'totalNotes': 0}
for note in notes:
noteCounts['totalNotes'] += 1
for permission in note['permissions']:
if permission.get('user', {}).get('email', '').lower() == user:
noteCounts[NOTES_COUNTS_MAP[permission['role']]] += 1
break
if not csvPF:
if not FJQC.formatJSON:
printEntityKVList([Ent.USER, user], ['noteOwner', noteCounts['noteOwner'], 'noteWriter', noteCounts['noteWriter']], i, count)
printEntityKVList([Ent.USER, user], ['noteOwner', noteCounts['noteOwner'],
'noteWriter', noteCounts['noteWriter'],
'totalNotes', noteCounts['totalNotes']], i, count)
else:
printLine(json.dumps(cleanJSON(noteCounts), ensure_ascii=False, sort_keys=True))
else:
row = {'User': user, 'noteOwner': noteCounts['noteOwner'], 'noteWriter': noteCounts['noteWriter']}
row = {'User': user, 'noteOwner': noteCounts['noteOwner'], 'noteWriter': noteCounts['noteWriter'],
'totalNotes': noteCounts['totalNotes']}
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'User': noteCounts.pop('User')}
row['JSON'] = json.dumps(cleanJSON(noteCounts), ensure_ascii=False, sort_keys=True)
csvPF.WriteRowNoFilter(row)
continue
if not csvPF:
elif not csvPF:
jcount = len(notes)
if not FJQC.formatJSON:
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.NOTE, i, count)

View File

@@ -293,6 +293,7 @@ INVALID_SCHEMA_VALUE = 'Invalid Schema Value'
INVALID_SCOPE = 'Invalid Scope'
INVALID_SITE = 'Invalid Site ({0}), must match pattern ({1})'
INVALID_TAG_SPECIFICATION = 'Invalid tag, expected field.subfield or field.subfield.subfield.string'
INVALID_TIMEOFDAY_RANGE = '{0} must be less than/equal to {1}'
IN_SKIPIDS = 'In skipids'
IN_THE = ' in the {0}'
IN_TRASH_AND_EXCLUDE_TRASHED = 'In Trash and excludeTrashed'

View File

@@ -32,7 +32,7 @@ _PRODUCTS = {
'101037': 'Google Workspace for Education',
'101038': 'AppSheet',
'101039': 'Assured Controls',
'101040': 'Beyond Corp Enterprise',
'101040': 'Chrome Enterprise',
'101043': 'Google Workspace Additional Storage',
'101047': 'Gemini',
'101049': 'Education Endpoint Management',
@@ -83,7 +83,7 @@ _SKUS = {
'1010390001': {
'product': '101039', 'aliases': ['assuredcontrols'], 'displayName': 'Assured Controls'},
'1010400001': {
'product': '101040', 'aliases': ['beyondcorp', 'beyondcorpenterprise', 'bce'], 'displayName': 'Beyond Corp Enterprise'},
'product': '101040', 'aliases': ['beyondcorp', 'beyondcorpenterprise', 'bce', 'cep', 'chromeenterprisepremium'], 'displayName': 'Chrome Enterprise Premium'},
'1010430001': {
'product': '101043', 'aliases': ['gwas', 'plusstorage'], 'displayName': 'Google Workspace Additional Storage'},
'1010470001': {