diff --git a/docs/GamUpdates.md b/docs/GamUpdates.md index bc8ed4a5..66763e8c 100644 --- a/docs/GamUpdates.md +++ b/docs/GamUpdates.md @@ -10,6 +10,11 @@ Add the `-s` option to the end of the above commands to suppress creating the `g See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Windows or other options, including manual installation +### 6.75.05 + +Added option `csv [todrive *]` to `gam archive|delete|modify|spam|trash|untrash messages|threads` +that causes GAM to display the command results in CSV form. + ### 6.75.04 Added a command to print user counts by OrgUnit. By default, all users in the workspace are counted; diff --git a/docs/How-to-Upgrade-from-Standard-GAM.md b/docs/How-to-Upgrade-from-Standard-GAM.md index b0e3962e..3488c759 100644 --- a/docs/How-to-Upgrade-from-Standard-GAM.md +++ b/docs/How-to-Upgrade-from-Standard-GAM.md @@ -335,7 +335,7 @@ writes the credentials into the file oauth2.txt. admin@server:/Users/admin/bin/gamadv-xtd3$ rm -f /Users/admin/GAMConfig/oauth2.txt admin@server:/Users/admin/bin/gamadv-xtd3$ ./gam version WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found -GAMADV-XTD3 6.75.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.75.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.3 64-bit final MacOS Sonoma 14.4.1 x86_64 @@ -1009,7 +1009,7 @@ writes the credentials into the file oauth2.txt. C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt C:\GAMADV-XTD3>gam version WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found -GAMADV-XTD3 6.75.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.75.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.3 64-bit final Windows-10-10.0.17134 AMD64 diff --git a/docs/Users-Gmail-Messages-Threads.md b/docs/Users-Gmail-Messages-Threads.md index 71c3e98d..7f621866 100644 --- a/docs/Users-Gmail-Messages-Threads.md +++ b/docs/Users-Gmail-Messages-Threads.md @@ -360,10 +360,14 @@ Your command line will have: `embedimage file1.jpg image1` embedimage file2.jpg gam archive messages (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_archive ])|(ids ) + [csv [todrive *]] ``` Messages are archived to the group specified by ``. +By default, the command results are displayed as indented keys and values. Use the `csv` option +to display the command results in CSV form. + See below for message selection. ## Export messages/threads @@ -421,20 +425,29 @@ See below for message selection. gam delete messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_delete ])|(ids ) + [csv [todrive *]] gam modify messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_modify ])|(ids ) (addlabel )* (removelabel )* + [csv [todrive *]] gam spam messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_spam ])|(ids ) + [csv [todrive *]] gam trash messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_trash ])|(ids ) + [csv [todrive *]] gam untrash messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_untrash ])|(ids ) + [csv [todrive *]] ``` + +By default, the command results are displayed as indented keys and values. Use the `csv` option +to display the command results in CSV form. + ### Manage a specific set of messages * `ids ` - A list of message ids diff --git a/docs/Version-and-Help.md b/docs/Version-and-Help.md index 27bfcb79..97088504 100644 --- a/docs/Version-and-Help.md +++ b/docs/Version-and-Help.md @@ -3,7 +3,7 @@ Print the current version of Gam with details ``` gam version -GAMADV-XTD3 6.75.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.75.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.3 64-bit final MacOS Sonoma 14.4.1 x86_64 @@ -15,7 +15,7 @@ Time: 2023-06-02T21:10:00-07:00 Print the current version of Gam with details and time offset information ``` gam version timeoffset -GAMADV-XTD3 6.75.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.75.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.3 64-bit final MacOS Sonoma 14.4.1 x86_64 @@ -27,7 +27,7 @@ Your system time differs from www.googleapis.com by less than 1 second Print the current version of Gam with extended details and SSL information ``` gam version extended -GAMADV-XTD3 6.75.04 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.75.05 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.3 64-bit final MacOS Sonoma 14.4.1 x86_64 @@ -64,7 +64,7 @@ MacOS High Sierra 10.13.6 x86_64 Path: /Users/Admin/bin/gamadv-xtd3 Version Check: Current: 5.35.08 - Latest: 6.75.04 + Latest: 6.75.05 echo $? 1 ``` @@ -72,7 +72,7 @@ echo $? Print the current version number without details ``` gam version simple -6.75.04 +6.75.05 ``` In Linux/MacOS you can do: ``` @@ -82,7 +82,7 @@ echo $VER Print the current version of Gam and address of this Wiki ``` gam help -GAM 6.75.04 - https://github.com/taers232c/GAMADV-XTD3 +GAM 6.75.05 - https://github.com/taers232c/GAMADV-XTD3 Ross Scroggs Python 3.12.3 64-bit final MacOS Sonoma 14.4.1 x86_64 diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 5194eb85..5f60f1cd 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -7167,22 +7167,28 @@ gam insert message gam archive messages (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_archive ])|(ids ) + [csv [todrive *]] gam delete messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_delete ])|(ids ) + [csv [todrive *]] gam modify messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_modify ])|(ids ) (addlabel )* (removelabel )* + [csv [todrive *]] gam spam messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_spam ])|(ids ) + [csv [todrive *]] gam trash messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_trash ])|(ids ) + [csv [todrive *]] gam untrash messages|threads (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_untrash ])|(ids ) + [csv [todrive *]] gam export message|messages (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_export ])|(ids ) diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 3ca9ac93..deeea2be 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -2,6 +2,11 @@ Merged GAM-Team version +6.75.05 + +Added option `csv [todrive *]` to `gam archive|delete|modify|spam|trash|untrash messages|threads` +that causes GAM to display the command results in CSV form. + 6.75.04 Added a command to print user counts by OrgUnit. By default, all users in the workspace are counted; diff --git a/src/gam/__init__.py b/src/gam/__init__.py index b4b1d39f..0cbeb1d8 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -67050,14 +67050,27 @@ def _finalizeMessageSelectParameters(parameters, queryOrIdsRequired): # gam archive messages # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_archive ])|(ids ) +# [csv [todrive *]] def archiveMessages(users): + def _processMessageFailed(user, idsList, errMsg, j=0, jcount=0): + if not csvPF: + entityActionFailedWarning([Ent.USER, user, entityType, idsList], errMsg, j, jcount) + else: + csvPF.WriteRow({'User': user, entityHeader: idsList, 'action': Act.Failed(), 'error': errMsg}) + entityType = Ent.MESSAGE + entityHeader = 'id' parameters = _initMessageThreadParameters(entityType, False, 0) group = getEmailAddress() + csvPF = None while Cmd.ArgumentsRemaining(): myarg = getArgument() if _getMessageSelectParameters(myarg, parameters): pass + elif myarg == 'csv': + csvPF = CSVPrintFile(['User', entityHeader, 'action', 'error']) + elif csvPF and myarg == 'todrive': + csvPF.GetTodriveParameters() else: unknownArgumentExit() _finalizeMessageSelectParameters(parameters, False) @@ -67115,7 +67128,7 @@ def archiveMessages(users): j += 1 try: message = callGAPI(service, 'get', - throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT], + throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID], userId='me', id=messageId, format='raw') stream = io.BytesIO() stream.write(base64.urlsafe_b64decode(str(message['raw']))) @@ -67124,20 +67137,31 @@ def archiveMessages(users): throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN], groupId=group, media_body=googleapiclient.http.MediaIoBaseUpload(stream, mimetype='message/rfc822', resumable=True)) - entityActionPerformed([Ent.USER, user, entityType, messageId], j, jcount) + if not csvPF: + entityActionPerformed([Ent.USER, user, entityType, messageId], j, jcount) + else: + csvPF.WriteRow({'User': user, entityHeader: messageId, 'action': Act.Performed()}) except GAPI.serviceNotAvailable: entityServiceNotApplicableWarning(Ent.USER, user, i, count) break except (GAPI.badRequest, GAPI.invalid, GAPI.failedPrecondition, GAPI.forbidden) as e: - entityActionFailedWarning([Ent.USER, user, entityType, messageId], str(e), j, jcount) + _processMessageFailed(user, messageId, str(e), j, jcount) except (GAPI.serviceNotAvailable, GAPI.badRequest): entityServiceNotApplicableWarning(Ent.USER, user, i, count) break - except (GAPI.notFound, GAPI.invalidArgument) as e: - entityActionFailedWarning([Ent.USER, user, entityType, messageId], str(e), j, jcount) + except (GAPI.notFound, GAPI.invalidMessageId) as e: + _processMessageFailed(user, messageId, str(e), j, jcount) Ind.Decrement() + if csvPF: + csvPF.writeCSVfile(f'{Act.ToPerform()} Messages') def _processMessagesThreads(users, entityType): + def _processMessageFailed(user, idsList, errMsg, j=0, jcount=0): + if not csvPF: + entityActionFailedWarning([Ent.USER, user, entityType, idsList], errMsg, j, jcount) + else: + csvPF.WriteRow({'User': user, entityHeader: idsList, 'action': Act.Failed(), 'error': errMsg}) + def _batchDeleteModifyMessages(gmail, function, user, jcount, messageIds, body): mcount = 0 bcount = min(jcount-mcount, GC.Values[GC.MESSAGE_BATCH_SIZE]) @@ -67154,34 +67178,37 @@ def _processMessagesThreads(users, entityType): userId='me', body=body) for messageId in body['ids']: mcount += 1 - entityActionPerformed([Ent.USER, user, entityType, messageId], mcount, jcount) + if not csvPF: + entityActionPerformed([Ent.USER, user, entityType, messageId], mcount, jcount) + else: + csvPF.WriteRow({'User': user, entityHeader: messageId, 'action': Act.Performed()}) except (GAPI.serviceNotAvailable, GAPI.badRequest): mcount += bcount except (GAPI.invalid, GAPI.permissionDenied) as e: - entityActionFailedWarning([Ent.USER, user, entityType, idsList], f'{str(e)} ({mcount+1}-{mcount+bcount}/{jcount})') + _processMessageFailed(user, idsList, f'{str(e)} ({mcount+1}-{mcount+bcount}/{jcount})') mcount += bcount except GAPI.invalidMessageId: - entityActionFailedWarning([Ent.USER, user, entityType, idsList], f'{Msg.INVALID_MESSAGE_ID} ({mcount+1}-{mcount+bcount}/{jcount})') + _processMessageFailed(user, idsList, f'{Msg.INVALID_MESSAGE_ID} ({mcount+1}-{mcount+bcount}/{jcount})') mcount += bcount except GAPI.failedPrecondition: - entityActionFailedWarning([Ent.USER, user, entityType, idsList], f'{Msg.FAILED_PRECONDITION} ({mcount+1}-{mcount+bcount}/{jcount})') + _processMessageFailed(user, idsList, f'{Msg.FAILED_PRECONDITION} ({mcount+1}-{mcount+bcount}/{jcount})') mcount += bcount bcount = min(jcount-mcount, GC.Values[GC.MESSAGE_BATCH_SIZE]) _GMAIL_ERROR_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST, GAPI.INVALID_MESSAGE_ID: Msg.INVALID_MESSAGE_ID, GAPI.FAILED_PRECONDITION: Msg.FAILED_PRECONDITION} - def _handleProcessGmailError(exception, ri): - http_status, reason, message = checkGAPIError(exception) - errMsg = getHTTPError(_GMAIL_ERROR_REASON_TO_MESSAGE_MAP, http_status, reason, message) - entityActionFailedWarning([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT])) def _callbackProcessMessage(request_id, response, exception): ri = request_id.splitlines() if exception is None: - entityActionPerformed([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT])) + if not csvPF: + entityActionPerformed([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT])) + else: + csvPF.WriteRow({'User': ri[RI_ENTITY], entityHeader: ri[RI_ITEM], 'action': Act.Performed()}) else: - _handleProcessGmailError(exception, ri) + http_status, reason, message = checkGAPIError(exception) + _processMessageFailed(ri[RI_ENTITY], ri[RI_ITEM], getHTTPError(_GMAIL_ERROR_REASON_TO_MESSAGE_MAP, http_status, reason, message), int(ri[RI_J]), int(ri[RI_JCOUNT])) def _batchProcessMessagesThreads(service, function, user, jcount, messageIds, **kwargs): svcargs = dict([('userId', 'me'), ('id', None), ('fields', '')]+list(kwargs.items())+GM.Globals[GM.EXTRA_ARGS_LIST]) @@ -67210,6 +67237,8 @@ def _processMessagesThreads(users, entityType): addLabelIds = [] removeLabelNames = [] removeLabelIds = [] + csvPF = None + entityHeader = 'id' if entityType == Ent.MESSAGE else 'threadId' while Cmd.ArgumentsRemaining(): myarg = getArgument() if _getMessageSelectParameters(myarg, parameters): @@ -67218,6 +67247,10 @@ def _processMessagesThreads(users, entityType): addLabelNames.append(getString(Cmd.OB_LABEL_NAME)) elif (function == 'modify') and (myarg == 'removelabel'): removeLabelNames.append(getString(Cmd.OB_LABEL_NAME)) + elif myarg == 'csv': + csvPF = CSVPrintFile(['User', entityHeader, 'action', 'error']) + elif csvPF and myarg == 'todrive': + csvPF.GetTodriveParameters() else: unknownArgumentExit() _finalizeMessageSelectParameters(parameters, True) @@ -67285,32 +67318,44 @@ def _processMessagesThreads(users, entityType): kwargs = {} _batchProcessMessagesThreads(service, function, user, jcount, messageIds, **kwargs) Ind.Decrement() + if csvPF: + csvPF.writeCSVfile(f'{Act.ToPerform()} Messages') # gam delete message|messages # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_delete ])|(ids ) +# [csv [todrive *]] # gam modify message|messages # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_modify ])|(ids ) # (addlabel )* (removelabel )* +# [csv [todrive *]] # gam spam message|messages # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_spam ])|(ids ) +# [csv [todrive *]] # gam trash message|messages # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_trash ])|(ids ) +# [csv [todrive *]] # gam untrash message|messages # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_untrash ])|(ids ) +# [csv [todrive *]] def processMessages(users): _processMessagesThreads(users, Ent.MESSAGE) # gam delete thread|threads # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_delete ])|(ids ) +# [csv [todrive *]] # gam modify thread|threads # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_modify ])|(ids ) # (addlabel )* (removelabel )* +# [csv [todrive *]] # gam spam thread|threads # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_spam ])|(ids ) +# [csv [todrive *]] # gam trash thread|threads # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_trash ])|(ids ) +# [csv [todrive *]] # gam untrash thread|threads # (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_untrash ])|(ids ) +# [csv [todrive *]] def processThreads(users): _processMessagesThreads(users, Ent.THREAD) @@ -67397,7 +67442,7 @@ def exportMessagesThreads(users, entityType): k += 1 try: result = callGAPI(gmail.users().messages(), 'get', - throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT], + throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID], userId='me', id=messageId, format='raw') if targetName: msgName = targetName.replace('#id#', messageId) @@ -67412,7 +67457,7 @@ def exportMessagesThreads(users, entityType): except (GAPI.serviceNotAvailable, GAPI.badRequest): entityServiceNotApplicableWarning(Ent.USER, user, i, count) break - except (GAPI.notFound, GAPI.invalidArgument) as e: + except (GAPI.notFound, GAPI.invalidMessageId) as e: entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], str(e), k, kcount) continue if entityType == Ent.THREAD: @@ -67514,7 +67559,7 @@ def forwardMessagesThreads(users, entityType): if entityType == Ent.THREAD: try: result = callGAPI(gmail.users().threads(), 'get', - throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT], + throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID], userId='me', id=entityId, fields='messages(id)') messageIds = [message['id'] for message in result['messages']] kcount = len(messageIds) @@ -67524,7 +67569,7 @@ def forwardMessagesThreads(users, entityType): except (GAPI.serviceNotAvailable, GAPI.badRequest): entityServiceNotApplicableWarning(Ent.USER, user, i, count) break - except (GAPI.notFound, GAPI.invalidArgument) as e: + except (GAPI.notFound, GAPI.invalidMessageId) as e: entityActionFailedWarning([Ent.USER, user, Ent.THREAD, entityId], str(e), j, jcount) continue else: @@ -67535,7 +67580,7 @@ def forwardMessagesThreads(users, entityType): k += 1 try: result = callGAPI(gmail.users().messages(), 'get', - throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT], + throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID], userId='me', id=messageId, format='raw') for encoding in encodings: try: @@ -67573,7 +67618,7 @@ def forwardMessagesThreads(users, entityType): except (GAPI.serviceNotAvailable, GAPI.badRequest): entityServiceNotApplicableWarning(Ent.USER, user, i, count) break - except (GAPI.notFound, GAPI.invalidArgument) as e: + except (GAPI.notFound, GAPI.invalidMessageId) as e: entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], str(e), k, kcount) continue if entityType == Ent.THREAD: