Compare commits

...

69 Commits
v4.88 ... v4.92

Author SHA1 Message Date
Jay Lee
2bc6c8bca0 200 or bust for URL shorten 2019-08-13 08:35:06 -04:00
Jay Lee
fc1e81a01d oauthbrowser.txt, GAM 4.92 2019-08-12 14:02:59 -04:00
Jay Lee
eebfaaf373 finish pyinstaller install 2019-08-12 12:48:01 -04:00
Jay Lee
652223d9bc Pyinstaller windows 64 bit bootloader compile 2019-08-12 12:29:51 -04:00
Jay Lee
e75664fd2e roll Python 3.5 back to Xenial 2019-08-12 12:08:09 -04:00
Jay Lee
556278b216 Use bionic for Python source tests 2019-08-12 11:57:24 -04:00
Jay Lee
f9bfaa98bb fix cros/mobile print 2019-08-12 11:47:44 -04:00
Jay Lee
b6bd2da6ce Short OAuth URLs, make console flow default to reduce issues 2019-08-12 11:00:26 -04:00
Jay Lee
7c36a6b601 Merge branch 'master' of https://github.com/jay0lee/GAM 2019-08-12 10:41:22 -04:00
Ross Scroggs
413924b11a Generalize expression to find group settings values (#994) 2019-08-12 10:41:17 -04:00
Jay Lee
251883dae5 add cros/mobile print tests 2019-08-10 15:46:52 -04:00
Jay Lee
7e4d0da8fb GAM 4.91 2019-08-10 15:39:14 -04:00
Ross Scroggs
3fc2aeed4d Fix group settings (#990)
* Fix group settings

Clean up md5MatchesFile

* Simplify doGetCustomerInfo

Avoid trap when doPrintDomains returned a domain creationTime with no fraction.
Traceback (most recent call last):
  File "gam.py", line 14761, in <module>
  File "gam.py", line 14159, in ProcessGAMCommand
  File "gam.py", line 2053, in doGetDomainInfo
  File "gam.py", line 2088, in doGetCustomerInfo
  File "_strptime.py", line 577, in _strptime_datetime
  File "_strptime.py", line 359, in _strptime
ValueError: time data '2019-04-23 16:14:56' does not match format '%Y-%m-%d %H:%
M:%S.%f'

lansa.co.uk,True,2019-04-23 16:14:13.041000,secondary,
lansa.com.au,True,2019-04-23 16:14:56,secondary
2019-08-10 15:37:44 -04:00
Ross Scroggs
7f4f785f0b Lower default limit for 500 to 200 for devices (#987)
* Lower default limit for 500 to 200 for devices

* Lower limit from 200 to 100 to handle mobile devices
2019-08-05 18:16:46 -04:00
Jay Lee
9f5920989d remove bionic for now, re-enable e2e tests 2019-08-01 11:32:23 -04:00
Jay Lee
62bceb30c5 GAM 4.90, update mobile adjustments 2019-08-01 11:15:10 -04:00
Jay Lee
8c736e52ac Empty body no longer required 2019-08-01 10:58:01 -04:00
Jay Lee
2669079a31 don't cleanup 2019-07-27 10:01:33 -04:00
Jay Lee
cbfd93e440 Timeout optimized Python build so we can cache partial testing... 2019-07-27 09:36:01 -04:00
Jay Lee
ffd1c297e5 force configure 2019-07-26 15:51:35 -04:00
Jay Lee
1a11cb04f9 clean and optimize python build 2019-07-26 15:38:23 -04:00
Jay Lee
438fd5b59a turn off unsafe to speed up Python for moment 2019-07-26 15:23:58 -04:00
Jay Lee
2fef5e2cfa minimal tests so Bionic Python 3.7 can build and cache 2019-07-26 14:22:35 -04:00
Jay Lee
db8ad38fd3 try curl instead of wget 2019-07-26 13:28:24 -04:00
Jay Lee
cbb2722291 figure out what's up with Python wget 2019-07-26 13:07:27 -04:00
Jay Lee
be1c7f2167 Bionic Beaver build 2019-07-26 12:37:00 -04:00
Jay Lee
61f4a137b0 32-bit bootloader, fix path 2019-07-26 12:34:40 -04:00
Jay Lee
dac9e91428 fix python path 2019-07-26 12:14:45 -04:00
Jay Lee
83c64f1f71 few more fixes 2019-07-26 11:53:12 -04:00
Jay Lee
443f4e707b Use MacOS Python binary, fix Win32 Pyinstaller build 2019-07-26 11:33:00 -04:00
Jay Lee
70bf2a05f3 pyinstaller version 2019-07-26 11:09:12 -04:00
Jay Lee
33a4747677 rebuild w32 PyInstaller bootloader to avoid AV issues 2019-07-26 10:43:44 -04:00
Ross Scroggs
a4cce17767 Fix typos (#981) 2019-07-26 09:27:19 -04:00
Ross Scroggs
67cd03d3f1 I suggest match_users instead of if_users (#980)
* I suggest match_users instead of if_users

* Allow if_users or match_users
2019-07-26 09:06:45 -04:00
Jay Lee
1f88c18f94 Update gam.py 2019-07-24 15:07:12 -04:00
Jay Lee
d2fc706b17 fix doit mobile update 2019-07-24 12:28:22 -04:00
Jay Lee
b5e5786813 Require doit argument to update >1 devices 2019-07-24 12:21:30 -04:00
Jay Lee
38e741c788 Update mobile devices by query 2019-07-24 11:16:54 -04:00
Jay Lee
e9a0b85682 retry notFound when changing group settings after create to handle sporadic latency issues that cause failure 2019-07-23 10:09:55 -04:00
Jay Lee
5ab3602c2a gam download storagebucket initial work 2019-07-20 10:08:48 -04:00
Jay Lee
dd4bf7b144 10 second timeout on update check (default seems to be 2 min) 2019-07-20 10:07:34 -04:00
Ross Scroggs
922326c5ce Code cleanup (#975) 2019-07-17 08:41:38 -04:00
Jay Lee
c69be414ca Update gam.py
Fix one more case.
2019-07-16 17:45:03 -04:00
Ross Scroggs
10bc47402c Cleanup (#974)
* Cleanup

parent is not valid with use project

* Cleanup

* Cleanup

* Code fix
2019-07-16 17:42:42 -04:00
Ross Scroggs
193e42cf22 Add new forms of create/use project (#973) 2019-07-16 12:50:36 -04:00
Jay Lee
e0f58e5264 Allow setting project parent 2019-07-15 13:24:54 -04:00
Jay Lee
f14e48320c noupx, strip osx/linux 2019-07-14 17:33:50 -04:00
Jay Lee
474fcd33a6 disable PyInstaller debug 2019-07-13 15:12:50 -04:00
Jay Lee
a2a9ffc895 force pip upgrades 2019-07-13 14:40:01 -04:00
Jay Lee
b02416b32c Actually use GAM_CA_FILE env var 2019-07-12 15:10:20 -04:00
Jay Lee
fa52d9e89e Merge branch 'master' of https://github.com/jay0lee/GAM 2019-07-11 09:55:41 -04:00
Jay Lee
6383aa594a Batch Drive Deletes 2019-07-11 09:55:20 -04:00
Ross Scroggs
54eb59c27b Convert team to shared in vault export; prepare for the future (#971)
* Convert team to shared in vault export

Restore _getValidCourseStates(croom), it is used by _getCourseStates in print courses/course-participants

* Update GamCommands.txt

* Fix typo

* Update gam.py
2019-07-10 15:35:06 -04:00
Ross Scroggs
3877f8309b Update GamCommands.txt (#969) 2019-07-10 12:17:53 -04:00
Jay Lee
d8b0681831 Merge branch 'master' of https://github.com/jay0lee/GAM 2019-07-10 12:17:13 -04:00
Jay Lee
cd8303dbea GAM 4.89 2019-07-10 12:17:00 -04:00
Ross Scroggs
ab51d6e931 Update GamCommands.txt (#968) 2019-07-10 11:33:49 -04:00
Jay Lee
b7e402dca2 confidential vault, generalize enums from discovery, query RCs 2019-07-10 09:33:21 -04:00
Jay Lee
842040a8b3 run e2e tests again 2019-07-09 21:39:27 -04:00
Jay Lee
1205da5d34 Update linux-x86_64-before-install.sh 2019-07-09 20:36:33 -04:00
Jay Lee
f40824fedd disable e2e so Python 3.7.4 compile finishes 2019-07-09 13:18:29 -04:00
Jay Lee
67fa0cbc61 } 2019-07-09 12:23:29 -04:00
Jay Lee
e029c77f76 Python 3.7.4, accept newer Python/OpenSSL versions 2019-07-09 11:09:09 -04:00
Jay Lee
2b23ae4e67 remove dnspython requirement, minor fixes 2019-07-02 12:21:51 -04:00
Jay Lee
c8ecc23c9c Remove dnspython in favor of simple Google DNS JSON API 2019-07-02 11:13:31 -04:00
Ross Scroggs
94f8959879 Handle group members with no status (#962)
* Handle group members with no status

* Omit Advanced  form

* Update gam.py
2019-07-01 12:02:08 -04:00
Jay Lee
2cdb8eb44d fix message header argument, make sure we remove all headers in cases of duplicate header or non-matching case 2019-06-27 14:39:03 -04:00
Ross Scroggs
ebc1d1ecb3 Clean up sendOrDropEmail (#961)
* Clean up sendOrDropEmail

* Date allowed in all commands, only sets kwargs for import/insert

* Two updates

Allow headers in draft/import/insert/send email
Quote arguments in todrive decscription

* Make requested changes

* Don't user sendser as an alias for from as they can be different things in SMTP

* On import message, default to not checking for spam
2019-06-27 09:55:00 -04:00
Jay Lee
d9e99334d2 new options and improvements to message send/drop/draft 2019-06-25 13:24:36 -04:00
14 changed files with 704 additions and 309 deletions

View File

@@ -2,10 +2,11 @@ if: tag IS blank
env:
global:
- BUILD_PYTHON_VERSION=3.7.3
- BUILD_PYTHON_VERSION=3.7.4
- BUILD_OPENSSL_VERSION=1.1.1c
- PATCHELF_VERSION=0.9
- MUSL_VERSION=1.1.22
- PYINSTALLER_VERSION=3.5
- secure: "FSKvLaiqhKz21SVgAQZI3bSX34Ffyev4l+R2G//QXNDu6UVQcuFsykzw+eZEG7fkhotXr8BMDL7xIkookiL8eLwUtcd/Z95HCjPBBHcmCSQleyvuuJBxdrQ9xldmiGLzMCYiumSH9OH4uJhQ39Yjnjsa8TK+PlTci6a/BTzlYyBSyDYDf7Iv/uhfQPDHL3pNwrQPHf4fL6/jcvo+uaPcv83AVZkNzZjjyoi9Aa+uh9xlbyHg11jp44463qqxoxTdYik3pYuXRBPjknjOGcnFHqn+QOVSdRQoiwbmT8xVuYuCzTv9THhuJ//i5u7s4y3Xyl7u17B3tdm86UlMpQHy/w9EsYaSBPOU4oPNomRtOnTSugh0v9ZBwptP5XfbslII/iA+LQdzTHhchn0W0CRyDqjOMSestWlrsq5NZJtBJTYHbebllOhEI7xbj9tY+re1zFWSPMOPgHJP23ovsdk3hD9OT93AzRHInCx5IxL6QvEgRhAancRuGkf2rGP0g/vX9fQ0Il3rNMSQxHB5CyHUBtUJ9nhU79YkMDZicD0jFMEwjWJO3itAp3ynoLXRgktgQCYUfgc9SpdWKD5SXLCYnSo22JD3D1P6h2EertRHaoKRLb+CRXQC/lM8uh/W+BjA2Xe6Vut2I/72ndjM+10T7E2xk1CFyCH37a5p8cH26Fs="
- secure: "J9380tGLOZWa7dSH1y5Il8T5JQpN6ad81gI6VR1HIU0svpRdjgikyDA7ca2MKYDUYYY9yVSkTV6gCl6iIU/9+SKaYugpP+tkvdGYkC2moJdcTgYM/WOnIK9ExQ3BPhN1neGxJjPTwKo1ft27mtZ2I5vuCiBwIcnKWLnKPyW3PD+mWpfqiLuEzkHoAh6G3jC4qbcCrZDeX/knE+PzqESUEi+8k1G8gYcSDWujba9ypSsqZ8T/MXagGla6l7y2Rz+/KZTJmFHwKAA10V+xPLVqxoiqi4ar66yUqy0BamwRXPcseI+ns3Q+4lUpMqVQ5GlRy7LF1xC8myjmcAexXk0F9hg+CMzewKI8UgmQH/ZJvQZEh8s6mW26+CqA4d3zMQkWaR0WtEtpiuH7AGHCflIqvEQ6UiG7ia3B8iZfW2wl0j/kqx4OuHkS3r0pWKVVIIvCj9Ow2BHP7SpiV1AcUGsVxzwbgTh67fitna3Z3c6Uj8ccQlNr7ZIt1az6Wf3w5njijkLOiBpQSLKunTTCTSge/JzBTKUcie3RE9vzirl58gUxAt36nDtPWnory+RttMZrOkBVbTeSxp+IUe8pNwLFPHABsafXsjkfzBOtFmm+0ZXWt2Rlog5NvlemJfQUWDlsL4g+BSakzN+4sIPKzSauWDHyaEeULY7Uprkil6c5zwo="
- secure: "szcjWHPr0Bf1KCkyTrV5Fu3ADhWk+pg8YWucjXHdybmhaQIKG7iBNg8LJ5d0OBTwAg31wK4ZgyLVSa2gKrAZ3UeDjykJFsR711xDSQOod51Wrgqu4FbXDewE817DUk3Cwe1l5DCu3/fjEw4vbm8B/qb7iMTRKCq6hJd97FwT5oauP0QHNPer9JjrW4F0Hk9ttkgEU2dXWvBMsTJsDOGNI3ddABE2HskxV4T4thelDYGKBDHhUOAsRwSjXgWy77Tvz98psPIvd+6+WPYNRdRWcPDyAR3Z1O/fNjUymrQI6eMaHoSFrmhDS5lbhjINRfdUmECyfCfIFeLWWiw4g4bq7l+4HBORbei55tAIjhEsxJQoqHi0Q5dD5TFh8IiWqowkFbpvNonMSIpKtB0cyT5jU1G/jRA7MPcIvSrdzHaDkoDNHJgAeZfgjOhzTGYYD19lGIljz5BQBcNFZY2dJbja+Jr4He2CMAOBOdERa4Zn1VyNfOmd8Bn5hu0C9D2ybnSCxjXXq5TRiktR8X7WycVZYfqMZXAwP9FEHVitJ4MZEGUc7S92K5gX4wmjcJjLS+Xo/0nsduQm8PuiMjbcPM7/oGx8Xm1KuSfHdKWMBoaesPaDvRX+YcuiNstXf1DkCWl72TsFABzddlNUMl/s2YSKkCSHAJ5ILqrB28Gx89kzVlg="
@@ -36,6 +37,14 @@ cache:
matrix:
include:
# - os: linux
# name: "Linux 64-bit Bionic"
# dist: bionic
# language: bash
# env:
# - GAMOS=linux
# - PLATFORM=x86_64
# - VMTYPE=cache
- os: linux
name: "Linux 64-bit Xenial"
dist: xenial
@@ -61,7 +70,7 @@ matrix:
- PLATFORM=x86_64
- VMTYPE=build
- os: linux
name: "Linux 64-bit Xenial - Python 3.5 Source Testing"
name: "Linux 64-bit - Python 3.5 Source Testing"
dist: xenial
language: python
python:
@@ -71,8 +80,8 @@ matrix:
- PLATFORM=x86_64
- VMTYPE=test
- os: linux
name: "Linux 64-bit Xenial - Python 3.6 Source Testing"
dist: xenial
name: "Linux 64-bit - Python 3.6 Source Testing"
dist: bionic
language: python
python:
- "3.6"
@@ -81,8 +90,8 @@ matrix:
- PLATFORM=x86_64
- VMTYPE=test
- os: linux
name: "Linux 64-bit Xenial - Python 3.8-dev Source Testing"
dist: xenial
name: "Linux 64-bit - Python 3.8-dev Source Testing"
dist: bionic
language: python
python:
- "3.8-dev"
@@ -91,8 +100,8 @@ matrix:
- PLATFORM=x86_64
- VMTYPE=test
- os: linux
name: "Linux 64-bit Xenial - Python nightly Source Testing"
dist: xenial
name: "Linux 64-bit - Python nightly Source Testing"
dist: bionic
language: python
python:
- "nightly"
@@ -134,10 +143,12 @@ install:
script:
- $gam version extended
- $gam version | grep travis # travis should be part of the path (not /tmp or such)
- if [ "$VMTYPE" == "build" ]; then $gam version | grep "Python ${BUILD_PYTHON_VERSION//./\\.}"; fi # We should be building with latest Python
- if [ "$VMTYPE" == "build" ]; then $gam version extended | grep "OpenSSL ${BUILD_OPENSSL_VERSION//./\\.}"; fi # We should be using OpenSSL 1.1.1+
# determine which Python version GAM is built with and ensure it's at least build version from above.
- if [ "VMTYPE" == "build" ]; then vline=$(gam version | grep "Python "); python_line=($vline); this_python=${python_line[1]}; tools/a_atleast_b.py $this_python $BUILD_PYTHON_VERSION; fi
# determine which OpenSSL version GAM is built with and ensure it's at least build version from above.
- if [ "VMTYPE" == "build" ]; then vline=$(gam version extended | grep "OpenSSL "); openssl_line=($vline); this_openssl=${openssl_line[1]}; tools/a_atleast_b.py $this_openssl $BUILD_OPENSSL_VERSION; fi
- if [ "$VMTYPE" == "build" ]; then $gam version extended | grep TLSv1\.[23]; fi # Builds should default TLS 1.2 or 1.3 to Google
- if [ "$VMTYPE" == "build" ]; then GAM_TLS_MIN_VERSION=TLSv1_2 $gam version extended location tls-v1-0.badssl.com:1010; [[ $? == 3 ]]; fi # expect fail if server doesn't support our TLS version
- if [ "$VMTYPE" == "build" ]; then GAM_TLS_MIN_VERSION=TLSv1_2 $gam version extended location tls-v1-0.badssl.com:1010; [[ $? == 3 ]]; fi # expect fail since server doesn't support our TLS version
- export jid="$(cut -d'.' -f2 <<<"$TRAVIS_JOB_NUMBER")"
- if [ "$TRAVIS_EVENT_TYPE" != "pull_request" ]; then export e2e=true; fi
- if [ "$e2e" = true ]; then export gam_user=gam-travis-$jid@pdl.jaylee.us; fi
@@ -216,6 +227,8 @@ script:
- if [ "$e2e" = true ]; then $gam user $gam_user show tokens; fi
- if [ "$e2e" = true ]; then $gam delete user $newuser; fi
- if [ "$e2e" = true ]; then $gam print users query "travis.jid=$jid" | $gam csv - gam delete user ~primaryEmail; fi
- if [ "$e2e" = true ]; then $gam print mobile; fi
- if [ "$e2e" = true ]; then $gam print cros allfields nolists; fi
before_deploy:
- export TRAVIS_TAG="preview"

View File

@@ -813,7 +813,9 @@ Example: gam csv Users.csv gam update user "~primaryEmail" address type work uns
Each user (~primaryEmail, e.g. foo@bar.com) would have their work address updated
gam create project [<EmailAddress>] [<ProjectID>]
gam create project [admin <EmailAddress>] [project <ProjectID>] [parent <String>]
gam use project [<EmailAddress>] [<ProjectID>]
gam use project [admin <EmailAddress>] [project <ProjectID>]
gam update project [<EmailAddress>] [gam|<ProjectID>|(filter <String>)]
gam delete project [<EmailAddress>] [gam|<ProjectID>|(filter <String>)]
gam show projects [<EmailAddress>] [all|gam|<ProjectID>|(filter <String>)]
@@ -872,7 +874,7 @@ gam info resoldsubscriptions <CustomerID> [customer_auth_token <String>]
<ReportsAppList> ::= "<ReportsApp>(,<ReportsApp>)*"
gam report users|user [todrive] [date <Date>] [fulldatarequired all|<ReportsAppList>]
[(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath>)] [filter|filters <String>] [fields|parameters <String>]
[(user <UserItem>)|(orgunit|org|ou <OrgUnitPath>)] [filter|filters <String>] [fields|parameters <String>]
gam report customers|customer|domain [todrive] [date <Date>] [fulldatarequired all|<ReportsAppList>]
[fields|parameters <String>]
gam report admin|calendar|calendars|drive|docs|doc|groups|group|logins|login|mobile|tokens|token [todrive]
@@ -945,12 +947,12 @@ gam calendar <CalendarItem> showacl
gam calendar <CalendarItem> printacl [todrive]
<EventNotificationAttribute> ::=
notifyattendees|(sendnotifications <Boolean>)|(sendupdates all|enternalonly|none)
notifyattendees|(sendnotifications <Boolean>)|(sendupdates all|enternalonly|none)
The following attributes are equivalent:
notifyattendees - sendupdates all
sendnotifications false - sendupdates none
sendnotifications true - sendupdates all
notifyattendees - sendupdates all
sendnotifications false - sendupdates none
sendnotifications true - sendupdates all
<EventAttributes> ::=
anyonecanaddself|
@@ -976,15 +978,15 @@ The following attributes are equivalent:
(visibility default|public|prvate)
<EventSelectProperty:> ::=
(after <Time>)|
(before <Time>)|
includeeleted|
includehidden|
(query <QueryCalendar>)|
(updatedmin <Time>)
(after <Time>)|
(before <Time>)|
includeeleted|
includehidden|
(query <QueryCalendar>)|
(updatedmin <Time>)
<EventDisplayProperty> ::=
(timezone <TimeZone>)
(timezone <TimeZone>)
gam calendar <CalendarItem> addevent <EventAttributes>+ [<EventNotificationAttribute>]
gam calendar <CalendarItem> deleteevent id|eventid <EventID> [doit] [<EventNotificationAttribute>]
@@ -1048,7 +1050,7 @@ The listlimit <Number> argument limits the number of recent users, time ranges a
The start <Date> and end <Date> arguments filter the time ranges.
Delimiter defaults to comma.
gam update mobile <MobileID> action <MobileAction>
gam update mobile <MobileID>|query:<QueryMobile> action <MobileAction> [doit] [if_users|match_users <UserTypeEntity>]
gam delete mobile <MobileID>
gam info mobile <MobileID>
gam print mobile [todrive] [(query <QueryMobile>)|(queries <QueryMobileList>)] [basic|full] [orderby <MobileOrderByFieldName> [ascending|descending]]
@@ -1092,7 +1094,7 @@ gam create resource <ResourceID> <Name> <ResourceAttributes>*
gam update resource <ResourceID> <ResourceAttributes>*
gam delete resource <ResourceID>
gam info resource <ResourceID>
gam print resources [todrive] [allfields] <ResourceFieldName>*
gam print resources [todrive] [allfields] <ResourceFieldName>* [query <String>]
gam create schema|schemas <SchemaName> <SchemaFieldDefinition>+
gam update schema <SchemaName> <SchemaFieldDefinition>* (deletefield <FieldName>)*
@@ -1173,9 +1175,11 @@ gam print printjobs [todrive] [printer|printerid <PrinterID>]
gam create vaultexport|export matter <MatterItem> [name <name>] corpus <drive|mail|groups|hangouts_chat>
(accounts <EmailAddressList>) | (orgunit|ou <OrgUnitPath>) | (teamdrives <TeamDriveList>) | (rooms <ChatRoomList>) | everyone
[scope <all_data|held_data|unprocessed_data>]
[terms <terms>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>] [format mbox|pst]
[excludedrafts <Boolean>] [driveversiondate <Date>|<Time>] [includeteamdrives] [includerooms]
[includeaccessinfo <Boolean>]
[terms <terms>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>]
[excludedrafts <Boolean>] [format mbox|pst] [showconfidentialmodecontent <Boolean>]
[includerooms]
[driveversiondate <Date>|<Time>] [includeshareddrives|includeteamdrives] [includeaccessinfo <Boolean>]
[region any|europe|us]
gam delete export <MatterItem> <ExportItem>
gam info export <MatterItem> <ExportItem>
gam print exports [todrive] [matters <MatterItemList>]
@@ -1286,10 +1290,20 @@ gam <UserTypeEntity> untrash messages query <QueryGmail> [doit] [max_to_untrash|
gam <UserTypeEntity> show gmailprofile [todrive]
gam <UserTypeEntity> draftemail [recipient <EmailAddress>] [subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
gam <UserTypeEntity> importemail [recipient <EmailAddress>] [subject <String> [(message <String>)|(file <FileName> [charset <Charset>])]]
gam <UserTypeEntity> insertemail [recipient <EmailAddress>] [subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
gam <UserTypeEntity> sendemail [recipient <EmailAddress>] [subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
gam <UserTypeEntity> draftemail [recipient|to <EmailAddress>] [from <EmailAddress>]
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
gam <UserTypeEntity> importemail [recipient|to <EmailAddress>] [from <EmailAddress>]
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
[labels <LabelNameList>] (header <String> <String>)*
[deleted] [date <Time>]
[nevercheckspam] [processforcalendar]
gam <UserTypeEntity> insertemail [recipient|to <EmailAddress>] [from <EmailAddress>]
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
[labels <LabelNameList>] (header <String> <String>)*
[deleted] [date <Time>]
gam <UserTypeEntity> sendemail [recipient|to <EmailAddress>] [from <EmailAddress>]
[subject <String>] [(message <String>)|(file <FileName> [charset <Charset>])]
(header <String> <String>)*
gam <UserTypeEntity> create|add delegate|delegates <EmailAddress>
gam <UserTypeEntity> delegate|delegates to <EmailAddress>

View File

@@ -92,7 +92,7 @@
"crambo chromebook": "2020-06-01T00:00:00.000Z",
"ctl chromebook j41 / j41t": "2023-11-01T00:00:00.000Z",
"ctl chromebook nl7": "2023-11-01T00:00:00.000Z",
"ctl chromebook nl7 / nl7t-360 / nl7tw-360": "2023-11-01T00:00:00.000Z",
"ctl chromebook nl7t-360 / nl7tw-360": "2023-11-01T00:00:00.000Z",
"ctl chromebook tab tx1": "2023-08-01T00:00:00.000Z",
"ctl chromebook tablet tx1 for education": "2023-08-01T00:00:00.000Z",
"ctl chromebox cbx1": "2024-06-01T00:00:00.000Z",
@@ -212,10 +212,11 @@
"pcmerge chromebookpcm-116e/pcm-116eb": "2020-06-01T00:00:00.000Z",
"pcmerge chromebookpcm-116t-432b": "2021-08-01T00:00:00.000Z",
"poin2 chromebook 11": "2020-06-01T00:00:00.000Z",
"poin2 chromebook 11c": "2022-03-01T00:00:00.000Z",
"poin2 chromebook 11c": "2022-11-01T00:00:00.000Z",
"poin2 chromebook 14": "2022-03-01T00:00:00.000Z",
"positivo chromebook c216b": "2021-08-01T00:00:00.000Z",
"positivo chromebook ch1190": "2020-06-01T00:00:00.000Z",
"promethean chromebox": "2024-06-01T00:00:00.000Z",
"prowise 11.6\" entry line chromebook": "2020-06-01T00:00:00.000Z",
"prowise chromebook eduline": "2023-11-01T00:00:00.000Z",
"prowise chromebook entryline": "2020-06-01T00:00:00.000Z",
@@ -224,11 +225,12 @@
"rgs education chromebook": "2020-06-01T00:00:00.000Z",
"samsung chromebook": "2018-07-01T00:00:00.000Z",
"samsung chromebook - xe303": "2018-07-01T00:00:00.000Z",
"samsung chromebook 2 11\"": "2019-06-01T00:00:00.000Z",
"samsung chromebook 2 11\" - xe500c12": "2020-06-01T00:00:00.000Z",
"samsung chromebook 2 13\"": "2019-06-01T00:00:00.000Z",
"samsung chromebook 2 11": "2019-06-01T00:00:00.000Z",
"samsung chromebook 2 11 - xe500c12": "2020-06-01T00:00:00.000Z",
"samsung chromebook 2 13": "2019-06-01T00:00:00.000Z",
"samsung chromebook 3": "2021-06-01T00:00:00.000Z",
"samsung chromebook plus": "2023-08-01T00:00:00.000Z",
"samsung chromebook plus (lte)": "2024-06-01T00:00:00.000Z",
"samsung chromebook plus (v2)": "2024-06-01T00:00:00.000Z",
"samsung chromebook pro": "2022-11-01T00:00:00.000Z",
"samsung chromebook series 5": "2016-06-01T00:00:00.000Z",

View File

@@ -55,7 +55,6 @@ from multiprocessing import Pool
from multiprocessing import freeze_support
from urllib.parse import urlencode, urlparse
from passlib.hash import sha512_crypt
import dns.resolver
import dateutil.parser
import googleapiclient
@@ -110,7 +109,7 @@ google_auth_httplib2.AuthorizedHttp.request = _request_with_user_agent(
def _createHttpObj(cache=None, override_min_tls=None, override_max_tls=None):
tls_minimum_version = override_min_tls if override_min_tls else GC_Values[GC_TLS_MIN_VERSION]
tls_maximum_version = override_max_tls if override_max_tls else GC_Values[GC_TLS_MAX_VERSION]
return httplib2.Http(tls_maximum_version=tls_maximum_version, tls_minimum_version=tls_minimum_version,
return httplib2.Http(ca_certs=GC_Values[GC_CA_FILE], tls_maximum_version=tls_maximum_version, tls_minimum_version=tls_minimum_version,
cache=cache)
def showUsage():
@@ -684,6 +683,7 @@ def SetGlobalVariables():
_getOldEnvVar(GC_CA_FILE, 'GAM_CA_FILE')
_getOldSignalFile(GC_DEBUG_LEVEL, 'debug.gam', filePresentValue=4, fileAbsentValue=0)
_getOldSignalFile(GC_NO_BROWSER, 'nobrowser.txt')
_getOldSignalFile(GC_OAUTH_BROWSER, 'oauthbrowser.txt')
# _getOldSignalFile(GC_NO_CACHE, u'nocache.txt')
# _getOldSignalFile(GC_CACHE_DISCOVERY_ONLY, u'allcache.txt', filePresentValue=False, fileAbsentValue=True)
_getOldSignalFile(GC_NO_CACHE, 'allcache.txt', filePresentValue=False, fileAbsentValue=True)
@@ -743,6 +743,7 @@ def doGAMCheckForUpdates(forceCheck=False):
check_url = GAM_LATEST_RELEASE # latest full release
headers = {'Accept': 'application/vnd.github.v3.text+json'}
simplehttp = _createHttpObj()
simplehttp.timeout = 10
try:
(_, c) = simplehttp.request(check_url, 'GET', headers=headers)
try:
@@ -775,7 +776,7 @@ def doGAMCheckForUpdates(forceCheck=False):
sys.exit(0)
writeFile(GM_Globals[GM_LAST_UPDATE_CHECK_TXT], str(now_time), continueOnError=True, displayError=forceCheck)
return
except (httplib2.HttpLib2Error, httplib2.ServerNotFoundError, RuntimeError):
except (httplib2.HttpLib2Error, httplib2.ServerNotFoundError, RuntimeError, socket.timeout):
return
def doGAMVersion(checkForArgs=True):
@@ -1778,7 +1779,7 @@ def watchGmail(users):
break
else:
topic = gamTopics+str(uuid.uuid4())
callGAPI(pubsub.projects().topics(), 'create', name=topic, body={})
callGAPI(pubsub.projects().topics(), 'create', name=topic)
body = {'policy': {'bindings': [{'members': ['serviceAccount:gmail-api-push@system.gserviceaccount.com'], 'role': 'roles/pubsub.editor'}]}}
callGAPI(pubsub.projects().topics(), 'setIamPolicy', resource=topic, body=body)
subscriptions = callGAPIpages(pubsub.projects().topics().subscriptions(), 'list', items='subscriptions', topic=topic)
@@ -1975,9 +1976,6 @@ def doDelCourse():
callGAPI(croom.courses(), 'delete', id=courseId)
print('Deleted Course %s' % courseId)
def _getValidCourseStates(croom):
return [state for state in croom._rootDesc['schemas']['Course']['properties']['courseState']['enum'] if state != 'COURSE_STATE_UNSPECIFIED']
def _getValidatedState(state, validStates):
state = state.upper()
if state not in validStates:
@@ -1998,13 +1996,13 @@ def getCourseAttribute(myarg, value, body, croom, function):
elif myarg in ['owner', 'ownerid', 'teacher']:
body['ownerId'] = normalizeEmailAddressOrUID(value)
elif myarg in ['state', 'status']:
validStates = _getValidCourseStates(croom)
validStates = _getEnumValuesMinusUnspecified(croom._rootDesc['schemas']['Course']['properties']['courseState']['enum'])
body['courseState'] = _getValidatedState(value, validStates)
else:
systemErrorExit(2, '%s is not a valid argument to "gam %s course"' % (myarg, function))
def _getCourseStates(croom, value, courseStates):
validStates = _getValidCourseStates(croom)
validStates = _getEnumValuesMinusUnspecified(croom._rootDesc['schemas']['Course']['properties']['courseState']['enum'])
for state in value.replace(',', ' ').split():
courseStates.append(_getValidatedState(state, validStates))
@@ -2085,13 +2083,14 @@ def doGetCustomerInfo():
# If customer has changed primary domain customerCreationTime is date
# of current primary being added, not customer create date.
# We should also get all domains and use oldest date
domains = doPrintDomains(return_results=True)
oldest = datetime.datetime.strptime(customer_info['customerCreationTime'], '%Y-%m-%dT%H:%M:%S.%fZ')
domains = callGAPIitems(cd.domains(), 'list', 'domains',
customer=GC_Values[GC_CUSTOMER_ID], fields='domains(creationTime)')
for domain in domains:
domain_creation = datetime.datetime.strptime(domain['creationTime'], '%Y-%m-%d %H:%M:%S.%f')
domain_creation = datetime.datetime.fromtimestamp(int(domain['creationTime'])/1000)
if domain_creation < oldest:
oldest = domain_creation
print('Customer Creation Time: %s' % oldest)
print('Customer Creation Time: %s' % oldest.strftime('%Y-%m-%dT%H:%M:%SZ'))
print('Default Language: %s' % customer_info.get('language', 'Unset (defaults to en)'))
if 'postalAddress' in customer_info:
print('Address:')
@@ -2170,7 +2169,7 @@ def doDelDomainAlias():
domainAliasName = sys.argv[3]
callGAPI(cd.domainAliases(), 'delete', customer=GC_Values[GC_CUSTOMER_ID], domainAliasName=domainAliasName)
def doPrintDomains(return_results=False):
def doPrintDomains():
cd = buildGAPIObject('directory')
todrive = False
titles = ['domainName',]
@@ -2211,8 +2210,6 @@ def doPrintDomains(return_results=False):
titles.append(attr)
aliasdomain_attributes[attr] = aliasdomain[attr]
csvRows.append(aliasdomain_attributes)
if return_results:
return csvRows
writeCSVfile(csvRows, titles, 'Domains', todrive)
def doPrintDomainAliases():
@@ -4695,11 +4692,24 @@ def deleteDriveFile(users):
file_ids = [fileIds,]
if not file_ids:
print('No files to %s for %s' % (function, user))
i = 0
j = 0
batch_size = 10
dbatch = drive.new_batch_http_request(callback=drive_del_result)
method = getattr(drive.files(), function)
for fileId in file_ids:
i += 1
print('%s %s for %s (%s/%s)' % (action, fileId, user, i, len(file_ids)))
callGAPI(drive.files(), function, fileId=fileId, supportsAllDrives=True)
j += 1
dbatch.add(method(fileId=fileId, supportsAllDrives=True))
if len(dbatch._order) == batch_size:
print('%s %s files...' % (action, len(dbatch._order)))
dbatch.execute()
dbatch = drive.new_batch_http_request(callback=drive_del_result)
if len(dbatch._order) > 0:
print('%s %s files...' % (action, len(dbatch._order)))
dbatch.execute()
def drive_del_result(request_id, response, exception):
if exception:
print(exception)
def printDriveFolderContents(feed, folderId, indent):
for f_file in feed:
@@ -5346,8 +5356,13 @@ def transferDriveFiles(users):
def sendOrDropEmail(users, method='send'):
body = subject = ''
recipient = None
labels = None
recipient = labels = sender = None
kwargs = {}
if method in ['insert', 'import']:
kwargs['internalDateSource'] = 'receivedTime'
if method == 'import':
kwargs['neverMarkSpam'] = True
msgHeaders = {}
i = 4
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
@@ -5361,16 +5376,35 @@ def sendOrDropEmail(users, method='send'):
elif myarg == 'subject':
subject = sys.argv[i+1]
i += 2
elif myarg == 'recipient':
elif myarg in ['recipient', 'to']:
recipient = sys.argv[i+1]
i += 2
elif myarg == 'labels':
labels = sys.argv[i+1].split(',')
elif myarg == 'from':
sender = sys.argv[i+1]
i += 2
elif myarg == 'header':
msgHeaders[sys.argv[i+1]] = sys.argv[i+2]
i += 3
elif method in ['insert', 'import'] and myarg == 'labels':
labels = shlexSplitList(sys.argv[i+1])
i += 2
elif method in ['insert', 'import'] and myarg == 'deleted':
kwargs['deleted'] = True
i += 1
elif myarg == 'date':
msgHeaders['Date'] = getTimeOrDeltaFromNow(sys.argv[i+1])
if method in ['insert', 'import']:
kwargs['internalDateSource'] = 'dateHeader'
i += 2
elif method == 'import' and myarg == 'checkspam':
kwargs['neverMarkSpam'] = False
i += 1
elif method == 'import' and myarg == 'processforcalendar':
kwargs['processForCalendar'] = True
else:
systemErrorExit(2, '%s is not a valid argument for "gam <users> sendemail"' % sys.argv[i])
systemErrorExit(2, '%s is not a valid argument for "gam <users> %semail"' % (sys.argv[i], method))
for user in users:
send_email(subject, body, recipient, user, method, labels)
send_email(subject, body, recipient, sender, user, method, labels, msgHeaders, kwargs)
def doImap(users):
enable = getBoolean(sys.argv[4], 'gam <users> imap')
@@ -7571,6 +7605,21 @@ def getUserAttributes(i, cd, updateCmd):
body['hashFunction'] = 'crypt'
return body
class ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
def authorization_url(self, **kwargs):
long_url, state = super(ShortURLFlow, self).authorization_url(**kwargs)
simplehttp = httplib2.Http()
simplehttp.timeout = 10
url_shortnr = 'https://gam-shortn.appspot.com/create'
headers = {'Content-Type': 'application/json'}
try:
resp, content = simplehttp.request(url_shortnr, 'POST', '{"long_url": "%s"}' % long_url, headers=headers)
except:
return long_url, state
if resp.status != 200:
return long_url, state
return json.loads(content).get('short_url', ''), state
def _run_oauth_flow(client_id, client_secret, scopes, access_type, login_hint=None):
client_config = {
'installed': {
@@ -7582,11 +7631,11 @@ def _run_oauth_flow(client_id, client_secret, scopes, access_type, login_hint=No
}
}
flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_config(client_config, scopes)
flow = ShortURLFlow.from_client_config(client_config, scopes)
kwargs = {'access_type': access_type}
if login_hint:
kwargs['login_hint'] = login_hint
if GC_Values[GC_NO_BROWSER]:
if not GC_Values[GC_OAUTH_BROWSER]:
flow.run_console(
authorization_prompt_message=MESSAGE_CONSOLE_AUTHORIZATION_PROMPT,
authorization_code_message=MESSAGE_CONSOLE_AUTHORIZATION_CODE,
@@ -7603,11 +7652,17 @@ def getCRMService(login_hint):
client_id = '297408095146-fug707qsjv4ikron0hugpevbrjhkmsk7.apps.googleusercontent.com'
client_secret = 'qM3dP8f_4qedwzWQE1VR4zzU'
credentials = _run_oauth_flow(client_id, client_secret, scopes, 'online', login_hint)
http = google_auth_httplib2.AuthorizedHttp(credentials)
httpc = google_auth_httplib2.AuthorizedHttp(credentials)
return (googleapiclient.discovery.build('cloudresourcemanager', 'v1',
http=http, cache_discovery=False,
http=httpc, cache_discovery=False,
discoveryServiceUrl=googleapiclient.discovery.V2_DISCOVERY_URI),
http)
httpc)
# Ugh, v2 doesn't contain all the operations of v1 so we need to use both here.
def getCRM2Service(httpc):
return googleapiclient.discovery.build('cloudresourcemanager', 'v2',
http=httpc, cache_discovery=False,
discoveryServiceUrl=googleapiclient.discovery.V2_DISCOVERY_URI)
def getGAMProjectFile(filepath):
file_url = GAM_PROJECT_FILEPATH+filepath
@@ -7759,7 +7814,7 @@ def _createClientSecretsOauth2service(httpObj, projectId):
VALIDEMAIL_PATTERN = re.compile(r'^[^@]+@[^@]+\.[^@]+$')
def _getValidateLoginHint(login_hint):
def _getValidateLoginHint(login_hint=None):
while True:
if not login_hint:
login_hint = input('\nWhat is your G Suite admin email address? ').strip()
@@ -7791,11 +7846,29 @@ PROJECTID_FORMAT_REQUIRED = '[a-z][a-z0-9-]{4,28}[a-z0-9]'
def _getLoginHintProjectId(createCmd):
login_hint = None
projectId = None
try:
login_hint = sys.argv[3]
projectId = sys.argv[4]
except IndexError:
pass
parent = None
if len(sys.argv) >= 4 and sys.argv[3].lower() not in ['admin', 'project', 'parent']:
# legacy "gam create/use project <email> <project-id>
try:
login_hint = sys.argv[3]
projectId = sys.argv[4]
except IndexError:
pass
else:
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'admin':
login_hint = sys.argv[i+1]
i += 2
elif myarg == 'project':
projectId = sys.argv[i+1]
i += 2
elif createCmd and myarg == 'parent':
parent = sys.argv[i+1]
i += 2
else:
systemErrorExit(3, '%s is not a valid argument for "gam %s project", expected one of: admin, project%s' % (myarg, ['use', 'create'][createCmd], ['', ' or parent'][createCmd]))
login_hint = _getValidateLoginHint(login_hint)
if projectId:
if not PROJECTID_PATTERN.match(projectId):
@@ -7809,6 +7882,14 @@ def _getLoginHintProjectId(createCmd):
if not PROJECTID_PATTERN.match(projectId):
systemErrorExit(2, 'Invalid Project ID: {0}, expected <{1}>'.format(projectId, PROJECTID_FORMAT_REQUIRED))
crm, httpObj = getCRMService(login_hint)
if parent and not parent.startswith('organizations/') and not parent.startswith('folders/'):
crm2 = getCRM2Service(httpObj)
parent = convertGCPFolderNameToID(parent, crm2)
if parent:
parent_type, parent_id = parent.split('/')
if parent_type[-1] == 's':
parent_type = parent_type[:-1] # folders > folder, organizations > organization
parent = {'type': parent_type, 'id': parent_id}
projects = _getProjects(crm, 'id:{0}'.format(projectId))
if not createCmd:
if not projects:
@@ -7818,9 +7899,29 @@ def _getLoginHintProjectId(createCmd):
else:
if projects:
systemErrorExit(2, 'User: {0}, Project ID: {1}, Duplicate'.format(login_hint, projectId))
return (crm, httpObj, login_hint, projectId)
return (crm, httpObj, login_hint, projectId, parent)
PROJECTID_FILTER_REQUIRED = 'gam|<ProjectID>|(filter <String>)'
def convertGCPFolderNameToID(parent, crm2):
# crm2.folders() is broken requiring pageToken, etc in body, not URL.
# for now just use callGAPI and if user has that many folders they'll
# just need to be specific.
folders = callGAPIitems(crm2.folders(), 'search', items='folders',
body={'pageSize': 1000, 'query': 'displayName="%s"' % parent})
if not folders:
systemErrorExit(1, 'ERROR: No folder found matching displayName=%s' % parent)
if len(folders) > 1:
print('Multiple matches:')
for folder in folders:
print(' Name: %s ID: %s' % (folder['name'], folder['displayName']))
systemErrorExit(2, 'ERROR: Multiple matching folders, please specify one.')
return folders[0]['name']
def createGCPFolder():
login_hint = _getValidateLoginHint()
_, httpObj = getCRMService(login_hint)
crm2 = getCRM2Service(httpObj)
callGAPI(crm2.folders(), 'create', body={'name': sys.argv[3], 'displayName': sys.argv[3]})
def _getLoginHintProjects(printShowCmd):
login_hint = None
@@ -7865,9 +7966,11 @@ def _checkForExistingProjectFiles():
def doCreateProject():
_checkForExistingProjectFiles()
crm, httpObj, login_hint, projectId = _getLoginHintProjectId(True)
crm, httpObj, login_hint, projectId, parent = _getLoginHintProjectId(True)
login_domain = login_hint[login_hint.find('@')+1:]
body = {'projectId': projectId, 'name': 'GAM Project'}
if parent:
body['parent'] = parent
while True:
create_again = False
print('Creating project "%s"...' % body['name'])
@@ -7890,7 +7993,7 @@ def doCreateProject():
except (KeyError, IndexError):
systemErrorExit(3, 'you have no rights to create projects for your organization and you don\'t seem to be a super admin! Sorry, there\'s nothing more I can do.')
org_policy = callGAPI(crm.organizations(), 'getIamPolicy',
resource=organization, body={})
resource=organization)
if 'bindings' not in org_policy:
org_policy['bindings'] = []
print('Looks like no one has rights to your Google Cloud Organization. Attempting to give you create rights...')
@@ -7940,7 +8043,7 @@ and accept the Terms of Service (ToS). As soon as you've accepted the ToS popup,
def doUseProject():
_checkForExistingProjectFiles()
_, httpObj, _, projectId = _getLoginHintProjectId(False)
_, httpObj, _, projectId, _ = _getLoginHintProjectId(False)
_createClientSecretsOauth2service(httpObj, projectId)
def doUpdateProjects():
@@ -7981,7 +8084,7 @@ def doPrintShowProjects(csvFormat):
todrive = True
i += 1
else:
systemErrorExit(2, '%s is not a valid argument for "gam %s projects"' % (myarg, ['show', 'print'][csvFormat]))
systemErrorExit(3, '%s is not a valid argument for "gam %s projects"' % (myarg, ['show', 'print'][csvFormat]))
if not csvFormat:
count = len(projects)
print('User: {0}, Show {1} Projects'.format(login_hint, count))
@@ -8197,25 +8300,20 @@ VAULT_SEARCH_METHODS_MAP = {
'ou': 'ORG_UNIT',
'room': 'ROOM',
'rooms': 'ROOM',
'teamdrive': 'TEAM_DRIVE',
'teamdrives': 'TEAM_DRIVE',
'shareddrive': 'SHARED_DRIVE',
'shareddrives': 'SHARED_DRIVE',
'teamdrive': 'SHARED_DRIVE',
'teamdrives': 'SHARED_DRIVE',
}
VAULT_SEARCH_METHODS_LIST = ['accounts', 'orgunit', 'teamdrives', 'rooms', 'everyone']
VAULT_SEARCH_METHODS_LIST = ['accounts', 'orgunit', 'shareddrives', 'rooms', 'everyone']
def doCreateVaultExport():
v = buildGAPIObject('vault')
allowed_corpuses = v._rootDesc['schemas']['Query']['properties']['corpus']['enum']
try:
allowed_corpuses.remove('CORPUS_TYPE_UNSPECIFIED')
except ValueError:
pass
allowed_scopes = v._rootDesc['schemas']['Query']['properties']['dataScope']['enum']
try:
allowed_scopes.remove('DATA_SCOPE_UNSPECIFIED')
except ValueError:
pass
allowed_formats = ['MBOX', 'PST']
allowed_corpuses = _getEnumValuesMinusUnspecified(v._rootDesc['schemas']['Query']['properties']['corpus']['enum'])
allowed_scopes = _getEnumValuesMinusUnspecified(v._rootDesc['schemas']['Query']['properties']['dataScope']['enum'])
allowed_formats = _getEnumValuesMinusUnspecified(v._rootDesc['schemas']['MailExportOptions']['properties']['exportFormat']['enum'])
export_format = 'MBOX'
showConfidentialModeContent = None # default to not even set
matterId = None
body = {'query': {'dataScope': 'ALL_DATA'}, 'exportOptions': {}}
i = 3
@@ -8244,8 +8342,8 @@ def doCreateVaultExport():
elif searchMethod == 'ORG_UNIT':
body['query']['orgUnitInfo'] = {'orgUnitId': getOrgUnitId(sys.argv[i+1])[1]}
i += 2
elif searchMethod == 'TEAM_DRIVE':
body['query']['teamDriveInfo'] = {'teamDriveIds': sys.argv[i+1].split(',')}
elif searchMethod == 'SHARED_DRIVE':
body['query']['sharedDriveInfo'] = {'sharedDriveIds': sys.argv[i+1].split(',')}
i += 2
elif searchMethod == 'ROOM':
body['query']['hangoutsChatInfo'] = {'roomId': sys.argv[i+1].split(',')}
@@ -8275,8 +8373,8 @@ def doCreateVaultExport():
elif myarg in ['driveversiondate']:
body['query'].setdefault('driveOptions', {})['versionDate'] = getDateZeroTimeOrFullTime(sys.argv[i+1])
i += 2
elif myarg in ['includeteamdrives']:
body['query'].setdefault('driveOptions', {})['includeTeamDrives'] = getBoolean(sys.argv[i+1], myarg)
elif myarg in ['includeshareddrives', 'includeteamdrives']:
body['query'].setdefault('driveOptions', {})['includeSharedDrives'] = getBoolean(sys.argv[i+1], myarg)
i += 2
elif myarg in ['includerooms']:
body['query']['hangoutsChatOptions'] = {'includeRooms': getBoolean(sys.argv[i+1], myarg)}
@@ -8287,6 +8385,15 @@ def doCreateVaultExport():
print('ERROR: export format can be one of %s, got %s' % (', '.join(allowed_formats), export_format))
sys.exit(3)
i += 2
elif myarg in ['showconfidentialmodecontent']:
showConfidentialModeContent = getBoolean(sys.argv[i+1], myarg)
i += 2
elif myarg in ['region']:
allowed_regions = _getEnumValuesMinusUnspecified(v._rootDesc['schemas']['ExportOptions']['properties']['region']['enum'])
body['exportOptions']['region'] = sys.argv[i+1].upper()
if body['exportOptions']['region'] not in allowed_regions:
systemErrorExit(3, 'region should be one of %s, got %s' % (', '.join(allowed_regions), body['exportOptions']['region']))
i += 2
elif myarg in ['includeaccessinfo']:
body['exportOptions'].setdefault('driveOptions', {})['includeAccessInfo'] = getBoolean(sys.argv[i+1], myarg)
i += 2
@@ -8310,6 +8417,8 @@ def doCreateVaultExport():
if options_field:
body['exportOptions'].pop('driveOptions', None)
body['exportOptions'][options_field] = {'exportFormat': export_format}
if showConfidentialModeContent is not None:
body['exportOptions'][options_field]['showConfidentialModeContent'] = showConfidentialModeContent
results = callGAPI(v.matters().exports(), 'create', matterId=matterId, body=body)
print('Created export %s' % results['id'])
print_json(None, results)
@@ -8328,6 +8437,75 @@ def doGetVaultExportInfo():
export = callGAPI(v.matters().exports(), 'get', matterId=matterId, exportId=exportId)
print_json(None, export)
def _getCloudStorageObject(s, bucket, object_, local_file=None, expectedMd5=None):
if not local_file:
local_file = object_
if os.path.exists(local_file):
sys.stdout.write(' File already exists. ')
sys.stdout.flush()
if expectedMd5:
sys.stdout.write('Verifying %s hash...' % expectedMd5)
sys.stdout.flush()
if md5MatchesFile(local_file, expectedMd5, False):
print('VERIFIED')
return
print('not verified. Downloading again and over-writing...')
else:
return # nothing to verify, just assume we're good.
print('saving to %s' % local_file)
request = s.objects().get_media(bucket=bucket, object=object_)
file_path = os.path.dirname(local_file)
if not os.path.exists(file_path):
os.makedirs(file_path)
f = openFile(local_file, 'wb')
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
done = False
while not done:
status, done = downloader.next_chunk()
sys.stdout.write(' Downloaded: {0:>7.2%}\r'.format(status.progress()))
sys.stdout.flush()
sys.stdout.write('\n Download complete. Flushing to disk...\n')
# Necessary to make sure file is flushed by both Python and OS
# https://stackoverflow.com/a/13762137/1503886
f.flush()
os.fsync(f.fileno())
closeFile(f)
if expectedMd5:
f = openFile(local_file, 'rb')
sys.stdout.write(' Verifying file hash is %s...' % expectedMd5)
sys.stdout.flush()
md5MatchesFile(local_file, expectedMd5, True)
print('VERIFIED')
closeFile(f)
def md5MatchesFile(local_file, expected_md5, exitOnError):
f = openFile(local_file, 'rb')
hash_md5 = hashlib.md5()
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
actual_hash = hash_md5.hexdigest()
if exitOnError and actual_hash != expected_md5:
systemErrorExit(6, 'actual hash was %s. Exiting on corrupt file.' % actual_hash)
return actual_hash == expected_md5
def doDownloadCloudStorageBucket():
bucket_url = sys.argv[3]
bucket_regex = r'(takeout-export-[a-f,0-9,-]*)'
bucket_match = re.search(bucket_regex, bucket_url)
if bucket_match:
bucket = bucket_match.group(1)
else:
systemErrorExit(5, 'Could not find a takeout-export-* bucket in that URL')
s = buildGAPIObject('storage')
page_message = 'Got %%total_items%% files...'
objects = callGAPIpages(s.objects(), 'list', 'items', page_message=page_message, bucket=bucket, projection='noAcl', fields='nextPageToken,items(name,id,md5Hash)')
i = 1
for object_ in objects:
print("%s/%s" % (i, len(objects)))
expectedMd5 = base64.b64decode(object_['md5Hash']).hex()
_getCloudStorageObject(s, bucket, object_['name'], expectedMd5=expectedMd5)
i += 1
def doDownloadVaultExport():
verifyFiles = True
extractFiles = True
@@ -8372,21 +8550,12 @@ def doDownloadVaultExport():
f.flush()
os.fsync(f.fileno())
closeFile(f)
f = openFile(filename, 'rb')
if verifyFiles:
expected_hash = s_file['md5Hash']
sys.stdout.write(' Verifying file hash is %s...' % expected_hash)
sys.stdout.flush()
hash_md5 = hashlib.md5()
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
actual_hash = hash_md5.hexdigest()
if actual_hash == expected_hash:
print('VERIFIED')
else:
print('ERROR: actual hash was %s. Exiting on corrupt file.' % actual_hash)
sys.exit(6)
closeFile(f)
md5MatchesFile(filename, expected_hash, True)
print('VERIFIED')
if extractFiles and re.search(r'\.zip$', filename):
extract_nested_zip(filename, targetFolder)
@@ -8710,21 +8879,11 @@ def doCreateUser():
def GroupIsAbuseOrPostmaster(emailAddr):
return emailAddr.startswith('abuse@') or emailAddr.startswith('postmaster@')
def mapCollaborativeACL(myarg, value):
value = value.lower().replace('_', '')
if value in COLLABORATIVE_ACL_CHOICES:
return COLLABORATIVE_ACL_CHOICES[value]
systemErrorExit(3, 'allowed choices for %s are %s, got %s' % (myarg, ', '.join(sorted(COLLABORATIVE_ACL_CHOICES)), value))
GROUP_SETTINGS_LIST_PATTERN = re.compile(r'([A-Z][A-Z_]+[A-Z]?)')
def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
if myarg == 'collaborative':
value = mapCollaborativeACL(myarg, value)
for attrName, attrValue in list(COLLABORATIVE_INBOX_ATTRIBUTES.items()):
if attrValue == 'acl':
gs_body[attrName] = value
else:
gs_body[attrName] = attrValue
return
myarg = 'enablecollaborativeinbox'
for (attrib, params) in list(gs_object['schemas']['Groups']['properties'].items()):
if attrib in ['kind', 'etag', 'email']:
continue
@@ -8746,19 +8905,19 @@ def getGroupAttrValue(myarg, value, gs_object, gs_body, function):
value = value.replace('\\n', '\n')
elif attrib == 'primaryLanguage':
value = LANGUAGE_CODES_MAP.get(value.lower(), value)
elif COLLABORATIVE_INBOX_ATTRIBUTES.get(attrib) == 'acl':
value = mapCollaborativeACL(myarg, value)
elif params['description'].find(value.upper()) != -1: # ugly hack because API wants some values uppercased.
elif attrib in GROUP_SETTINGS_LIST_ATTRIBUTES:
value = value.upper()
elif value.lower() in true_values:
value = 'true'
elif value.lower() in false_values:
value = 'false'
# Another ugly hack because Groups Settings API doesn't have proper enumerator values set in discovery file.
if 'description' in params and params['description'].find('Possible values are: ') != -1:
possible_values = params['description'][params['description'].find('Possible values are: ')+21:].split(' ')
if value not in possible_values:
systemErrorExit(2, 'value for %s must be one of %s. Got %s.' % (attrib, ', '.join(possible_values), value))
possible_values = GROUP_SETTINGS_LIST_PATTERN.findall(params['description'])
if value not in possible_values:
systemErrorExit(2, 'value for %s must be one of %s. Got %s.' % (attrib, ', '.join(possible_values), value))
elif attrib in GROUP_SETTINGS_BOOLEAN_ATTRIBUTES:
value = value.lower()
if value in true_values:
value = 'true'
elif value in false_values:
value = 'false'
else:
systemErrorExit(2, 'value for %s must be true|false. Got %s.' % (attrib, value))
gs_body[attrib] = value
return
systemErrorExit(2, '%s is not a valid argument for "gam %s group"' % (myarg, function))
@@ -8806,12 +8965,12 @@ def doCreateGroup():
if gs and not GroupIsAbuseOrPostmaster(body['email']):
if gs_get_before_update:
current_settings = callGAPI(gs.groups(), 'get',
retry_reasons=['serviceLimit'],
retry_reasons=['serviceLimit', 'notFound'],
groupUniqueId=body['email'], fields='*')
if current_settings is not None:
gs_body = dict(list(current_settings.items()) + list(gs_body.items()))
if gs_body:
callGAPI(gs.groups(), 'update', retry_reasons=['serviceLimit'], groupUniqueId=body['email'], body=gs_body)
callGAPI(gs.groups(), 'update', retry_reasons=['serviceLimit', 'notFound'], groupUniqueId=body['email'], body=gs_body)
def doCreateAlias():
cd = buildGAPIObject('directory')
@@ -9149,6 +9308,16 @@ def checkGroupExists(cd, group, i=0, count=0):
entityUnknownWarning('Group', group, i, count)
return None
def _checkMemberRoleIsSuspended(member, validRoles, isSuspended):
if validRoles and member.get('role', ROLE_MEMBER) not in validRoles:
return False
if isSuspended is None:
return True
memberStatus = member.get('status', 'UNKNOWN')
if not isSuspended:
return memberStatus != 'SUSPENDED'
return memberStatus == 'SUSPENDED'
UPDATE_GROUP_SUBCMDS = ['add', 'clear', 'delete', 'remove', 'sync', 'update']
GROUP_ROLES_MAP = {
'owner': ROLE_OWNER, 'owners': ROLE_OWNER,
@@ -9353,12 +9522,7 @@ def doUpdateGroup():
if not result:
print('Group already has 0 members')
return
if checkSuspended is None:
users_email = [member.get('email', member['id']) for member in result if not validRoles or member.get('role', ROLE_MEMBER) in validRoles]
elif checkSuspended:
users_email = [member.get('email', member['id']) for member in result if (not validRoles or member.get('role', ROLE_MEMBER) in validRoles) and member['status'] == 'SUSPENDED']
else: # elif not checkSuspended
users_email = [member.get('email', member['id']) for member in result if (not validRoles or member.get('role', ROLE_MEMBER) in validRoles) and member['status'] != 'SUSPENDED']
users_email = [member.get('email', member['id']) for member in result if _checkMemberRoleIsSuspended(member, validRoles, checkSuspended)]
if len(users_email) > 1:
sys.stderr.write('Group: {0}, Will remove {1} {2}{3}s.\n'.format(group, len(users_email), '' if checkSuspended is None else ['Non-suspended ', 'Suspended '][checkSuspended], roles))
for user_email in users_email:
@@ -9528,11 +9692,21 @@ def doUpdateCros():
def doUpdateMobile():
cd = buildGAPIObject('directory')
resourceId = sys.argv[3]
resourceIds = sys.argv[3]
match_users = None
doit = False
if resourceIds[:6] == 'query:':
query = resourceIds[6:]
fields = 'nextPageToken,mobiledevices(resourceId,email)'
page_message = 'Got %%total_items%% mobile devices...\n'
devices = callGAPIpages(cd.mobiledevices(), 'list', page_message=page_message, customerId=GC_Values[GC_CUSTOMER_ID], items='mobiledevices', query=query, fields=fields)
else:
devices = [{'resourceId': resourceIds, 'email': ['not set']}]
doit = True
i = 4
body = {}
while i < len(sys.argv):
myarg = sys.argv[i].lower()
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'action':
body['action'] = sys.argv[i+1].lower()
if body['action'] == 'wipe':
@@ -9542,10 +9716,29 @@ def doUpdateMobile():
if body['action'] not in ['admin_remote_wipe', 'admin_account_wipe', 'approve', 'block', 'cancel_remote_wipe_then_activate', 'cancel_remote_wipe_then_block']:
systemErrorExit(2, 'action must be one of wipe, wipeaccount, approve, block, cancel_remote_wipe_then_activate, cancel_remote_wipe_then_block; got %s' % body['action'])
i += 2
elif myarg in ['ifusers', 'matchusers']:
match_users = getUsersToModify(entity_type=sys.argv[i+1].lower(), entity=sys.argv[i+2])
i += 3
elif myarg == 'doit':
doit = True
i += 1
else:
systemErrorExit(2, '%s is not a valid argument for "gam update mobile"' % sys.argv[i])
if body:
callGAPI(cd.mobiledevices(), 'action', resourceId=resourceId, body=body, customerId=GC_Values[GC_CUSTOMER_ID])
if doit:
print('Updating %s devices' % len(devices))
describe_as = 'Performing'
else:
print('Showing %s changes that would be made, not actually making changes because doit argument not specified' % len(devices))
describe_as = 'Would perform'
for device in devices:
device_user = device.get('email', [''])[0]
if match_users and device_user not in match_users:
print('Skipping device for user %s that did not match match_users argument' % device_user)
else:
print('%s %s on user %s device %s' % (describe_as, body['action'], device_user, device['resourceId']))
if doit:
callGAPI(cd.mobiledevices(), 'action', resourceId=device['resourceId'], body=body, customerId=GC_Values[GC_CUSTOMER_ID])
def doDeleteMobile():
cd = buildGAPIObject('directory')
@@ -10469,27 +10662,42 @@ def doSiteVerifyAttempt():
print('ERROR: %s' % str(e))
verify_data = callGAPI(verif.webResource(), 'getToken', body=body)
print('Method: %s' % verify_data['method'])
print('Token: %s' % verify_data['token'])
print('Expected Token: %s' % verify_data['token'])
if verify_data['method'] in ['DNS_CNAME', 'DNS_TXT']:
resolver = dns.resolver.Resolver()
resolver.nameservers = ['8.8.8.8', '8.8.4.4']
simplehttp = _createHttpObj()
base_url = 'https://dns.google/resolve?'
query_params = {}
if verify_data['method'] == 'DNS_CNAME':
cname_token = verify_data['token']
cname_list = cname_token.split(' ')
cname_subdomain = cname_list[0]
try:
answers = resolver.query('%s.%s' % (cname_subdomain, a_domain), 'A')
for answer in answers:
print('DNS Record: %s' % answer)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
print('ERROR: No such domain found in DNS!')
query_params['name'] = '%s.%s' % (cname_subdomain, a_domain)
query_params['type'] = 'cname'
else:
try:
answers = resolver.query(a_domain, 'TXT')
for answer in answers:
print('DNS Record: %s' % str(answer).replace('"', ''))
except dns.resolver.NXDOMAIN:
print('ERROR: no such domain found in DNS!')
query_params['name'] = a_domain
query_params['type'] = 'txt'
full_url = base_url + urlencode(query_params)
(_, c) = simplehttp.request(full_url, 'GET')
result = json.loads(c.decode(UTF8))
status = result['Status']
if status == 0 and 'Answer' in result:
answers = result['Answer']
if verify_data['method'] == 'DNS_CNAME':
answer = answers[0]['data']
else:
answer = 'no matching record found'
for possible_answer in answers:
possible_answer['data'] = possible_answer['data'].strip('"')
if possible_answer['data'].startswith('google-site-verification'):
answer = possible_answer['data']
break
else:
print('Unrelated TXT record: %s' % possible_answer['data'])
print('Found DNS Record: %s' % answer)
elif status == 0:
systemErrorExit(1, 'DNS record not found')
else:
systemErrorExit(status, DNS_ERROR_CODES_MAP.get(status, 'Unknown error %s' % status))
return
print('SUCCESS!')
print('Verified: %s' % verify_result['site']['identifier'])
@@ -10928,30 +11136,42 @@ def doDeleteOrg():
print("Deleting organization %s" % name)
callGAPI(cd.orgunits(), 'delete', customerId=GC_Values[GC_CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(name)))
def send_email(subject, body, recipient=None, sender=None, method='send', labels=None):
def send_email(subject, body, recipient=None, sender=None, user=None, method='send', labels=None, msgHeaders={}, kwargs={}):
api_body = {}
kwargs = {}
if not sender:
sender = _getValueFromOAuth('email')
userId, gmail = buildGmailGAPIObject(sender)
default_sender = default_recipient = False
if not user:
user = _getValueFromOAuth('email')
userId, gmail = buildGmailGAPIObject(user)
resource = gmail.users().messages()
if labels and method in ['insert', 'import']:
if labels:
api_body['labelIds'] = labelsToLabelIds(gmail, labels)
elif labels:
systemErrorExit(3, 'labels argument is only valid for importemail and insertemail')
if not sender:
sender = userId
default_sender = True
if not recipient:
recipient = userId
default_recipient = True
msg = message_from_string(body)
msg['Subject'] = subject
msg['From'] = userId
msg['To'] = recipient
for header, value in msgHeaders.items():
msg.__delitem__(header) # can remove multiple case-insensitive matching headers
msg.add_header(header, value)
if subject:
msg.__delitem__('Subject')
msg['Subject'] = subject
if not default_sender:
msg.__delitem__('From')
if not msg['From']:
msg['From'] = sender
if not default_recipient:
msg.__delitem__('to')
if not msg['To']:
msg['To'] = recipient
api_body['raw'] = base64.urlsafe_b64encode(msg.as_bytes()).decode()
if method == 'draft':
resource = gmail.users().drafts()
method = 'create'
api_body = {'message': api_body}
elif method in ['insert', 'import']:
kwargs['internalDateSource'] = 'dateHeader'
if method == 'import':
method = 'import_'
callGAPI(resource, method, userId=userId, body=api_body, **kwargs)
@@ -11016,6 +11236,9 @@ def sortCSVTitles(firstTitle, titles):
for title in restoreTitles[::-1]:
titles.insert(0, title)
def QuotedArgumentList(items):
return ' '.join([item if item and (item.find(' ') == -1) and (item.find(',') == -1) else '"'+item+'"' for item in items])
def writeCSVfile(csvRows, titles, list_type, todrive):
def rowDateTimeFilterMatch(dateMode, rowDate, op, filterDate):
if not rowDate or not isinstance(rowDate, str):
@@ -11120,7 +11343,7 @@ and follow recommend steps to authorize GAM for Drive access.''' % (admin_email)
mimeType = 'text/csv'
else:
mimeType = MIMETYPE_GA_SPREADSHEET
body = {'description': ' '.join(sys.argv),
body = {'description': QuotedArgumentList(sys.argv),
'name': '%s - %s' % (GC_Values[GC_DOMAIN], list_type),
'mimeType': mimeType}
result = callGAPI(drive.files(), 'create', fields='webViewLink',
@@ -11384,12 +11607,12 @@ def doPrintShowAlertFeedback():
for feedbac in feedback:
print(feedbac)
def _getValidAlertFeedbackTypes(ac):
return [aftype for aftype in ac._rootDesc['schemas']['AlertFeedback']['properties']['type']['enum'] if aftype != 'ALERT_FEEDBACK_TYPE_UNSPECIFIED']
def _getEnumValuesMinusUnspecified(values):
return [a_type for a_type in values if '_UNSPECIFIED' not in a_type]
def doCreateAlertFeedback():
_, ac = buildAlertCenterGAPIObject(_getValueFromOAuth('email'))
valid_types = _getValidAlertFeedbackTypes(ac)
valid_types = _getEnumValuesMinusUnspecified(ac._rootDesc['schemas']['AlertFeedback']['properties']['type']['enum'])
alertId = sys.argv[3]
body = {'type': sys.argv[4].upper()}
if body['type'] not in valid_types:
@@ -11416,11 +11639,14 @@ GROUP_ARGUMENT_TO_PROPERTY_TITLE_MAP = {
GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP = {
'allowexternalmembers': 'allowExternalMembers',
'allowgooglecommunication': 'allowGoogleCommunication',
'allowwebposting': 'allowWebPosting',
'archiveonly': 'archiveOnly',
'customfootertext': 'customFooterText',
'customreplyto': 'customReplyTo',
'defaultmessagedenynotificationtext': 'defaultMessageDenyNotificationText',
'enablecollaborativeinbox': 'enableCollaborativeInbox',
'favoriterepliesontop': 'favoriteRepliesOnTop',
'gal': 'includeInGlobalAddressList',
'includecustomfooter': 'includeCustomFooter',
'includeinglobaladdresslist': 'includeInGlobalAddressList',
@@ -11433,16 +11659,33 @@ GROUP_ATTRIBUTES_ARGUMENT_TO_PROPERTY_MAP = {
'showingroupdirectory': 'showInGroupDirectory',
'spammoderationlevel': 'spamModerationLevel',
'whocanadd': 'whoCanAdd',
'whocanapprovemembers': 'whoCanApproveMembers',
'whocanapprovemessages': 'whoCanApproveMessages',
'whocanassigntopics': 'whoCanAssignTopics',
'whocanassistcontent': 'whoCanAssistContent',
'whocanbanusers': 'whoCanBanUsers',
'whocancontactowner': 'whoCanContactOwner',
'whocandeleteanypost': 'whoCanDeleteAnyPost',
'whocandeletetopics': 'whoCanDeleteTopics',
'whocandiscovergroup': 'whoCanDiscoverGroup',
'whocanenterfreeformtags': 'whoCanEnterFreeFormTags',
'whocanhideabuse': 'whoCanHideAbuse',
'whocaninvite': 'whoCanInvite',
'whocanjoin': 'whoCanJoin',
'whocanleavegroup': 'whoCanLeaveGroup',
'whocanlocktopics': 'whoCanLockTopics',
'whocanmaketopicssticky': 'whoCanMakeTopicsSticky',
'whocanmarkduplicate': 'whoCanMarkDuplicate',
'whocanmarkfavoritereplyonanytopic': 'whoCanMarkFavoriteReplyOnAnyTopic',
'whocanmarkfavoritereplyonowntopic': 'whoCanMarkFavoriteReplyOnOwnTopic',
'whocanmarknoresponseneeded': 'whoCanMarkNoResponseNeeded',
'whocanmoderatecontent': 'whoCanModerateContent',
'whocanmoderatemembers': 'whoCanModerateMembers',
'whocanmodifymembers': 'whoCanModifyMembers',
'whocanmodifytagsandcategories': 'whoCanModifyTagsAndCategories',
'whocanmovetopicsin': 'whoCanMoveTopicsIn',
'whocanmovetopicsout': 'whoCanMoveTopicsOut',
'whocanpostannouncements': 'whoCanPostAnnouncements',
'whocanpostmessage': 'whoCanPostMessage',
'whocantaketopics': 'whoCanTakeTopics',
'whocanunassigntopic': 'whoCanUnassignTopic',
@@ -11868,8 +12111,7 @@ def doPrintGroupMembers():
soft_errors=True,
groupKey=group_email, roles=listRoles, fields=listFields, maxResults=GC_Values[GC_MEMBER_MAX_RESULTS])
for member in group_members:
if ((validRoles and member.get('role', ROLE_MEMBER) not in validRoles) or
(checkSuspended is not None and ((not checkSuspended and member['status'] == 'SUSPENDED') or (checkSuspended and member['status'] != 'SUSPENDED')))):
if not _checkMemberRoleIsSuspended(member, validRoles, checkSuspended):
continue
for title in member:
if title not in titles:
@@ -12671,12 +12913,16 @@ def doPrintResourceCalendars():
fieldsTitles = {}
titles = []
csvRows = []
query = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg == 'todrive':
todrive = True
i += 1
elif myarg == 'query':
query = sys.argv[i+1]
i += 2
elif myarg == 'allfields':
fieldsList = []
fieldsTitles = {}
@@ -12699,7 +12945,8 @@ def doPrintResourceCalendars():
page_message = 'Got %%total_items%% Resource Calendars: %%first_item%% - %%last_item%%\n'
resources = callGAPIpages(cd.resources().calendars(), 'list', 'items',
page_message=page_message, message_attribute='resourceId',
customer=GC_Values[GC_CUSTOMER_ID], fields=fields)
customer=GC_Values[GC_CUSTOMER_ID], query=query,
fields=fields)
for resource in resources:
if 'featureInstances' in resource:
resource['featureInstances'] = ','.join([a_feature['feature']['name'] for a_feature in resource.pop('featureInstances')])
@@ -12761,9 +13008,7 @@ def getUsersToModify(entity_type=None, entity=None, silent=False, member_type=No
groupKey=group, roles=listRoles, fields=listFields, maxResults=GC_Values[GC_MEMBER_MAX_RESULTS])
users = []
for member in members:
if (((not groupUserMembersOnly) or (member['type'] == 'USER')) and
(not validRoles or member.get('role', ROLE_MEMBER) in validRoles) and
(checkSuspended is None or (not checkSuspended and member['status'] != 'SUSPENDED') or (checkSuspended and member['status'] == 'SUSPENDED'))):
if ((not groupUserMembersOnly) or (member['type'] == 'USER')) and _checkMemberRoleIsSuspended(member, validRoles, checkSuspended):
users.append(member.get('email', member['id']))
elif entity_type in ['ou', 'org', 'ou_ns', 'org_ns', 'ou_susp', 'org_susp',]:
if entity_type in ['ou_ns', 'org_ns']:
@@ -13841,6 +14086,8 @@ def ProcessGAMCommand(args):
doCreateFeature()
elif argument in ['alertfeedback']:
doCreateAlertFeedback()
elif argument in ['gcpfolder']:
createGCPFolder()
else:
systemErrorExit(2, '%s is not a valid argument for "gam create"' % argument)
sys.exit(0)
@@ -14198,6 +14445,8 @@ def ProcessGAMCommand(args):
argument = sys.argv[2].lower()
if argument in ['export', 'vaultexport']:
doDownloadVaultExport()
elif argument in ['storagebucket']:
doDownloadCloudStorageBucket()
else:
systemErrorExit(2, '%s is not a valid argument for "gam download"' % argument)
sys.exit(0)

View File

@@ -1,6 +1,5 @@
python-dateutil
dnspython
google-api-python-client
google-api-python-client>=1.7.10
google-auth
google-auth-httplib2
google-auth-oauthlib==0.4.0

View File

@@ -10,7 +10,7 @@ else
echo "RUNNING: apt update..."
sudo apt-get -qq --yes update > /dev/null
echo "RUNNING: apt dist-upgrade..."
sudo apt-get -qq --yes dist-upgrade > /dev/null
# sudo apt-get -qq --yes dist-upgrade > /dev/null
echo "Installing build tools..."
sudo apt-get -qq --yes install build-essential
@@ -47,28 +47,38 @@ else
cd ~/pybuild
# Compile latest Python
if [ ! -d Python-$BUILD_PYTHON_VERSION ]; then
wget --quiet https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/Python-$BUILD_PYTHON_VERSION.tar.xz
echo "Downloading Python $BUILD_PYTHON_VERSION..."
curl -O https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/Python-$BUILD_PYTHON_VERSION.tar.xz
echo "Extracting Python..."
tar xf Python-$BUILD_PYTHON_VERSION.tar.xz
fi
cd Python-$BUILD_PYTHON_VERSION
#if [[ "$dist" == "bionic" ]]; then
# echo "running bionic make clean"
# make clean
# rm Makefile
#fi
echo "Compiling Python $BUILD_PYTHON_VERSION..."
safe_flags="--with-openssl=$mypath/ssl --enable-shared --prefix=$mypath/python --with-ensurepip=upgrade"
unsafe_flags="--enable-optimizations --with-lto"
if [ ! -e Makefile ]; then
echo "running configure with safe and unsafe"
./configure $safe_flags $unsafe_flags > /dev/null
fi
make -j$cpucount -s
timeout 1800 make -j$cpucount -s
RESULT=$?
echo "First make exited with $RESULT"
if [ $RESULT != 0 ]; then
echo "Trying Python compile again without unsafe flags..."
make clean
./configure $safe_flags > /dev/null
make -j$cpucount -s
#echo "Trying Python compile again without unsafe flags..."
#make clean
#./configure $safe_flags > /dev/null
#make -j$cpucount -s
echo "Sticking with safe Python for now..."
else
echo "Installing optimized Python..."
make install > /dev/null
fi
echo "Installing Python..."
make install > /dev/null
cd ~
export LD_LIBRARY_PATH=~/ssl/lib:~/python/lib
@@ -100,11 +110,11 @@ else
fi
$pip install git+https://github.com/JonathonReinhart/staticx.git@master
fi
$pip install pyinstaller
cd $whereibelong
fi
echo "Upgrading pip packages..."
$pip freeze > upgrades.txt
$pip install --upgrade -r upgrades.txt
$pip install -r src/requirements.txt
$pip install --upgrade -r src/requirements.txt
$pip install --upgrade pyinstaller

View File

@@ -3,7 +3,7 @@ if [ "$VMTYPE" == "test" ]; then
export gam="$python gam.py"
export gampath=$(readlink -e .)
else
$python -OO -m PyInstaller --clean --debug -F --distpath=gam $GAMOS-gam.spec
$python -OO -m PyInstaller --clean --noupx --strip -F --distpath=gam $GAMOS-gam.spec
export gam="gam/gam"
export gampath=$(readlink -e gam)
export GAMVERSION=`$gam version simple`

View File

@@ -1,54 +1,63 @@
mypath=$HOME
whereibelong=$(pwd)
echo "Brew installing xz..."
brew install xz > /dev/null
#echo "Brew installing xz..."
#brew install xz > /dev/null
cd ~/pybuild
# Compile latest OpenSSL
if [ ! -d openssl-$BUILD_OPENSSL_VERSION ]; then
wget --quiet https://www.openssl.org/source/openssl-$BUILD_OPENSSL_VERSION.tar.gz
echo "Extracting OpenSSL..."
tar xf openssl-$BUILD_OPENSSL_VERSION.tar.gz
if [ ! -f python-$BUILD_PYTHON_VERSION-macosx10.9.pkg ]; then
wget --quiet https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/python-$BUILD_PYTHON_VERSION-macosx10.9.pkg
fi
cd openssl-$BUILD_OPENSSL_VERSION
echo "Compiling OpenSSL $BUILD_OPENSSL_VERSION..."
./config shared --prefix=$mypath/ssl
echo "Running make for OpenSSL..."
make -j$cpucount -s
echo "Running make install for OpenSSL..."
make install > /dev/null
export LD_LIBRARY_PATH=~/ssl/lib
cd ~/pybuild
sudo installer -pkg python-$BUILD_PYTHON_VERSION-macosx10.9.pkg -target /
export python=python3
export pip=pip3
# Compile latest OpenSSL
#if [ ! -d openssl-$BUILD_OPENSSL_VERSION ]; then
# wget --quiet https://www.openssl.org/source/openssl-$BUILD_OPENSSL_VERSION.tar.gz
# echo "Extracting OpenSSL..."
# tar xf openssl-$BUILD_OPENSSL_VERSION.tar.gz
#fi
#cd openssl-$BUILD_OPENSSL_VERSION
#echo "Compiling OpenSSL $BUILD_OPENSSL_VERSION..."
#./config shared --prefix=$mypath/ssl
#echo "Running make for OpenSSL..."
#make -j$cpucount -s
#echo "Running make install for OpenSSL..."
#make install > /dev/null
#export LD_LIBRARY_PATH=~/ssl/lib
#cd ~/pybuild
# Compile latest Python
if [ ! -d Python-$BUILD_PYTHON_VERSION ]; then
wget --quiet https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/Python-$BUILD_PYTHON_VERSION.tar.xz
echo "Extracting Python..."
tar xf Python-$BUILD_PYTHON_VERSION.tar.xz
fi
cd Python-$BUILD_PYTHON_VERSION
echo "Compiling Python $BUILD_PYTHON_VERSION..."
safe_flags="--with-openssl=$mypath/ssl --enable-shared --prefix=$mypath/python --with-ensurepip=upgrade"
unsafe_flags="--enable-optimizations --with-lto"
if [ ! -e Makefile ]; then
./configure $safe_flags $unsafe_flags > /dev/null
fi
make -j$cpucount -s
RESULT=$?
echo "Make Python exited with $RESULT"
if [ $RESULT != 0 ]; then
echo "Trying Python make again without unsafe flags..."
make clean
./configure $safe_flags > /dev/null
make -j$cpucount -s
fi
echo "Installing Python..."
make install > /dev/null
cd ~
#if [ ! -d Python-$BUILD_PYTHON_VERSION ]; then
# wget --quiet https://www.python.org/ftp/python/$BUILD_PYTHON_VERSION/Python-$BUILD_PYTHON_VERSION.tar.xz
# echo "Extracting Python..."
# tar xf Python-$BUILD_PYTHON_VERSION.tar.xz
#fi
#cd Python-$BUILD_PYTHON_VERSION
#echo "Compiling Python $BUILD_PYTHON_VERSION..."
#safe_flags="--with-openssl=$mypath/ssl --enable-shared --prefix=$mypath/python --with-ensurepip=upgrade"
#unsafe_flags="--enable-optimizations --with-lto"
#if [ ! -e Makefile ]; then
# ./configure $safe_flags $unsafe_flags > /dev/null
#fi
#make -j$cpucount -s
#RESULT=$?
#echo "Make Python exited with $RESULT"
#if [ $RESULT != 0 ]; then
# echo "Trying Python make again without unsafe flags..."
# make clean
# ./configure $safe_flags > /dev/null
# make -j$cpucount -s
#fi
#echo "Installing Python..."
#make install > /dev/null
#cd ~
#export LD_LIBRARY_PATH=~/ssl/lib:~/python/lib
#python=~/python/bin/python3
#pip=~/python/bin/pip3
export LD_LIBRARY_PATH=~/ssl/lib:~/python/lib
python=~/python/bin/python3
pip=~/python/bin/pip3
$python -V
@@ -58,5 +67,5 @@ export PATH=/usr/local/opt/python/libexec/bin:$PATH
$pip install --upgrade pip
$pip freeze > upgrades.txt
$pip install --upgrade -r upgrades.txt
$pip install -r src/requirements.txt
$pip install pyinstaller
$pip install --upgrade -r src/requirements.txt
$pip install --upgrade pyinstaller

View File

@@ -1,5 +1,5 @@
cd src
$python -OO -m PyInstaller --clean --debug -F --distpath=gam $GAMOS-gam.spec
$python -OO -m PyInstaller --clean --noupx --strip -F --distpath=gam $GAMOS-gam.spec
export gam="gam/gam"
export gampath=gam
$gam version extended

View File

@@ -2,26 +2,44 @@ echo "Installing Net-Framework-Core..."
export mypath=$(pwd)
until powershell Install-WindowsFeature Net-Framework-Core; do echo "trying again..."; done
cd ~/pybuild
export exefile=Win32OpenSSL_Light-${BUILD_OPENSSL_VERSION//./_}.exe
if [ ! -e $exefile ]; then
echo "Downloading $exefile..."
wget --quiet https://slproweb.com/download/$exefile
fi
echo "Installing $exefile..."
powershell ".\\${exefile} /silent /sp- /suppressmsgboxes /DIR=C:\\ssl"
#export exefile=Win32OpenSSL_Light-${BUILD_OPENSSL_VERSION//./_}.exe
#if [ ! -e $exefile ]; then
# echo "Downloading $exefile..."
# wget --quiet https://slproweb.com/download/$exefile
#fi
#echo "Installing $exefile..."
#powershell ".\\${exefile} /silent /sp- /suppressmsgboxes /DIR=C:\\ssl"
cinst -y --forcex86 python3
until cinst -y wixtoolset; do echo "trying again..."; done
echo "OpenSSL dlls..."
ls -alRF /c/ssl
echo "c drive"
ls -al /c/
echo "Python dlls..."
ls -al /c/Python37/DLLs
until cp -v /c/ssl/*.dll /c/Python37/DLLs; do echo "trying again..."; done
#echo "OpenSSL dlls..."
#ls -alRF /c/ssl
#echo "c drive"
#ls -al /c/
#echo "Python dlls..."
#ls -al /c/Python37/DLLs
#until cp -v /c/ssl/*.dll /c/Python37/DLLs; do echo "trying again..."; done
export PATH=$PATH:/c/Python37/scripts
cd $mypath
pip install --upgrade pip
pip freeze > upgrades.txt
pip install --upgrade -r upgrades.txt
pip install -r src/requirements.txt
pip install pyinstaller
pip install --upgrade -r src/requirements.txt
#pip install --upgrade pyinstaller
# Install PyInstaller from source and build bootloader
# to try and avoid getting flagged as malware since
# lots of malware uses PyInstaller default bootloader
# https://stackoverflow.com/questions/53584395/how-to-recompile-the-bootloader-of-pyinstaller
echo "Downloading PyInstaller..."
wget --quiet https://github.com/pyinstaller/pyinstaller/releases/download/v$PYINSTALLER_VERSION/PyInstaller-$PYINSTALLER_VERSION.tar.gz
tar xf PyInstaller-$PYINSTALLER_VERSION.tar.gz
cd PyInstaller-$PYINSTALLER_VERSION/bootloader
echo "bootloader before:"
md5sum ../PyInstaller/bootloader/Windows-32bit/*
/c/python37/python ./waf all --target-arch=32bit
echo "bootloader after:"
md5sum ../PyInstaller/bootloader/Windows-32bit/*
echo "PATH: $PATH"
cd ..
/c/python37/python setup.py install
cd $mypath

View File

@@ -1,5 +1,5 @@
cd src
pyinstaller --clean -F --distpath=gam $GAMOS-gam.spec
pyinstaller --clean --noupx -F --distpath=gam $GAMOS-gam.spec
export gam="gam/gam"
export gampath=$(readlink -e gam)
$gam version extended

View File

@@ -2,26 +2,43 @@ echo "Installing Net-Framework-Core..."
export mypath=$(pwd)
until powershell Install-WindowsFeature Net-Framework-Core; do echo "trying again..."; done
cd ~/pybuild
export exefile=Win64OpenSSL_Light-${BUILD_OPENSSL_VERSION//./_}.exe
if [ ! -e $exefile ]; then
echo "Downloading $exefile..."
wget --quiet https://slproweb.com/download/$exefile
fi
echo "Installing $exefile..."
powershell ".\\${exefile} /silent /sp- /suppressmsgboxes /DIR=C:\\ssl"
#export exefile=Win64OpenSSL_Light-${BUILD_OPENSSL_VERSION//./_}.exe
#if [ ! -e $exefile ]; then
# echo "Downloading $exefile..."
# wget --quiet https://slproweb.com/download/$exefile
#fi
#echo "Installing $exefile..."
#powershell ".\\${exefile} /silent /sp- /suppressmsgboxes /DIR=C:\\ssl"
cinst -y python3
until cinst -y wixtoolset; do echo "trying again..."; done
echo "OpenSSL dlls..."
ls -alRF /c/ssl
echo "c drive"
ls -al /c
echo "Python dlls..."
ls -al /c/Python37/DLLs
until cp -v /c/ssl/*.dll /c/Python37/DLLs; do echo "trying again..."; done
#echo "OpenSSL dlls..."
#ls -alRF /c/ssl
#echo "c drive"
#ls -al /c
#echo "Python dlls..."
#ls -al /c/Python37/DLLs
#until cp -v /c/ssl/*.dll /c/Python37/DLLs; do echo "trying again..."; done
export PATH=$PATH:/c/Python37/scripts
cd $mypath
pip install --upgrade pip
pip freeze > upgrades.txt
pip install --upgrade -r upgrades.txt
pip install -r src/requirements.txt
pip install pyinstaller
pip install --upgrade -r src/requirements.txt
#pip install --upgrade pyinstaller
# Install PyInstaller from source and build bootloader
# to try and avoid getting flagged as malware since
# lots of malware uses PyInstaller default bootloader
# https://stackoverflow.com/questions/53584395/how-to-recompile-the-bootloader-of-pyinstaller
echo "Downloading PyInstaller..."
wget --quiet https://github.com/pyinstaller/pyinstaller/releases/download/v$PYINSTALLER_VERSION/PyInstaller-$PYINSTALLER_VERSION.tar.gz
tar xf PyInstaller-$PYINSTALLER_VERSION.tar.gz
cd PyInstaller-$PYINSTALLER_VERSION/bootloader
echo "bootloader before:"
md5sum ../PyInstaller/bootloader/Windows-64bit/*
/c/python37/python ./waf all --target-arch=64bit
echo "bootloader after:"
md5sum ../PyInstaller/bootloader/Windows-64bit/*
echo "PATH: $PATH"
cd ..
/c/python37/python setup.py install
cd $mypath

View File

@@ -1,5 +1,5 @@
cd src
pyinstaller --clean -F --distpath=gam $GAMOS-gam.spec
pyinstaller --clean --noupx -F --distpath=gam $GAMOS-gam.spec
export gam="gam/gam"
export gampath=$(readlink -e gam)
$gam version

View File

@@ -6,15 +6,15 @@ import platform
import re
gam_author = 'Jay Lee <jay0lee@gmail.com>'
gam_version = '4.88'
gam_version = '4.92'
gam_license = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://git.io/gam'
GAM_INFO = 'GAM {0} - {1} / {2} / Python {3}.{4}.{5} {6} / {7} {8} /'.format(gam_version, GAM_URL,
gam_author,
sys.version_info[0], sys.version_info[1],
sys.version_info[2], sys.version_info[3],
platform.platform(), platform.machine())
gam_author,
sys.version_info[0], sys.version_info[1],
sys.version_info[2], sys.version_info[3],
platform.platform(), platform.machine())
GAM_RELEASES = 'https://github.com/jay0lee/GAM/releases'
GAM_WIKI = 'https://github.com/jay0lee/GAM/wiki'
@@ -157,13 +157,13 @@ API_VER_MAPPING = {
API_SCOPE_MAPPING = {
'alertcenter': ['https://www.googleapis.com/auth/apps.alerts',],
'appsactivity': ['https://www.googleapis.com/auth/activity',
'https://www.googleapis.com/auth/drive',],
'https://www.googleapis.com/auth/drive',],
'calendar': ['https://www.googleapis.com/auth/calendar',],
'drive': ['https://www.googleapis.com/auth/drive',],
'drive3': ['https://www.googleapis.com/auth/drive',],
'gmail': ['https://mail.google.com/',
'https://www.googleapis.com/auth/gmail.settings.basic',
'https://www.googleapis.com/auth/gmail.settings.sharing',],
'https://www.googleapis.com/auth/gmail.settings.basic',
'https://www.googleapis.com/auth/gmail.settings.sharing',],
'sheets': ['https://www.googleapis.com/auth/spreadsheets',],
}
@@ -403,7 +403,7 @@ DOCUMENT_FORMATS_MAP = {
'mht': [{'mime': 'message/rfc822', 'ext': 'mht'}],
'odp': [{'mime': 'application/vnd.oasis.opendocument.presentation', 'ext': '.odp'}],
'ods': [{'mime': 'application/x-vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
{'mime': 'application/vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'}],
{'mime': 'application/vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'}],
'odt': [{'mime': 'application/vnd.oasis.opendocument.text', 'ext': '.odt'}],
'pdf': [{'mime': 'application/pdf', 'ext': '.pdf'}],
'png': [{'mime': 'image/png', 'ext': '.png'}],
@@ -414,7 +414,7 @@ DOCUMENT_FORMATS_MAP = {
'rtf': [{'mime': 'application/rtf', 'ext': '.rtf'}],
'svg': [{'mime': 'image/svg+xml', 'ext': '.svg'}],
'tsv': [{'mime': 'text/tab-separated-values', 'ext': '.tsv'},
{'mime': 'text/tsv', 'ext': '.tsv'}],
{'mime': 'text/tsv', 'ext': '.tsv'}],
'txt': [{'mime': 'text/plain', 'ext': '.txt'}],
'xls': [{'mime': 'application/vnd.ms-excel', 'ext': '.xls'}],
'xlt': [{'mime': 'application/vnd.ms-excel', 'ext': '.xlt'}],
@@ -425,9 +425,21 @@ DOCUMENT_FORMATS_MAP = {
'microsoft': _MICROSOFT_FORMATS_LIST,
'micro$oft': _MICROSOFT_FORMATS_LIST,
'openoffice': [{'mime': 'application/vnd.oasis.opendocument.presentation', 'ext': '.odp'},
{'mime': 'application/x-vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
{'mime': 'application/vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
{'mime': 'application/vnd.oasis.opendocument.text', 'ext': '.odt'}],
{'mime': 'application/x-vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
{'mime': 'application/vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
{'mime': 'application/vnd.oasis.opendocument.text', 'ext': '.odt'}],
}
DNS_ERROR_CODES_MAP = {
1: 'DNS Query Format Error',
2: 'Server failed to complete the DNS request',
3: 'Domain name does not exist',
4: 'Function not implemented',
5: 'The server refused to answer for the query',
6: 'Name that should not exist, does exist',
7: 'RRset that should not exist, does exist',
8: 'Server not authoritative for the zone',
9: 'Name not in zone'
}
EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP = {
@@ -600,28 +612,76 @@ CROS_END_ARGUMENTS = ['end', 'enddate']
CROS_TPM_VULN_VERSIONS = ['41f', '420', '628', '8520',]
CROS_TPM_FIXED_VERSIONS = ['422', '62b', '8521',]
COLLABORATIVE_ACL_CHOICES = {
'members': 'ALL_MEMBERS',
'managersonly': 'MANAGERS_ONLY',
'managers': 'OWNERS_AND_MANAGERS',
'owners': 'OWNERS_ONLY',
'none': 'NONE',
}
COLLABORATIVE_INBOX_ATTRIBUTES = [
'whoCanAddReferences',
'whoCanAssignTopics',
'whoCanEnterFreeFormTags',
'whoCanMarkDuplicate',
'whoCanMarkFavoriteReplyOnAnyTopic',
'whoCanMarkFavoriteReplyOnOwnTopic',
'whoCanMarkNoResponseNeeded',
'whoCanModifyTagsAndCategories',
'whoCanTakeTopics',
'whoCanUnassignTopic',
'whoCanUnmarkFavoriteReplyOnAnyTopic',
'favoriteRepliesOnTop',
]
COLLABORATIVE_INBOX_ATTRIBUTES = {
'whoCanAddReferences': 'acl',
'whoCanAssignTopics': 'acl',
'whoCanEnterFreeFormTags': 'acl',
'whoCanMarkDuplicate': 'acl',
'whoCanMarkFavoriteReplyOnAnyTopic': 'acl',
'whoCanMarkFavoriteReplyOnOwnTopic': 'acl',
'whoCanMarkNoResponseNeeded': 'acl',
'whoCanModifyTagsAndCategories': 'acl',
'whoCanTakeTopics': 'acl',
'whoCanUnassignTopic': 'acl',
'whoCanUnmarkFavoriteReplyOnAnyTopic': 'acl',
'favoriteRepliesOnTop': True,
}
GROUP_SETTINGS_LIST_ATTRIBUTES = set([
# ACL choices
'whoCanAdd',
'whoCanApproveMembers',
'whoCanApproveMessages',
'whoCanAssignTopics',
'whoCanAssistContent',
'whoCanBanUsers',
'whoCanContactOwner',
'whoCanDeleteAnyPost',
'whoCanDeleteTopics',
'whoCanDiscoverGroup',
'whoCanEnterFreeFormTags',
'whoCanHideAbuse',
'whoCanInvite',
'whoCanJoin',
'whoCanLeaveGroup',
'whoCanLockTopics',
'whoCanMakeTopicsSticky',
'whoCanMarkDuplicate',
'whoCanMarkFavoriteReplyOnAnyTopic',
'whoCanMarkFavoriteReplyOnOwnTopic',
'whoCanMarkNoResponseNeeded',
'whoCanModerateContent',
'whoCanModerateMembers',
'whoCanModifyMembers',
'whoCanModifyTagsAndCategories',
'whoCanMoveTopicsIn',
'whoCanMoveTopicsOut',
'whoCanPostAnnouncements',
'whoCanPostMessage',
'whoCanTakeTopics',
'whoCanUnassignTopic',
'whoCanUnmarkFavoriteReplyOnAnyTopic',
'whoCanViewGroup',
'whoCanViewMembership',
# Miscellaneous hoices
'messageModerationLevel',
'replyTo',
'spamModerationLevel',
])
GROUP_SETTINGS_BOOLEAN_ATTRIBUTES = set([
'allowExternalMembers',
'allowGoogleCommunication',
'allowWebPosting',
'archiveOnly',
'enableCollaborativeInbox',
'favoriteRepliesOnTop',
'includeCustomFooter',
'includeInGlobalAddressList',
'isArchived',
'membersCanPostAsTheGroup',
'sendMessageDenyNotification',
'showInGroupDirectory',
])
#
# Global variables
@@ -733,6 +793,8 @@ GC_MEMBER_MAX_RESULTS = 'member_max_results'
# If no_browser is False, writeCSVfile won't open a browser when todrive is set
# and doRequestOAuth prints a link and waits for the verification code when oauth2.txt is being created
GC_NO_BROWSER = 'no_browser'
# oauth_browser forces usage of web server OAuth flow that proved problematic.
GC_OAUTH_BROWSER = 'oauth_browser'
# Disable GAM API caching
GC_NO_CACHE = 'no_cache'
# Disable GAM update check
@@ -777,7 +839,7 @@ GC_Defaults = {
GC_CUSTOMER_ID: MY_CUSTOMER,
GC_DEBUG_LEVEL: 0,
GC_DECODED_ID_TOKEN: '',
GC_DEVICE_MAX_RESULTS: 500,
GC_DEVICE_MAX_RESULTS: 100,
GC_DOMAIN: '',
GC_DRIVE_DIR: '',
GC_DRIVE_MAX_RESULTS: 1000,
@@ -786,6 +848,7 @@ GC_Defaults = {
GC_NO_CACHE: False,
GC_NO_UPDATE_CHECK: False,
GC_NUM_THREADS: 25,
GC_OAUTH_BROWSER: False,
GC_OAUTH2_TXT: _FN_OAUTH2_TXT,
GC_OAUTH2SERVICE_JSON: _FN_OAUTH2SERVICE_JSON,
GC_SECTION: '',
@@ -837,6 +900,7 @@ GC_VAR_INFO = {
GC_NO_CACHE: {GC_VAR_TYPE: GC_TYPE_BOOLEAN},
GC_NO_UPDATE_CHECK: {GC_VAR_TYPE: GC_TYPE_BOOLEAN},
GC_NUM_THREADS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, None)},
GC_OAUTH_BROWSER: {GC_VAR_TYPE: GC_TYPE_BOOLEAN},
GC_OAUTH2_TXT: {GC_VAR_TYPE: GC_TYPE_FILE},
GC_OAUTH2SERVICE_JSON: {GC_VAR_TYPE: GC_TYPE_FILE},
GC_SECTION: {GC_VAR_TYPE: GC_TYPE_STRING},