diff --git a/src/gam.py b/src/gam.py index cde2405b..d4da6e67 100755 --- a/src/gam.py +++ b/src/gam.py @@ -25,7 +25,7 @@ For more information, see http://git.io/gam """ __author__ = u'Jay Lee ' -__version__ = u'3.63' +__version__ = u'3.64' __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, string, StringIO, subprocess @@ -108,7 +108,7 @@ GM_MAP_USER_ID_TO_NAME = u'ui2n' # GM_Globals = { GM_SYSEXITRC: 0, - GM_GAM_PATH: os.path.dirname(os.path.realpath(__file__)), + GM_GAM_PATH: os.path.dirname(os.path.realpath(__file__)) if not getattr(sys, u'frozen', False) else os.path.dirname(sys.executable), GM_WINDOWS: os.name == u'nt', GM_SYS_ENCODING: sys.getfilesystemencoding() if os.name == u'nt' else u'utf-8', GM_BATCH_QUEUE: None, @@ -217,35 +217,35 @@ GC_TYPE_INTEGER = u'inte' GC_TYPE_LANGUAGE = u'lang' GC_TYPE_STRING = u'stri' -GC_VAR_TYPE_KEY = u'type' -GC_VAR_LIMITS_KEY = u'lmit' +GC_VAR_TYPE = u'type' +GC_VAR_LIMITS = u'lmit' GC_VAR_INFO = { - GC_ACTIVITY_MAX_RESULTS: {GC_VAR_TYPE_KEY: GC_TYPE_INTEGER, GC_VAR_LIMITS_KEY: (1, 500)}, - GC_AUTO_BATCH_MIN: {GC_VAR_TYPE_KEY: GC_TYPE_INTEGER, GC_VAR_LIMITS_KEY: (0, None)}, - GC_CACHE_DIR: {GC_VAR_TYPE_KEY: GC_TYPE_DIRECTORY}, - GC_CHARSET: {GC_VAR_TYPE_KEY: GC_TYPE_STRING}, - GC_CLIENT_SECRETS_JSON: {GC_VAR_TYPE_KEY: GC_TYPE_FILE}, - GC_CONFIG_DIR: {GC_VAR_TYPE_KEY: GC_TYPE_DIRECTORY}, - GC_CUSTOMER_ID: {GC_VAR_TYPE_KEY: GC_TYPE_STRING}, - GC_DEBUG_LEVEL: {GC_VAR_TYPE_KEY: GC_TYPE_INTEGER, GC_VAR_LIMITS_KEY: (0, None)}, - GC_DEVICE_MAX_RESULTS: {GC_VAR_TYPE_KEY: GC_TYPE_INTEGER, GC_VAR_LIMITS_KEY: (1, 1000)}, - GC_DOMAIN: {GC_VAR_TYPE_KEY: GC_TYPE_STRING}, - GC_DRIVE_DIR: {GC_VAR_TYPE_KEY: GC_TYPE_DIRECTORY}, - GC_DRIVE_MAX_RESULTS: {GC_VAR_TYPE_KEY: GC_TYPE_INTEGER, GC_VAR_LIMITS_KEY: (1, 1000)}, - GC_NO_BROWSER: {GC_VAR_TYPE_KEY: GC_TYPE_BOOLEAN}, - GC_NO_CACHE: {GC_VAR_TYPE_KEY: GC_TYPE_BOOLEAN}, - GC_NO_UPDATE_CHECK: {GC_VAR_TYPE_KEY: GC_TYPE_BOOLEAN}, - GC_NO_VERIFY_SSL: {GC_VAR_TYPE_KEY: GC_TYPE_BOOLEAN}, - GC_NUM_THREADS: {GC_VAR_TYPE_KEY: GC_TYPE_INTEGER, GC_VAR_LIMITS_KEY: (1, None)}, - GC_OAUTH2_TXT: {GC_VAR_TYPE_KEY: GC_TYPE_FILE}, - GC_OAUTH2SERVICE_JSON: {GC_VAR_TYPE_KEY: GC_TYPE_FILE}, - GC_SECTION: {GC_VAR_TYPE_KEY: GC_TYPE_STRING}, - GC_SHOW_COUNTS_MIN: {GC_VAR_TYPE_KEY: GC_TYPE_INTEGER, GC_VAR_LIMITS_KEY: (0, None)}, - GC_SHOW_GETTINGS: {GC_VAR_TYPE_KEY: GC_TYPE_BOOLEAN}, - GC_SITE_DIR: {GC_VAR_TYPE_KEY: GC_TYPE_DIRECTORY}, - GC_USER_BATCH_SIZE: {GC_VAR_TYPE_KEY: GC_TYPE_INTEGER, GC_VAR_LIMITS_KEY: (1, 1000)}, - GC_USER_MAX_RESULTS: {GC_VAR_TYPE_KEY: GC_TYPE_INTEGER, GC_VAR_LIMITS_KEY: (1, 500)}, + GC_ACTIVITY_MAX_RESULTS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 500)}, + GC_AUTO_BATCH_MIN: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (0, None)}, + GC_CACHE_DIR: {GC_VAR_TYPE: GC_TYPE_DIRECTORY}, + GC_CHARSET: {GC_VAR_TYPE: GC_TYPE_STRING}, + GC_CLIENT_SECRETS_JSON: {GC_VAR_TYPE: GC_TYPE_FILE}, + GC_CONFIG_DIR: {GC_VAR_TYPE: GC_TYPE_DIRECTORY}, + GC_CUSTOMER_ID: {GC_VAR_TYPE: GC_TYPE_STRING}, + GC_DEBUG_LEVEL: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (0, None)}, + GC_DEVICE_MAX_RESULTS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 1000)}, + GC_DOMAIN: {GC_VAR_TYPE: GC_TYPE_STRING}, + GC_DRIVE_DIR: {GC_VAR_TYPE: GC_TYPE_DIRECTORY}, + GC_DRIVE_MAX_RESULTS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 1000)}, + GC_NO_BROWSER: {GC_VAR_TYPE: GC_TYPE_BOOLEAN}, + GC_NO_CACHE: {GC_VAR_TYPE: GC_TYPE_BOOLEAN}, + GC_NO_UPDATE_CHECK: {GC_VAR_TYPE: GC_TYPE_BOOLEAN}, + GC_NO_VERIFY_SSL: {GC_VAR_TYPE: GC_TYPE_BOOLEAN}, + GC_NUM_THREADS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, None)}, + 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}, + GC_SHOW_COUNTS_MIN: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (0, None)}, + GC_SHOW_GETTINGS: {GC_VAR_TYPE: GC_TYPE_BOOLEAN}, + GC_SITE_DIR: {GC_VAR_TYPE: GC_TYPE_DIRECTORY}, + GC_USER_BATCH_SIZE: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 1000)}, + GC_USER_MAX_RESULTS: {GC_VAR_TYPE: GC_TYPE_INTEGER, GC_VAR_LIMITS: (1, 500)}, } MESSAGE_CLIENT_API_ACCESS_DENIED = u'Access Denied. Please make sure the Client Name:\n\n{0}\n\nis authorized for the API Scope(s):\n\n{1}\n\nThis can be configured in your Control Panel under:\n\nSecurity -->\nAdvanced Settings -->\nManage API client access' @@ -426,10 +426,10 @@ def SetGlobalVariables(): def _getOldEnvVar(itemName, envVar): value = os.environ.get(envVar, GC_Defaults[itemName]) - if GC_VAR_INFO[itemName][GC_VAR_TYPE_KEY] == GC_TYPE_INTEGER: + if GC_VAR_INFO[itemName][GC_VAR_TYPE] == GC_TYPE_INTEGER: try: number = int(value) - minVal, maxVal = GC_VAR_INFO[itemName][GC_VAR_LIMITS_KEY] + minVal, maxVal = GC_VAR_INFO[itemName][GC_VAR_LIMITS] if number < minVal: number = minVal elif maxVal and (number > maxVal): @@ -481,10 +481,10 @@ def SetGlobalVariables(): _getOldSignalFile(GC_NO_UPDATE_CHECK, u'noupdatecheck.txt') # Assign directories first for itemName in GC_VAR_INFO: - if GC_VAR_INFO[itemName][GC_VAR_TYPE_KEY] == GC_TYPE_DIRECTORY: + if GC_VAR_INFO[itemName][GC_VAR_TYPE] == GC_TYPE_DIRECTORY: GC_Values[itemName] = _getCfgDirectory(itemName) for itemName in GC_VAR_INFO: - varType = GC_VAR_INFO[itemName][GC_VAR_TYPE_KEY] + varType = GC_VAR_INFO[itemName][GC_VAR_TYPE] if varType == GC_TYPE_FILE: GC_Values[itemName] = _getCfgFile(itemName) else: @@ -1607,14 +1607,14 @@ def doCreateAdmin(): def doPrintAdminRoles(): cd = buildGAPIObject(u'directory') roles = callGAPIpages(service=cd.roles(), function=u'list', items=u'items', - customer=GC_Values[GC_CUSTOMER_ID]) + customer=GC_Values[GC_CUSTOMER_ID]) roles_attrib = [{}] for role in roles: role_attrib = {} for key, value in role.items(): if key in [u'kind', u'etag', u'etags']: continue - if not isinstance( value, (str, unicode, bool)): + if not isinstance(value, (str, unicode, bool)): continue if key not in roles_attrib[0]: roles_attrib[0][key] = key @@ -3478,6 +3478,8 @@ def doDriveSearch(drive, query=None): ids.append(f_file[u'id']) return ids +DELETE_DRIVEFILE_FUNCTION_TO_ACTION_MAP = {u'delete': u'purging', u'trash': u'trashing', u'untrash': u'untrashing',} + def deleteDriveFile(users): fileIds = sys.argv[5] function = u'trash' @@ -3486,9 +3488,13 @@ def deleteDriveFile(users): if sys.argv[i].lower() == u'purge': function = u'delete' i += 1 + elif sys.argv[i].lower() == u'untrash': + function = u'untrash' + i += 1 else: print u'ERROR: %s is not a valid argument for "gam delete drivefile"' % sys.argv[i] sys.exit(2) + action = DELETE_DRIVEFILE_FUNCTION_TO_ACTION_MAP[function] for user in users: drive = buildGAPIServiceObject(u'drive', user) if fileIds[:6].lower() == u'query:': @@ -3500,14 +3506,11 @@ def deleteDriveFile(users): fileIds = fileIds[:fileIds.find(u'/')] file_ids = [fileIds,] if not file_ids: - print u'No files to delete for %s' % user + print u'No files to %s for %s' % (function, user) i = 0 for fileId in file_ids: i += 1 - if function == u'trash': - print u'trashing %s for %s (%s of %s)' % (fileId, user, i, len(file_ids)) - else: - print u'purging %s for %s (%s of %s)' % (fileId, user, i, len(file_ids)) + print u'%s %s for %s (%s of %s)' % (action, fileId, user, i, len(file_ids)) callGAPI(service=drive.files(), function=function, fileId=fileId) def printDriveFolderContents(feed, folderId, indent): @@ -4451,10 +4454,12 @@ def doLabel(users): i += 1 callGAPI(service=gmail.users().labels(), function=u'create', soft_errors=True, userId=user, body=body) -def doDeleteMessages(trashOrDelete, users): +PROCESS_MESSAGE_FUNCTION_TO_ACTION_MAP = {u'delete': u'deleted', u'trash': u'trashed', u'untrash': u'untrashed',} + +def doProcessMessages(users, function): query = None doIt = False - maxToDelete = 1 + maxToProcess = 1 i = 5 while i < len(sys.argv): if sys.argv[i].lower() == u'query': @@ -4463,15 +4468,16 @@ def doDeleteMessages(trashOrDelete, users): elif sys.argv[i].lower() == u'doit': doIt = True i += 1 - elif sys.argv[i].lower().replace(u'_', u'') == u'maxtodelete': - maxToDelete = int(sys.argv[i+1]) + elif sys.argv[i].lower().replace(u'_', u'') in [u'maxtodelete', u'maxtotrash', u'maxtomove', u'maxtountrash']: + maxToProcess = int(sys.argv[i+1]) i += 2 else: - print u'ERROR: %s is not a valid argument for "gam delete messages"' % sys.argv[i] + print u'ERROR: %s is not a valid argument for "gam %s messages"' % (sys.argv[i], function) sys.exit(2) if not query: print u'ERROR: No query specified. You must specify some query!' sys.exit(2) + action = PROCESS_MESSAGE_FUNCTION_TO_ACTION_MAP[function] for user in users: print u'Searching messages for %s' % user gmail = buildGAPIServiceObject(u'gmail', user) @@ -4481,16 +4487,16 @@ def doDeleteMessages(trashOrDelete, users): userId=u'me', q=query, includeSpamTrash=True, soft_errors=True) del_count = len(listResult) if not doIt: - print u'would try to delete %s messages for user %s (max %s)\n' % (del_count, user, maxToDelete) + print u'would try to %s %s messages for user %s (max %s)\n' % (function, del_count, user, maxToProcess) continue - elif del_count > maxToDelete: - print u'WARNING: refusing to delete ANY messages for %s since max_to_delete is %s and messages to be deleted is %s\n' % (user, maxToDelete, del_count) + elif del_count > maxToProcess: + print u'WARNING: refusing to %s ANY messages for %s since max messages to process is %s and messages to be %s is %s\n' % (function, user, maxToProcess, action, del_count) continue i = 0 for del_me in listResult: i += 1 - print u' %s message %s for user %s (%s/%s)' % (trashOrDelete, del_me[u'id'], user, i, del_count) - callGAPI(service=gmail.users().messages(), function=trashOrDelete, + print u' %s message %s for user %s (%s/%s)' % (function, del_me[u'id'], user, i, del_count) + callGAPI(service=gmail.users().messages(), function=function, id=del_me[u'id'], userId=u'me') def doDeleteLabel(users): @@ -4884,13 +4890,13 @@ def doWebClips(users): def doVacation(users): subject = message = u'' if sys.argv[4].lower() in true_values: - enable = u'true' + enable = True elif sys.argv[4].lower() in false_values: - enable = u'false' + enable = False else: print u'ERROR: value for "gam vacation" must be true or false, got %s' % sys.argv[4] sys.exit(2) - contacts_only = domain_only = u'false' + contacts_only = domain_only = False start_date = end_date = None i = 5 while i < len(sys.argv): @@ -4901,10 +4907,10 @@ def doVacation(users): message = sys.argv[i+1] i += 2 elif sys.argv[i].lower() == u'contactsonly': - contacts_only = u'true' + contacts_only = True i += 1 elif sys.argv[i].lower() == u'domainonly': - domain_only = u'true' + domain_only = True i += 1 elif sys.argv[i].lower() == u'startdate': start_date = sys.argv[i+1] @@ -5959,9 +5965,9 @@ def doUpdateGroup(): if sys.argv[4].lower() == u'add': body = {u'role': role} body[u'email'] = user_email - result = callGAPI(service=cd.members(), function=u'insert', soft_errors=True, groupKey=group, body=body) + callGAPI(service=cd.members(), function=u'insert', soft_errors=True, groupKey=group, body=body) elif sys.argv[4].lower() == u'update': - result = callGAPI(service=cd.members(), function=u'update', soft_errors=True, groupKey=group, memberKey=user_email, body={u'email': user_email, u'role': role}) + callGAPI(service=cd.members(), function=u'update', soft_errors=True, groupKey=group, memberKey=user_email, body={u'email': user_email, u'role': role}) except googleapiclient.errors.HttpError: pass elif sys.argv[4].lower() == u'sync': @@ -5995,7 +6001,7 @@ def doUpdateGroup(): if user_email != u'*' and user_email.find(u'@') == -1: user_email = u'%s@%s' % (user_email, GC_Values[GC_DOMAIN]) sys.stderr.write(u' removing %s\n' % user_email) - result = callGAPI(service=cd.members(), function=u'delete', soft_errors=True, groupKey=group, memberKey=user_email) + callGAPI(service=cd.members(), function=u'delete', soft_errors=True, groupKey=group, memberKey=user_email) else: i = 4 use_cd_api = False @@ -6493,7 +6499,8 @@ def doGetUserInfo(user_email=None): def doGetGroupInfo(group_name=None): cd = buildGAPIObject(u'directory') gs = buildGAPIObject(u'groupssettings') - get_users = True + getAliases = getUsers = True + getGroups = False if group_name == None: group_name = sys.argv[3] i = 4 @@ -6501,7 +6508,13 @@ def doGetGroupInfo(group_name=None): i = 3 while i < len(sys.argv): if sys.argv[i].lower() == u'nousers': - get_users = False + getUsers = False + i += 1 + elif sys.argv[i].lower() == u'noaliases': + getAliases = False + i += 1 + elif sys.argv[i].lower() == u'groups': + getGroups = True i += 1 else: print u'ERROR: %s is not a valid argument for "gam info group"' % sys.argv[i] @@ -6519,9 +6532,9 @@ def doGetGroupInfo(group_name=None): print u'' print u'Group Settings:' for key, value in basic_info.items(): - if key in [u'kind', u'etag']: + if (key in [u'kind', u'etag']) or ((key == u'aliases') and (not getAliases)): continue - elif type(value) == type(list()): + if type(value) == type(list()): print u' %s:' % key for val in value: print u' %s' % val @@ -6539,7 +6552,14 @@ def doGetGroupInfo(group_name=None): print u' %s: %s' % (key, value) except UnboundLocalError: pass - if get_users: + if getGroups: + groups = callGAPIpages(cd.groups(), u'list', u'groups', + userKey=basic_info[u'email'], fields=u'nextPageToken,groups(name,email)') + if groups: + print u'Groups: ({0})'.format(len(groups)) + for groupm in groups: + print u' %s: %s' % (groupm[u'name'], groupm[u'email']) + if getUsers: members = callGAPIpages(service=cd.members(), function=u'list', items=u'members', groupKey=group_name) print u'Members:' for member in members: @@ -6900,14 +6920,14 @@ def doGetBackupCodes(users): codes = callGAPI(service=cd.verificationCodes(), function=u'list', throw_reasons=[u'invalidArgument', u'invalid'], userKey=user) except googleapiclient.errors.HttpError: codes = None - printBackupCodes(user, codes) + printBackupCodes(user, codes) def doGenBackupCodes(users): cd = buildGAPIObject(u'directory') for user in users: callGAPI(service=cd.verificationCodes(), function=u'generate', userKey=user) codes = callGAPI(service=cd.verificationCodes(), function=u'list', userKey=user) - printBackupCodes(user, codes) + printBackupCodes(user, codes) def doDelBackupCodes(users): cd = buildGAPIObject(u'directory') @@ -7373,21 +7393,23 @@ def output_csv(csv_list, titles, list_type, todrive): import webbrowser webbrowser.open(file_url) -def flatten_json(structure, key="", path="", flattened=None): +def flatten_json(structure, key="", path="", flattened=None, listLimit=None): if flattened == None: flattened = {} - if type(structure) not in(dict, list): + if not isinstance(structure, (dict, list)): flattened[((path + ".") if path else "") + key] = structure elif isinstance(structure, list): for i, item in enumerate(structure): - flatten_json(item, "%d" % i, ".".join(filter(None, [path, key])), flattened) + if listLimit and (i >= listLimit): + break + flatten_json(item, "%d" % i, ".".join(filter(None, [path, key])), flattened=flattened, listLimit=listLimit) else: for new_key, value in structure.items(): if new_key in [u'kind', u'etag']: continue if value == u'1970-01-01T00:00:00.000Z': value = u'Never' - flatten_json(value, new_key, ".".join(filter(None, [path, key])), flattened) + flatten_json(value, new_key, ".".join(filter(None, [path, key])), flattened=flattened, listLimit=listLimit) return flattened def doPrintUsers(): @@ -7444,7 +7466,7 @@ def doPrintUsers(): elif sys.argv[i].lower() == u'query': query = sys.argv[i+1] i += 2 - elif sys.argv[i].lower() in [u'firstname', u'givenname', u'lastname', u'familyName', u'fullname']: + elif sys.argv[i].lower() in [u'firstname', u'givenname', u'lastname', u'familyName', u'fullname', u'name']: user_fields.append(u'name') i += 1 elif sys.argv[i].lower() == u'ou': @@ -7993,7 +8015,7 @@ def doPrintCrosDevices(): todrive = False query = projection = orderBy = sortOrder = None noLists = False - selectAttrib = None + listLimit = selectAttrib = None i = 3 while i < len(sys.argv): my_arg = sys.argv[i].lower().replace(u'_', u'') @@ -8015,6 +8037,9 @@ def doPrintCrosDevices(): selectAttrib = u'activeTimeRanges' noLists = False i += 1 + elif my_arg == u'listlimit': + listLimit = int(sys.argv[i+1]) + i += 2 elif my_arg == u'orderby': orderBy = sys.argv[i+1].lower().replace(u'_', u'') allowed_values = [u'location', u'user', u'lastsync', u'notes', u'serialnumber', u'status', u'supportenddate'] @@ -8051,7 +8076,7 @@ def doPrintCrosDevices(): if all_cros: if (not noLists) and (not selectAttrib): for cros in all_cros: - cros_attributes.append(flatten_json(cros)) + cros_attributes.append(flatten_json(cros, listLimit=listLimit)) for item in cros_attributes[-1]: if item not in cros_attributes[0]: cros_attributes[0][item] = item @@ -8077,7 +8102,9 @@ def doPrintCrosDevices(): cros_attributes[0][xattrib] = xattrib titles.append(xattrib) attribMap[attrib] = xattrib - for item in cros[selectAttrib]: + for i, item in enumerate(cros[selectAttrib]): + if listLimit and(i >= listLimit): + break new_row = row.copy() for attrib in item: if isinstance(item[attrib], (bool, int)): @@ -8664,19 +8691,29 @@ def getUsersToModify(entity_type=None, entity=None, silent=False, return_uids=Fa pass elif entity_type == u'file': users = [] - filename = entity - users = readFile(filename, u'rb').splitlines() - elif entity_type == u'csv': - (filename, column) = entity.split(u':') - file_contents = readFile(filename) - f = StringIO.StringIO(file_contents) - input_file = csv.DictReader(f) + f = openFile(entity) + for row in f: + user = row.strip() + if user: + users.append(user) + closeFile(f) + elif entity_type in [u'csv', u'csvfile']: + try: + (filename, column) = entity.split(u':') + except ValueError: + filename = column = None + if (not filename) or (not column): + systemErrorExit(2, u'Expected {0} FileName:FieldName'.format(entity_type)) + f = openFile(filename) + input_file = csv.DictReader(f, restval=u'') + if column not in input_file.fieldnames: + systemErrorExit(2, MESSAGE_HEADER_NOT_FOUND_IN_CSV_HEADERS.format(column, ','.join(input_file.fieldnames))) users = [] for row in input_file: - if column not in row: - print u'ERROR: %s does not seem to be a header in CSV file %s' % (column, filename) - sys.exit(3) - users.append(row[column]) + user = row[column].strip() + if user: + users.append(user) + closeFile(f) elif entity_type in [u'courseparticipants', u'teachers', u'students']: croom = buildGAPIObject(u'classroom') users = [] @@ -9376,7 +9413,7 @@ try: if command == u'print': for user in users: print user - sys.exit(0) + sys.exit(0) try: if (GC_Values[GC_AUTO_BATCH_MIN] > 0) and (len(users) > GC_Values[GC_AUTO_BATCH_MIN]): items = [] @@ -9444,10 +9481,16 @@ try: sys.exit(2) elif command == u'trash': if sys.argv[4].lower() in [u'message', u'messages']: - doDeleteMessages(trashOrDelete=u'trash', users=users) + doProcessMessages(users, u'trash') else: print u'ERROR: %s is not a valid argument for "gam trash"' % sys.argv[4] sys.exit(2) + elif command == u'untrash': + if sys.argv[4].lower() in [u'message', u'messages']: + doProcessMessages(users, u'untrash') + else: + print u'ERROR: %s is not a valid argument for "gam untrash"' % sys.argv[4] + sys.exit(2) elif command == u'delete' or command == u'del': delWhat = sys.argv[4].lower() if delWhat == u'delegate': @@ -9457,7 +9500,7 @@ try: elif delWhat == u'label': doDeleteLabel(users) elif delWhat in [u'message', u'messages']: - doDeleteMessages(trashOrDelete=u'delete', users=users) + doProcessMessages(users, u'delete') elif delWhat == u'photo': deletePhoto(users) elif delWhat in [u'license', u'licence']: