mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 14:21:39 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f233f7505c | ||
|
|
315367c80c | ||
|
|
aba602ff2a | ||
|
|
da8833bdc6 | ||
|
|
ed932cce55 | ||
|
|
37ade0059f | ||
|
|
7f6683e21d | ||
|
|
ca44a0dfb6 | ||
|
|
d9f85c600b | ||
|
|
c019a6d2e8 | ||
|
|
e8e25c352b | ||
|
|
1f75ac6112 | ||
|
|
0f111e6eaf | ||
|
|
8ee5b0ffdb | ||
|
|
8813f6c046 | ||
|
|
58fde587a8 | ||
|
|
06573991f6 | ||
|
|
597d3ac4b4 | ||
|
|
c62fa88d00 | ||
|
|
fffb847b5f | ||
|
|
fe69068bba |
@@ -1,9 +1,7 @@
|
||||
rmdir /q /s gam
|
||||
rmdir /q /s gam-64
|
||||
rmdir /q /s python-src-%1
|
||||
rmdir /q /s build
|
||||
rmdir /q /s dist
|
||||
del /q /f gam-%1-python-src.zip
|
||||
del /q /f gam-%1-windows.zip
|
||||
del /q /f gam-%1-windows-x64.zip
|
||||
|
||||
|
||||
323
gam.py
323
gam.py
@@ -24,7 +24,7 @@ For more information, see http://code.google.com/p/google-apps-manager
|
||||
"""
|
||||
|
||||
__author__ = u'Jay Lee <jay0lee@gmail.com>'
|
||||
__version__ = u'3.32'
|
||||
__version__ = u'3.4'
|
||||
__license__ = u'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||
|
||||
import sys, os, time, datetime, random, socket, csv, platform, re, calendar, base64, hashlib
|
||||
@@ -447,17 +447,17 @@ def callGAPIpages(service, function, items=u'items', nextPageToken=u'nextPageTok
|
||||
show_message = page_message
|
||||
try:
|
||||
show_message = show_message.replace(u'%%num_items%%', str(page_items))
|
||||
except KeyError:
|
||||
except (IndexError, KeyError):
|
||||
show_message = show_message.replace(u'%%num_items%%', '0')
|
||||
try:
|
||||
show_message = show_message.replace(u'%%total_items%%', str(total_items))
|
||||
except KeyError:
|
||||
except (IndexError, KeyError):
|
||||
show_message = show_message.replace(u'%%total_items%%', '0')
|
||||
if message_attribute:
|
||||
try:
|
||||
show_message = show_message.replace(u'%%first_item%%', str(this_page[items][0][message_attribute]))
|
||||
show_message = show_message.replace(u'%%last_item%%', str(this_page[items][-1][message_attribute]))
|
||||
except KeyError:
|
||||
except (IndexError, KeyError):
|
||||
show_message = show_message.replace(u'%%first_item%%', '')
|
||||
show_message = show_message.replace(u'%%last_item%%', '')
|
||||
restart_line()
|
||||
@@ -467,7 +467,7 @@ def callGAPIpages(service, function, items=u'items', nextPageToken=u'nextPageTok
|
||||
pageToken = this_page[nextPageToken]
|
||||
if pageToken == '':
|
||||
return all_pages
|
||||
except KeyError:
|
||||
except (IndexError, KeyError):
|
||||
return all_pages
|
||||
|
||||
def getAPIVer(api):
|
||||
@@ -2214,6 +2214,7 @@ def downloadDriveFile(users):
|
||||
query = fileIds = None
|
||||
gdownload_format = u'openoffice'
|
||||
target_folder = getGamPath()
|
||||
safe_filename_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i].lower() == u'id':
|
||||
fileIds = [sys.argv[i+1],]
|
||||
@@ -2263,7 +2264,7 @@ def downloadDriveFile(users):
|
||||
if fileIds[0].find('/') != -1:
|
||||
fileIds[0] = fileIds[0][:fileIds[0].find('/')]
|
||||
if not fileIds:
|
||||
print u'No files to delete for %s' % user
|
||||
print u'No files to download for %s' % user
|
||||
i = 0
|
||||
for fileId in fileIds:
|
||||
extension = None
|
||||
@@ -2295,7 +2296,9 @@ def downloadDriveFile(users):
|
||||
except KeyError:
|
||||
pass
|
||||
break
|
||||
filename = u'%s/%s' % (target_folder, result[u'title'])
|
||||
file_title = result[u'title']
|
||||
safe_file_title = ''.join(c for c in file_title if c in safe_filename_chars)
|
||||
filename = u'%s/%s' % (target_folder, safe_file_title)
|
||||
if extension and filename.lower()[:len(extension)] != extension:
|
||||
filename = u'%s%s' % (filename, extension)
|
||||
y = 0
|
||||
@@ -2664,7 +2667,7 @@ def showSendAs(users):
|
||||
print u''
|
||||
|
||||
def doLanguage(users):
|
||||
language = sys.argv[4].lower()
|
||||
language = sys.argv[4]
|
||||
emailsettings = getEmailSettingsObject()
|
||||
count = len(users)
|
||||
i = 1
|
||||
@@ -3270,6 +3273,104 @@ def getVacation(users):
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def doCreateDomain():
|
||||
cd = buildGAPIObject(u'directory')
|
||||
domain_name = sys.argv[3]
|
||||
domain_type = sys.argv[4].upper()
|
||||
if domain_type.replace(u'_', '') in [u'ALIAS', u'MIRROR', u'DOMAINALIAS', u'ALIAS_DOMAIN']:
|
||||
domain_type = u'DOMAIN_ALIAS'
|
||||
elif domain_type.replace(u'_', '') in [u'SECONDARY', u'SEPARATE', u'MULTIDOMAIN', u'MULTI']:
|
||||
domain_type = u'MULTI_DOMAIN'
|
||||
elif domain_type.replace(u'_', '') in [u'PRIMARY', u'PRIMARYDOMAIN', u'HOME']:
|
||||
domain_type = u'PRIMARY'
|
||||
elif domain_type != u'UNKNOWN':
|
||||
print u'Error: domain type should be alias, secondary, primary or unknown. Got %s' % domain_type
|
||||
sys.exit(4)
|
||||
body = {u'domain_name': domain_name, u'domain_type': domain_type}
|
||||
callGAPI(service=cd.domains(), function=u'insert', customerId=customerId, body=body)
|
||||
print u'Added domain %s' % domain_name
|
||||
|
||||
def doDelSchema():
|
||||
cd = buildGAPIObject(u'directory')
|
||||
schemaKey = sys.argv[3]
|
||||
callGAPI(service=cd.schemas(), function=u'delete', customerId=customerId, schemaKey=schemaKey)
|
||||
print u'Deleted schema %s' % schemaKey
|
||||
|
||||
def doCreateOrUpdateUserSchema():
|
||||
cd = buildGAPIObject(u'directory')
|
||||
schemaName = sys.argv[3]
|
||||
body = {u'schemaName': schemaName, u'fields': []}
|
||||
i = 4
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] in [u'field']:
|
||||
a_field = {u'fieldName': sys.argv[i+1]}
|
||||
i += 2
|
||||
while True:
|
||||
if sys.argv[i].lower() in [u'type']:
|
||||
a_field[u'fieldType'] = sys.argv[i+1].upper()
|
||||
if a_field[u'fieldType'] not in [u'BOOL', u'DOUBLE', u'EMAIL', u'INT64', u'PHONE', u'STRING']:
|
||||
print 'Error: type must be bool, double, email, int64, phone or string. Got %s' % a_field[u'fieldType']
|
||||
sys.exit(3)
|
||||
i += 2
|
||||
elif sys.argv[i].lower() in [u'multivalued']:
|
||||
a_field[u'multiValued'] = True
|
||||
i += 1
|
||||
elif sys.argv[i].lower() in [u'indexed']:
|
||||
a_field[u'indexed'] = True
|
||||
i += 1
|
||||
elif sys.argv[i].lower() in [u'restricted']:
|
||||
a_field[u'readAccessType'] = u'ADMINS_AND_SELF'
|
||||
i += 1
|
||||
elif sys.argv[i].lower() in [u'range']:
|
||||
a_field[u'numericIndexingSpec'] = {u'minValue': sys.argv[i+1], u'maxValue': sys.argv[i+2]}
|
||||
i += 3
|
||||
elif sys.argv[i].lower() in [u'endfield']:
|
||||
body[u'fields'].append(a_field)
|
||||
i += 1
|
||||
break
|
||||
else:
|
||||
print 'Error: %s is not a valid argument to gam create schema' % sys.argv[i]
|
||||
sys.exit(4)
|
||||
if sys.argv[1].lower() == u'create':
|
||||
result = callGAPI(service=cd.schemas(), function=u'insert', customerId=customerId, body=body)
|
||||
print 'Created user schema %s' % result[u'schemaName']
|
||||
elif sys.argv[1].lower() == u'update':
|
||||
result = callGAPI(service=cd.schemas(), function=u'update', customerId=customerId, body=body, schemaKey=schemaName)
|
||||
print 'Updated user schema %s' % result[u'schemaName']
|
||||
|
||||
def doPrintUserSchemas():
|
||||
cd = buildGAPIObject(u'directory')
|
||||
schemas = callGAPI(service=cd.schemas(), function=u'list', customerId=customerId)
|
||||
for schema in schemas[u'schemas']:
|
||||
print u'Schema: %s' % schema[u'schemaName']
|
||||
for a_key in schema.keys():
|
||||
if a_key not in [u'schemaName', u'fields', u'etag', u'kind']:
|
||||
print '%s: %s' % (a_key, schema[a_key])
|
||||
print
|
||||
for field in schema[u'fields']:
|
||||
print u' Field: %s' % field[u'fieldName']
|
||||
for a_key in field.keys():
|
||||
if a_key not in [u'fieldName', u'kind', u'etag']:
|
||||
print ' %s: %s' % (a_key, field[a_key])
|
||||
print
|
||||
print
|
||||
|
||||
def doGetUserSchema():
|
||||
cd = buildGAPIObject(u'directory')
|
||||
schemaKey = sys.argv[3]
|
||||
schema = callGAPI(service=cd.schemas(), function=u'get', customerId=customerId, schemaKey=schemaKey)
|
||||
print u'Schema: %s' % schema[u'schemaName']
|
||||
for a_key in schema.keys():
|
||||
if a_key not in [u'schemaName', u'fields', u'etag', u'kind']:
|
||||
print '%s: %s' % (a_key, schema[a_key])
|
||||
print
|
||||
for field in schema[u'fields']:
|
||||
print u' Field: %s' % field[u'fieldName']
|
||||
for a_key in field.keys():
|
||||
if a_key not in [u'fieldName', u'kind', u'etag']:
|
||||
print ' %s: %s' % (a_key, field[a_key])
|
||||
print
|
||||
|
||||
def doCreateUser():
|
||||
cd = buildGAPIObject(u'directory')
|
||||
body = dict()
|
||||
@@ -3567,9 +3668,33 @@ def doCreateUser():
|
||||
except KeyError:
|
||||
body[u'externalIds'] = [externalid,]
|
||||
i += 1
|
||||
# else:
|
||||
# showUsage()
|
||||
# sys.exit(2)
|
||||
else:
|
||||
showUsage()
|
||||
sys.exit(2)
|
||||
if u'customSchemas' not in body:
|
||||
body[u'customSchemas'] = {}
|
||||
try:
|
||||
(schemaName, fieldName) = sys.argv[i].split(u'.')
|
||||
except ValueError:
|
||||
print 'Error: %s is not a valid create user argument or custom schema name.' % sys.argv[i]
|
||||
sys.exit(3)
|
||||
field_value = sys.argv[i+1]
|
||||
is_multivalue = False
|
||||
if field_value.lower() in [u'multivalue', u'multivalued', u'value']:
|
||||
is_multivalue = True
|
||||
field_value = sys.argv[i+2]
|
||||
if schemaName not in body[u'customSchemas']:
|
||||
body[u'customSchemas'][schemaName] = {}
|
||||
if is_multivalue:
|
||||
if fieldName not in body[u'customSchemas'][schemaName]:
|
||||
body[u'customSchemas'][schemaName][fieldName] = []
|
||||
body[u'customSchemas'][schemaName][fieldName].append({u'value': field_value})
|
||||
else:
|
||||
body[u'customSchemas'][schemaName][fieldName] = field_value
|
||||
i += 2
|
||||
if is_multivalue:
|
||||
i += 1
|
||||
if not gotFirstName:
|
||||
body[u'name'][u'givenName'] = u'Unknown'
|
||||
if not gotLastName:
|
||||
@@ -4039,11 +4164,36 @@ def doUpdateUser(users):
|
||||
except KeyError:
|
||||
body[u'externalIds'] = [externalid,]
|
||||
i += 1
|
||||
# else:
|
||||
# showUsage()
|
||||
# print u''
|
||||
# print u'Error: didn\'t expect %s command at position %s' % (sys.argv[i], i)
|
||||
# sys.exit(2)
|
||||
else:
|
||||
showUsage()
|
||||
print u''
|
||||
print u'Error: didn\'t expect %s command at position %s' % (sys.argv[i], i)
|
||||
sys.exit(2)
|
||||
do_update_user = True
|
||||
if u'customSchemas' not in body:
|
||||
body[u'customSchemas'] = {}
|
||||
try:
|
||||
(schemaName, fieldName) = sys.argv[i].split(u'.')
|
||||
except ValueError:
|
||||
print u'Error: %s is not a valid user update argument or custom schema name' % sys.argv[i]
|
||||
sys.exit(3)
|
||||
field_value = sys.argv[i+1]
|
||||
is_multivalue = False
|
||||
if field_value.lower() == u'multivalue':
|
||||
is_multivalue = True
|
||||
field_value = sys.argv[i+2]
|
||||
if schemaName not in body[u'customSchemas']:
|
||||
body[u'customSchemas'][schemaName] = {}
|
||||
if is_multivalue:
|
||||
if fieldName not in body[u'customSchemas'][schemaName]:
|
||||
body[u'customSchemas'][schemaName][fieldName] = []
|
||||
body[u'customSchemas'][schemaName][fieldName].append({u'value': field_value})
|
||||
else:
|
||||
body[u'customSchemas'][schemaName][fieldName] = field_value
|
||||
i += 2
|
||||
if is_multivalue:
|
||||
i += 1
|
||||
if gotPassword and not (isSHA1 or isMD5 or isCrypt or nohash):
|
||||
body[u'password'] = gen_sha512_hash(body[u'password'])
|
||||
body[u'hashFunction'] = u'crypt'
|
||||
@@ -4185,7 +4335,7 @@ def doUpdateGroup():
|
||||
gs_object = buildDiscoveryObject(u'groupssettings')
|
||||
matches_gs_setting = False
|
||||
for (attrib, params) in gs_object[u'schemas'][u'Groups'][u'properties'].items():
|
||||
if attrib in [u'kind', u'etag', u'email', u'name', u'description']:
|
||||
if attrib in [u'kind', u'etag', u'email']:
|
||||
continue
|
||||
if sys.argv[i].lower().replace(u'_', u'') == attrib.lower():
|
||||
matches_gs_setting = True
|
||||
@@ -4211,7 +4361,7 @@ def doUpdateGroup():
|
||||
value = u'false'
|
||||
break
|
||||
if not matches_gs_setting:
|
||||
print u'ERROR: %s is not a valid argument for "gam create group..."' % sys.argv[i]
|
||||
print u'ERROR: %s is not a valid argument for "gam update group..."' % sys.argv[i]
|
||||
sys.exit(9)
|
||||
gs_body[attrib] = value
|
||||
use_gs_api = True
|
||||
@@ -4472,7 +4622,9 @@ def doGetUserInfo(user_email=None):
|
||||
user_email = user_email[4:]
|
||||
elif user_email.find(u'@') == -1:
|
||||
user_email = u'%s@%s' % (user_email, domain)
|
||||
getAliases = getGroups = getLicenses = True
|
||||
getSchemas = getAliases = getGroups = getLicenses = True
|
||||
projection = u'full'
|
||||
customFieldMask = viewType = None
|
||||
i = 4
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i].lower() == u'noaliases':
|
||||
@@ -4484,10 +4636,23 @@ def doGetUserInfo(user_email=None):
|
||||
elif sys.argv[i].lower() == u'nolicenses':
|
||||
getLicenses = False
|
||||
i += 1
|
||||
elif sys.argv[i].lower() == u'noschemas':
|
||||
getSchemas = False
|
||||
projection = u'basic'
|
||||
i += 1
|
||||
elif sys.argv[i].lower() == u'schemas':
|
||||
getSchemas = True
|
||||
projection = u'custom'
|
||||
customFieldMask = sys.argv[i+1]
|
||||
i += 2
|
||||
elif sys.argv[i].lower() == u'userview':
|
||||
viewType = u'domain_public'
|
||||
getGroups = getLicenses = False
|
||||
i += 1
|
||||
else:
|
||||
print u'%s is not a valid argument for gam info user' % sys.argv[i]
|
||||
sys.exit(3)
|
||||
user = callGAPI(service=cd.users(), function=u'get', userKey=user_email)
|
||||
user = callGAPI(service=cd.users(), function=u'get', userKey=user_email, projection=projection, customFieldMask=customFieldMask, viewType=viewType)
|
||||
print u'User: %s' % user[u'primaryEmail']
|
||||
if u'name' in user and u'givenName' in user[u'name']:
|
||||
print u'First Name: %s' % user[u'name'][u'givenName']
|
||||
@@ -4586,6 +4751,19 @@ def doGetUserInfo(user_email=None):
|
||||
else:
|
||||
print u' %s: %s' % (key, id[key])
|
||||
print u''
|
||||
if getSchemas:
|
||||
print u'Custom Schemas:'
|
||||
if u'customSchemas' in user:
|
||||
for schema in user[u'customSchemas']:
|
||||
print u' Schema: %s' % schema
|
||||
for field in user[u'customSchemas'][schema]:
|
||||
if type(user[u'customSchemas'][schema][field]) is list:
|
||||
print ' %s:' % field
|
||||
for an_item in user[u'customSchemas'][schema][field]:
|
||||
print ' %s' % an_item[u'value']
|
||||
else:
|
||||
print u' %s: %s' % (field, user[u'customSchemas'][schema][field])
|
||||
print
|
||||
if getAliases:
|
||||
if u'aliases' in user:
|
||||
print u'Email Aliases:'
|
||||
@@ -4860,28 +5038,34 @@ def doSiteVerifyAttempt():
|
||||
print u'Method: %s' % verify_data[u'method']
|
||||
print u'Token: %s' % verify_data[u'token']
|
||||
if verify_data[u'method'] == u'DNS_CNAME':
|
||||
import dns.resolver
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = nameservers=[u'8.8.8.8', u'8.8.4.4']
|
||||
cname_token = verify_data[u'token']
|
||||
cname_list = cname_token.split(u' ')
|
||||
cname_subdomain = cname_list[0]
|
||||
try:
|
||||
answers = resolver.query(u'%s.%s' % (cname_subdomain, a_domain), u'A')
|
||||
for answer in answers:
|
||||
print u'DNS Record: %s' % answer
|
||||
except dns.resolver.NXDOMAIN:
|
||||
print u'ERROR: No such domain found in DNS!'
|
||||
import dns.resolver
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = nameservers=[u'8.8.8.8', u'8.8.4.4']
|
||||
cname_token = verify_data[u'token']
|
||||
cname_list = cname_token.split(u' ')
|
||||
cname_subdomain = cname_list[0]
|
||||
try:
|
||||
answers = resolver.query(u'%s.%s' % (cname_subdomain, a_domain), u'A')
|
||||
for answer in answers:
|
||||
print u'DNS Record: %s' % answer
|
||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
print u'ERROR: No such domain found in DNS!'
|
||||
except ImportError:
|
||||
pass
|
||||
elif verify_data[u'method'] == u'DNS_TXT':
|
||||
import dns.resolver
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = nameservers=[u'8.8.8.8', u'8.8.4.4']
|
||||
try:
|
||||
answers = resolver.query(a_domain, u'TXT')
|
||||
for answer in answers:
|
||||
print u'DNS Record: %s' % str(answer).replace(u'"', u'')
|
||||
except dns.resolver.NXDOMAIN:
|
||||
print u'ERROR: no such domain found in DNS!'
|
||||
import dns.resolver
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = nameservers=[u'8.8.8.8', u'8.8.4.4']
|
||||
try:
|
||||
answers = resolver.query(a_domain, u'TXT')
|
||||
for answer in answers:
|
||||
print u'DNS Record: %s' % str(answer).replace(u'"', u'')
|
||||
except dns.resolver.NXDOMAIN:
|
||||
print u'ERROR: no such domain found in DNS!'
|
||||
except ImportError:
|
||||
pass
|
||||
return
|
||||
print u'SUCCESS!'
|
||||
print u'Verified: %s' % verify_result[u'site'][u'identifier']
|
||||
@@ -5286,7 +5470,8 @@ def doGetDomainInfo():
|
||||
if customerId != u'my_customer':
|
||||
customer_id = customerId
|
||||
else:
|
||||
customer_id = callGAPI(service=cd.users(), function=u'list', fields=u'users(customerId)', customer=customerId, maxResults=1)[u'users'][0][u'customerId']
|
||||
result = callGAPI(service=cd.users(), function=u'list', fields=u'users(customerId)', customer=customerId, maxResults=1, sortOrder=u'DESCENDING')
|
||||
customer_id = result[u'users'][0][u'customerId']
|
||||
print u'Customer ID: %s' % customer_id
|
||||
default_language = callGAPI(service=adm.defaultLanguage(), function=u'get', domainName=domain)
|
||||
print u'Default Language: %s' % default_language[u'entry'][u'apps$property'][0][u'value']
|
||||
@@ -5523,14 +5708,24 @@ def doPrintUsers():
|
||||
customer = customerId
|
||||
domain = None
|
||||
query = None
|
||||
getGroupFeed = getLicenseFeed = False
|
||||
projection = u'basic'
|
||||
customFieldMask = None
|
||||
getGroupFeed = getLicenseFeed = email_parts = False
|
||||
todrive = False
|
||||
deleted_only = orderBy = sortOrder = None
|
||||
viewType = deleted_only = orderBy = sortOrder = None
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i].lower() == u'allfields':
|
||||
fields = None
|
||||
i += 1
|
||||
elif sys.argv[i].lower() == u'custom':
|
||||
user_fields.append(u'customSchemas')
|
||||
if sys.argv[i+1].lower() == u'all':
|
||||
projection = u'full'
|
||||
else:
|
||||
projection = u'custom'
|
||||
customFieldMask = sys.argv[i+1]
|
||||
i += 2
|
||||
elif sys.argv[i].lower() == u'todrive':
|
||||
todrive = True
|
||||
i += 1
|
||||
@@ -5547,6 +5742,9 @@ def doPrintUsers():
|
||||
elif orderBy.lower() in [u'givenname', u'firstname']:
|
||||
orderBy= u'givenName'
|
||||
i += 2
|
||||
elif sys.argv[i].lower() == u'userview':
|
||||
viewType = u'domain_public'
|
||||
i += 1
|
||||
elif sys.argv[i].lower() in [u'ascending', u'descending']:
|
||||
sortOrder = sys.argv[i].upper()
|
||||
i += 1
|
||||
@@ -5631,7 +5829,7 @@ def doPrintUsers():
|
||||
fields = u'nextPageToken,users(%s)' % u','.join(user_fields)
|
||||
sys.stderr.write(u"Getting all users in Google Apps account (may take some time on a large account)...\n")
|
||||
page_message = u'Got %%total_items%% users: %%first_item%% - %%last_item%%\n'
|
||||
all_users = callGAPIpages(service=cd.users(), function=u'list', items=u'users', page_message=page_message, message_attribute=u'primaryEmail', customer=customer, domain=domain, fields=fields, showDeleted=deleted_only, maxResults=500, orderBy=orderBy, sortOrder=sortOrder, query=query)
|
||||
all_users = callGAPIpages(service=cd.users(), function=u'list', items=u'users', page_message=page_message, message_attribute=u'primaryEmail', customer=customer, domain=domain, fields=fields, showDeleted=deleted_only, maxResults=500, orderBy=orderBy, sortOrder=sortOrder, viewType=viewType, query=query, projection=projection, customFieldMask=customFieldMask)
|
||||
titles = []
|
||||
attributes = []
|
||||
for user in all_users:
|
||||
@@ -6620,7 +6818,7 @@ def getUsersToModify(entity_type=None, entity=None, silent=False, return_uids=Fa
|
||||
if not silent:
|
||||
sys.stderr.write(u"Getting %s of %s (may take some time for large groups)..." % (member_type_message, group))
|
||||
page_message = u'Got %%%%total_items%%%% %s...' % member_type_message
|
||||
members = callGAPIpages(service=cd.members(), function=u'list', page_message=page_message, groupKey=group, roles=member_type, fields=u'nextPageToken,members(email,id)')
|
||||
members = callGAPIpages(service=cd.members(), function=u'list', items=u'members', page_message=page_message, groupKey=group, roles=member_type, fields=u'nextPageToken,members(email,id)')
|
||||
users = []
|
||||
for member in members:
|
||||
if return_uids:
|
||||
@@ -6829,11 +7027,14 @@ found at:
|
||||
|
||||
%s
|
||||
|
||||
with information from the APIs Console <https://code.google.com/apis/console>.
|
||||
with information from the APIs Console <https://cloud.google.com/console>.
|
||||
|
||||
See https://code.google.com/p/google-apps-manager/wiki/CreatingClientSecretsFile
|
||||
for instructions.
|
||||
|
||||
""" % CLIENT_SECRETS
|
||||
|
||||
selected_scopes = [u'*'] * 19
|
||||
selected_scopes = [u'*'] * 20
|
||||
menu = u'''Select the authorized scopes for this token. Include a 'r' to grant read-only
|
||||
access or an 'a' to grant action-only access.
|
||||
|
||||
@@ -6856,10 +7057,11 @@ access or an 'a' to grant action-only access.
|
||||
[%s] 16) Notifications Directory API
|
||||
[%s] 17) Site Verification API
|
||||
(%s) 18) IMAP/SMTP Access (send notifications to admin)
|
||||
(%s) 19) User Schemas (supports read-only)
|
||||
|
||||
19) Select all scopes
|
||||
20) Unselect all scopes
|
||||
21) Continue
|
||||
20) Select all scopes
|
||||
21) Unselect all scopes
|
||||
22) Continue
|
||||
'''
|
||||
os.system([u'clear', u'cls'][os.name == u'nt'])
|
||||
while True:
|
||||
@@ -6867,7 +7069,7 @@ access or an 'a' to grant action-only access.
|
||||
try:
|
||||
if selection.lower().find(u'r') != -1:
|
||||
selection = int(selection.lower().replace(u'r', u''))
|
||||
if selection not in [0, 1, 2, 3, 4, 10]:
|
||||
if selection not in [0, 1, 2, 3, 4, 10, 19]:
|
||||
os.system([u'clear', u'cls'][os.name == u'nt'])
|
||||
print u'THAT SCOPE DOES NOT SUPPORT READ-ONLY MODE!\n'
|
||||
continue
|
||||
@@ -6879,18 +7081,18 @@ access or an 'a' to grant action-only access.
|
||||
print u'THAT SCOPE DOES NOT SUPPORT ACTION-ONLY MODE!\n'
|
||||
continue
|
||||
selected_scopes[selection] = u'A'
|
||||
elif int(selection) > -1 and int(selection) <= 18:
|
||||
elif int(selection) > -1 and int(selection) <= 19:
|
||||
if selected_scopes[int(selection)] == u' ':
|
||||
selected_scopes[int(selection)] = u'*'
|
||||
else:
|
||||
selected_scopes[int(selection)] = u' '
|
||||
elif selection == u'19':
|
||||
for i in range(0, len(selected_scopes)):
|
||||
selected_scopes[i] = u'*'
|
||||
elif selection == u'20':
|
||||
for i in range(0, len(selected_scopes)):
|
||||
selected_scopes[i] = u' '
|
||||
selected_scopes[i] = u'*'
|
||||
elif selection == u'21':
|
||||
for i in range(0, len(selected_scopes)):
|
||||
selected_scopes[i] = u' '
|
||||
elif selection == u'22':
|
||||
at_least_one = False
|
||||
for i in range(0, len(selected_scopes)):
|
||||
if selected_scopes[i] in [u'*', u'R', u'A']:
|
||||
@@ -6929,7 +7131,8 @@ access or an 'a' to grant action-only access.
|
||||
u'https://www.googleapis.com/auth/admin.directory.user.security', # User Security Directory API
|
||||
u'https://www.googleapis.com/auth/admin.directory.notifications', # Notifications Directory API
|
||||
u'https://www.googleapis.com/auth/siteverification', # Site Verification API
|
||||
u'https://mail.google.com/'] # IMAP/SMTP authentication for admin notifications
|
||||
u'https://mail.google.com/', # IMAP/SMTP authentication for admin notifications
|
||||
u'https://www.googleapis.com/auth/admin.directory.userschema'] # Customer User Schema
|
||||
if incremental_auth:
|
||||
scopes = []
|
||||
else:
|
||||
@@ -7067,12 +7270,14 @@ try:
|
||||
doCreateGroup()
|
||||
elif sys.argv[2].lower() in [u'nickname', u'alias']:
|
||||
doCreateAlias()
|
||||
elif sys.argv[2].lower() == u'org':
|
||||
elif sys.argv[2].lower() in [u'org', 'ou']:
|
||||
doCreateOrg()
|
||||
elif sys.argv[2].lower() == u'resource':
|
||||
doCreateResource()
|
||||
elif sys.argv[2].lower() in [u'verify', u'verification']:
|
||||
doSiteVerifyShow()
|
||||
elif sys.argv[2].lower() in [u'schema']:
|
||||
doCreateOrUpdateUserSchema()
|
||||
else:
|
||||
print u'Error: invalid argument to "gam create..."'
|
||||
sys.exit(2)
|
||||
@@ -7098,6 +7303,8 @@ try:
|
||||
doUpdateNotification()
|
||||
elif sys.argv[2].lower() in [u'verify', u'verification']:
|
||||
doSiteVerifyAttempt()
|
||||
elif sys.argv[2].lower() in [u'schema', u'schemas']:
|
||||
doCreateOrUpdateUserSchema()
|
||||
else:
|
||||
showUsage()
|
||||
print u'Error: invalid argument to "gam update..."'
|
||||
@@ -7124,6 +7331,8 @@ try:
|
||||
doGetNotifications()
|
||||
elif sys.argv[2].lower() in [u'verify', u'verification']:
|
||||
doGetSiteVerifications()
|
||||
elif sys.argv[2].lower() in [u'schema', u'schemas']:
|
||||
doGetUserSchema()
|
||||
else:
|
||||
print u'Error: invalid argument to "gam info..."'
|
||||
sys.exit(2)
|
||||
@@ -7145,6 +7354,8 @@ try:
|
||||
doDeleteNotification()
|
||||
elif sys.argv[2].lower() in [u'verify', u'verification']:
|
||||
doDelSiteVerify()
|
||||
elif sys.argv[2].lower() in [u'schema', u'schemas']:
|
||||
doDelSchema()
|
||||
else:
|
||||
print u'Error: invalid argument to "gam delete"'
|
||||
sys.exit(2)
|
||||
@@ -7222,6 +7433,8 @@ try:
|
||||
doPrintLicenses()
|
||||
elif sys.argv[2].lower() in [u'token', u'tokens']:
|
||||
doPrintTokens()
|
||||
elif sys.argv[2].lower() in [u'schema', u'schemas']:
|
||||
doPrintUserSchemas()
|
||||
else:
|
||||
print u'Error: invalid argument to "gam print..."'
|
||||
sys.exit(2)
|
||||
|
||||
@@ -1269,7 +1269,7 @@ class OAuth2WebServerFlow(Flow):
|
||||
'client_secret': self.client_secret,
|
||||
'code': code,
|
||||
'redirect_uri': self.redirect_uri,
|
||||
'scope': self.scope,
|
||||
# 'scope': self.scope,
|
||||
})
|
||||
headers = {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
|
||||
@@ -1,147 +1,265 @@
|
||||
# Early, and incomplete implementation of -04.
|
||||
#
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
URI Template (RFC6570) Processor
|
||||
"""
|
||||
|
||||
__copyright__ = """\
|
||||
Copyright 2011-2013 Joe Gregorio
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import re
|
||||
import urllib
|
||||
try:
|
||||
from urllib.parse import quote
|
||||
except ImportError:
|
||||
from urllib import quote
|
||||
|
||||
|
||||
|
||||
__version__ = "0.6"
|
||||
|
||||
RESERVED = ":/?#[]@!$&'()*+,;="
|
||||
OPERATOR = "+./;?|!@"
|
||||
EXPLODE = "*+"
|
||||
OPERATOR = "+#./;?&|!@"
|
||||
MODIFIER = ":^"
|
||||
TEMPLATE = re.compile(r"{(?P<operator>[\+\./;\?|!@])?(?P<varlist>[^}]+)}", re.UNICODE)
|
||||
VAR = re.compile(r"^(?P<varname>[^=\+\*:\^]+)((?P<explode>[\+\*])|(?P<partial>[:\^]-?[0-9]+))?(=(?P<default>.*))?$", re.UNICODE)
|
||||
|
||||
def _tostring(varname, value, explode, operator, safe=""):
|
||||
if type(value) == type([]):
|
||||
if explode == "+":
|
||||
return ",".join([varname + "." + urllib.quote(x, safe) for x in value])
|
||||
else:
|
||||
return ",".join([urllib.quote(x, safe) for x in value])
|
||||
if type(value) == type({}):
|
||||
keys = value.keys()
|
||||
keys.sort()
|
||||
if explode == "+":
|
||||
return ",".join([varname + "." + urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
|
||||
else:
|
||||
return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
|
||||
else:
|
||||
return urllib.quote(value, safe)
|
||||
TEMPLATE = re.compile("{([^\}]+)}")
|
||||
|
||||
|
||||
def _tostring_path(varname, value, explode, operator, safe=""):
|
||||
joiner = operator
|
||||
if type(value) == type([]):
|
||||
if explode == "+":
|
||||
return joiner.join([varname + "." + urllib.quote(x, safe) for x in value])
|
||||
elif explode == "*":
|
||||
return joiner.join([urllib.quote(x, safe) for x in value])
|
||||
else:
|
||||
return ",".join([urllib.quote(x, safe) for x in value])
|
||||
elif type(value) == type({}):
|
||||
keys = value.keys()
|
||||
keys.sort()
|
||||
if explode == "+":
|
||||
return joiner.join([varname + "." + urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys])
|
||||
elif explode == "*":
|
||||
return joiner.join([urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys])
|
||||
else:
|
||||
return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
|
||||
else:
|
||||
if value:
|
||||
return urllib.quote(value, safe)
|
||||
else:
|
||||
return ""
|
||||
def variables(template):
|
||||
'''Returns the set of keywords in a uri template'''
|
||||
vars = set()
|
||||
for varlist in TEMPLATE.findall(template):
|
||||
if varlist[0] in OPERATOR:
|
||||
varlist = varlist[1:]
|
||||
varspecs = varlist.split(',')
|
||||
for var in varspecs:
|
||||
# handle prefix values
|
||||
var = var.split(':')[0]
|
||||
# handle composite values
|
||||
if var.endswith('*'):
|
||||
var = var[:-1]
|
||||
vars.add(var)
|
||||
return vars
|
||||
|
||||
def _tostring_query(varname, value, explode, operator, safe=""):
|
||||
joiner = operator
|
||||
varprefix = ""
|
||||
if operator == "?":
|
||||
joiner = "&"
|
||||
varprefix = varname + "="
|
||||
if type(value) == type([]):
|
||||
if 0 == len(value):
|
||||
return ""
|
||||
if explode == "+":
|
||||
return joiner.join([varname + "=" + urllib.quote(x, safe) for x in value])
|
||||
elif explode == "*":
|
||||
return joiner.join([urllib.quote(x, safe) for x in value])
|
||||
|
||||
def _quote(value, safe, prefix=None):
|
||||
if prefix is not None:
|
||||
return quote(str(value)[:prefix], safe)
|
||||
return quote(str(value), safe)
|
||||
|
||||
|
||||
def _tostring(varname, value, explode, prefix, operator, safe=""):
|
||||
if isinstance(value, list):
|
||||
return ",".join([_quote(x, safe) for x in value])
|
||||
if isinstance(value, dict):
|
||||
keys = sorted(value.keys())
|
||||
if explode:
|
||||
return ",".join([_quote(key, safe) + "=" + \
|
||||
_quote(value[key], safe) for key in keys])
|
||||
else:
|
||||
return ",".join([_quote(key, safe) + "," + \
|
||||
_quote(value[key], safe) for key in keys])
|
||||
elif value is None:
|
||||
return
|
||||
else:
|
||||
return varprefix + ",".join([urllib.quote(x, safe) for x in value])
|
||||
elif type(value) == type({}):
|
||||
if 0 == len(value):
|
||||
return ""
|
||||
keys = value.keys()
|
||||
keys.sort()
|
||||
if explode == "+":
|
||||
return joiner.join([varname + "." + urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys])
|
||||
elif explode == "*":
|
||||
return joiner.join([urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys])
|
||||
return _quote(value, safe, prefix)
|
||||
|
||||
|
||||
def _tostring_path(varname, value, explode, prefix, operator, safe=""):
|
||||
joiner = operator
|
||||
if isinstance(value, list):
|
||||
if explode:
|
||||
out = [_quote(x, safe) for x in value if value is not None]
|
||||
else:
|
||||
joiner = ","
|
||||
out = [_quote(x, safe) for x in value if value is not None]
|
||||
if out:
|
||||
return joiner.join(out)
|
||||
else:
|
||||
return
|
||||
elif isinstance(value, dict):
|
||||
keys = sorted(value.keys())
|
||||
if explode:
|
||||
out = [_quote(key, safe) + "=" + \
|
||||
_quote(value[key], safe) for key in keys \
|
||||
if value[key] is not None]
|
||||
else:
|
||||
joiner = ","
|
||||
out = [_quote(key, safe) + "," + \
|
||||
_quote(value[key], safe) \
|
||||
for key in keys if value[key] is not None]
|
||||
if out:
|
||||
return joiner.join(out)
|
||||
else:
|
||||
return
|
||||
elif value is None:
|
||||
return
|
||||
else:
|
||||
return varprefix + ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
|
||||
else:
|
||||
if value:
|
||||
return varname + "=" + urllib.quote(value, safe)
|
||||
return _quote(value, safe, prefix)
|
||||
|
||||
|
||||
def _tostring_semi(varname, value, explode, prefix, operator, safe=""):
|
||||
joiner = operator
|
||||
if operator == "?":
|
||||
joiner = "&"
|
||||
if isinstance(value, list):
|
||||
if explode:
|
||||
out = [varname + "=" + _quote(x, safe) \
|
||||
for x in value if x is not None]
|
||||
if out:
|
||||
return joiner.join(out)
|
||||
else:
|
||||
return
|
||||
else:
|
||||
return varname + "=" + ",".join([_quote(x, safe) \
|
||||
for x in value])
|
||||
elif isinstance(value, dict):
|
||||
keys = sorted(value.keys())
|
||||
if explode:
|
||||
return joiner.join([_quote(key, safe) + "=" + \
|
||||
_quote(value[key], safe) \
|
||||
for key in keys if key is not None])
|
||||
else:
|
||||
return varname + "=" + ",".join([_quote(key, safe) + "," + \
|
||||
_quote(value[key], safe) for key in keys \
|
||||
if key is not None])
|
||||
else:
|
||||
return varname
|
||||
if value is None:
|
||||
return
|
||||
elif value:
|
||||
return (varname + "=" + _quote(value, safe, prefix))
|
||||
else:
|
||||
return varname
|
||||
|
||||
|
||||
def _tostring_query(varname, value, explode, prefix, operator, safe=""):
|
||||
joiner = operator
|
||||
if operator in ["?", "&"]:
|
||||
joiner = "&"
|
||||
if isinstance(value, list):
|
||||
if 0 == len(value):
|
||||
return None
|
||||
if explode:
|
||||
return joiner.join([varname + "=" + _quote(x, safe) \
|
||||
for x in value])
|
||||
else:
|
||||
return (varname + "=" + ",".join([_quote(x, safe) \
|
||||
for x in value]))
|
||||
elif isinstance(value, dict):
|
||||
if 0 == len(value):
|
||||
return None
|
||||
keys = sorted(value.keys())
|
||||
if explode:
|
||||
return joiner.join([_quote(key, safe) + "=" + \
|
||||
_quote(value[key], safe) \
|
||||
for key in keys])
|
||||
else:
|
||||
return varname + "=" + \
|
||||
",".join([_quote(key, safe) + "," + \
|
||||
_quote(value[key], safe) for key in keys])
|
||||
else:
|
||||
if value is None:
|
||||
return
|
||||
elif value:
|
||||
return (varname + "=" + _quote(value, safe, prefix))
|
||||
else:
|
||||
return (varname + "=")
|
||||
|
||||
|
||||
TOSTRING = {
|
||||
"" : _tostring,
|
||||
"+": _tostring,
|
||||
";": _tostring_query,
|
||||
"#": _tostring,
|
||||
";": _tostring_semi,
|
||||
"?": _tostring_query,
|
||||
"&": _tostring_query,
|
||||
"/": _tostring_path,
|
||||
".": _tostring_path,
|
||||
}
|
||||
|
||||
|
||||
def expand(template, vars):
|
||||
def _sub(match):
|
||||
groupdict = match.groupdict()
|
||||
operator = groupdict.get('operator')
|
||||
if operator is None:
|
||||
operator = ''
|
||||
varlist = groupdict.get('varlist')
|
||||
def expand(template, variables):
|
||||
"""
|
||||
Expand template as a URI Template using variables.
|
||||
"""
|
||||
def _sub(match):
|
||||
expression = match.group(1)
|
||||
operator = ""
|
||||
if expression[0] in OPERATOR:
|
||||
operator = expression[0]
|
||||
varlist = expression[1:]
|
||||
else:
|
||||
varlist = expression
|
||||
|
||||
safe = "@"
|
||||
if operator == '+':
|
||||
safe = RESERVED
|
||||
varspecs = varlist.split(",")
|
||||
varnames = []
|
||||
defaults = {}
|
||||
for varspec in varspecs:
|
||||
m = VAR.search(varspec)
|
||||
groupdict = m.groupdict()
|
||||
varname = groupdict.get('varname')
|
||||
explode = groupdict.get('explode')
|
||||
partial = groupdict.get('partial')
|
||||
default = groupdict.get('default')
|
||||
if default:
|
||||
defaults[varname] = default
|
||||
varnames.append((varname, explode, partial))
|
||||
safe = ""
|
||||
if operator in ["+", "#"]:
|
||||
safe = RESERVED
|
||||
varspecs = varlist.split(",")
|
||||
varnames = []
|
||||
defaults = {}
|
||||
for varspec in varspecs:
|
||||
default = None
|
||||
explode = False
|
||||
prefix = None
|
||||
if "=" in varspec:
|
||||
varname, default = tuple(varspec.split("=", 1))
|
||||
else:
|
||||
varname = varspec
|
||||
if varname[-1] == "*":
|
||||
explode = True
|
||||
varname = varname[:-1]
|
||||
elif ":" in varname:
|
||||
try:
|
||||
prefix = int(varname[varname.index(":")+1:])
|
||||
except ValueError:
|
||||
raise ValueError("non-integer prefix '{0}'".format(
|
||||
varname[varname.index(":")+1:]))
|
||||
varname = varname[:varname.index(":")]
|
||||
if default:
|
||||
defaults[varname] = default
|
||||
varnames.append((varname, explode, prefix))
|
||||
|
||||
retval = []
|
||||
joiner = operator
|
||||
prefix = operator
|
||||
if operator == "+":
|
||||
prefix = ""
|
||||
joiner = ","
|
||||
if operator == "?":
|
||||
joiner = "&"
|
||||
if operator == "":
|
||||
joiner = ","
|
||||
for varname, explode, partial in varnames:
|
||||
if varname in vars:
|
||||
value = vars[varname]
|
||||
#if not value and (type(value) == type({}) or type(value) == type([])) and varname in defaults:
|
||||
if not value and value != "" and varname in defaults:
|
||||
value = defaults[varname]
|
||||
elif varname in defaults:
|
||||
value = defaults[varname]
|
||||
else:
|
||||
continue
|
||||
retval.append(TOSTRING[operator](varname, value, explode, operator, safe=safe))
|
||||
if "".join(retval):
|
||||
return prefix + joiner.join(retval)
|
||||
else:
|
||||
return ""
|
||||
retval = []
|
||||
joiner = operator
|
||||
start = operator
|
||||
if operator == "+":
|
||||
start = ""
|
||||
joiner = ","
|
||||
if operator == "#":
|
||||
joiner = ","
|
||||
if operator == "?":
|
||||
joiner = "&"
|
||||
if operator == "&":
|
||||
start = "&"
|
||||
if operator == "":
|
||||
joiner = ","
|
||||
for varname, explode, prefix in varnames:
|
||||
if varname in variables:
|
||||
value = variables[varname]
|
||||
if not value and value != "" and varname in defaults:
|
||||
value = defaults[varname]
|
||||
elif varname in defaults:
|
||||
value = defaults[varname]
|
||||
else:
|
||||
continue
|
||||
expanded = TOSTRING[operator](
|
||||
varname, value, explode, prefix, operator, safe=safe)
|
||||
if expanded is not None:
|
||||
retval.append(expanded)
|
||||
if len(retval) > 0:
|
||||
return start + joiner.join(retval)
|
||||
else:
|
||||
return ""
|
||||
|
||||
return TEMPLATE.sub(_sub, template)
|
||||
return TEMPLATE.sub(_sub, template)
|
||||
|
||||
35
whatsnew.txt
35
whatsnew.txt
@@ -1,3 +1,36 @@
|
||||
GAM 3.4 "Oktoberfest"
|
||||
-Support for creating and setting custom user schemas http://goo.gl/M9rQrI
|
||||
-End user view of print users and user info commands.
|
||||
-fix updating name/description for groups
|
||||
-fix groups sync commands
|
||||
-gam print groups members no longer fails on zero member groups
|
||||
-make sure downloaded Drive file names are safe for OS filenames.
|
||||
-gam info domain should no longer crash on getting customer ID
|
||||
-other minor bug fixes
|
||||
|
||||
GAM 3.32
|
||||
-fix service account json files downloaded from new cloud console don't work with GAM
|
||||
-use the new Gmail API for label command. Offers ability to rename labels and show/hide labels from the label and message list.
|
||||
-copy Google drive files with commands like gam user <email> update drivefile id <fileid> copy
|
||||
-print only one SKU for licenses with commands like gam print licenses sku vault
|
||||
-short license names for Google Apps Message Security (gams), Google Apps Unlimtied (gau) and Google Vault Former Employee (vfe)
|
||||
|
||||
GAM 3.31
|
||||
-New command "gam user delete aliases" clears all aliases for user.
|
||||
-"gam update user email vfe" will rename user to vfe.oldname.XXXXX@olddomain.com
|
||||
-fix delete photo command
|
||||
-fix password update/create for Windows builds
|
||||
|
||||
GAM 3.3
|
||||
-Major rewrites of "gam report" and "gam print users". Note that CSV headers have changed. Better performance and print users now supports "rich" fields like organization, phone, relations, etc.
|
||||
-"gam report drive" now works for Google Apps Unlimited domains.
|
||||
-"gam info user" now shows the user's licenses. Use "nolicenses" to prevent showing licenses.
|
||||
-fix "gam print licenses" fails in large domains by reducing page size from 1000 to 100
|
||||
-GAM now sends a sha-512 salted hashed password on user create and update for better security.
|
||||
-cleanup unused features of old GData library.
|
||||
-fix for Drive file downloading default format.
|
||||
-upgrade to httplib v0.9 which may help with httplib.BadStatus errors.
|
||||
|
||||
GAM 3.21
|
||||
-Fix crash when attempting to perform Drive operations for users with Drive service disabled.
|
||||
-Fix "gam info org" only prints first 100 users in org.
|
||||
@@ -222,4 +255,4 @@ GAM 1.8
|
||||
|
||||
-The settings filter command http://code.google.com/p/google-apps-manager/wiki/ExamplesEmailSettings#Create_a_Filter now has more actions including forward, star, trash and never send to spam.
|
||||
|
||||
-Downloading Audit Exports now has partial resume support. GAM will not re-download files that already exist on the local drive. If a large export download fails you should delete the last file GAM was working on since it's incomplete and then restart the process, GAM will pick up with the last file.
|
||||
-Downloading Audit Exports now has partial resume support. GAM will not re-download files that already exist on the local drive. If a large export download fails you should delete the last file GAM was working on since it's incomplete and then restart the process, GAM will pick up with the last file.
|
||||
|
||||
Reference in New Issue
Block a user