Compare commits

...

10 Commits

Author SHA1 Message Date
Ross Scroggs
2fc8c8d718 gcpdetails/scopes cleanup #1891 2026-03-19 07:22:14 -07:00
Ross Scroggs
a3e5f7b504 gcpdetails/scopes cleanup #1891
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2026-03-18 17:47:43 -07:00
Ross Scroggs
4137b3b77b gcpdetails/scopes cleanup #1891
Some checks failed
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Push wiki / pushwiki (push) Has been cancelled
2026-03-18 17:22:57 -07:00
Ross Scroggs
ce47c9bc7c gcpdetails/scopes cleanup #1891 2026-03-18 16:39:20 -07:00
Ross Scroggs
d302563045 Merge branch 'main' of https://github.com/GAM-team/GAM 2026-03-18 13:19:23 -07:00
Jay Lee
c5f4bb18fa Merge branch 'main' of https://github.com/GAM-team/GAM 2026-03-18 20:11:57 +00:00
Jay Lee
0fdcab4c4f Initial gcpdetails on print token. #1891 2026-03-18 20:11:49 +00:00
Ross Scroggs
130a245906 Added gam <UserTypeEntity> sendreply 2026-03-18 10:50:49 -07:00
Ross Scroggs
d8bf368c92 Added gam <UserTypeEntity> sendreply 2026-03-18 10:50:37 -07:00
Ross Scroggs
e7b238e85e Update Send-Email.md
Some checks failed
Push wiki / pushwiki (push) Has been cancelled
Build and test GAM / build (false, build, 1, Build Intel Ubuntu Jammy, ubuntu-22.04) (push) Has been cancelled
Build and test GAM / build (false, build, 10, Build x86_64 macOS 15, macos-15-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 11, Build x86_64 macOS 26, macos-26-intel) (push) Has been cancelled
Build and test GAM / build (false, build, 12, Build Arm MacOS 26, macos-26) (push) Has been cancelled
Build and test GAM / build (false, build, 13, Build Intel Windows, windows-2025-vs2026) (push) Has been cancelled
Build and test GAM / build (false, build, 14, Build Arm Windows, windows-11-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 2, Build Intel Ubuntu Noble, ubuntu-24.04) (push) Has been cancelled
Build and test GAM / build (false, build, 3, Build Arm Ubuntu Noble, ubuntu-24.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 4, Build Arm Ubuntu Jammy, ubuntu-22.04-arm) (push) Has been cancelled
Build and test GAM / build (false, build, 5, Build Intel StaticX Legacy, ubuntu-22.04, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 6, Build Arm StaticX Legacy, ubuntu-22.04-arm, yes) (push) Has been cancelled
Build and test GAM / build (false, build, 8, Build Arm MacOS 14, macos-14) (push) Has been cancelled
Build and test GAM / build (false, build, 9, Build Arm MacOS 15, macos-15) (push) Has been cancelled
Build and test GAM / build (false, test, 15, Test Python 3.10, ubuntu-24.04, 3.10) (push) Has been cancelled
Build and test GAM / build (false, test, 16, Test Python 3.11, ubuntu-24.04, 3.11) (push) Has been cancelled
Build and test GAM / build (false, test, 17, Test Python 3.12, ubuntu-24.04, 3.12) (push) Has been cancelled
Build and test GAM / build (false, test, 18, Test Python 3.13, ubuntu-24.04, 3.13) (push) Has been cancelled
Build and test GAM / build (false, test, 19, Test Python 3.15-dev, ubuntu-24.04, 3.15-dev) (push) Has been cancelled
Build and test GAM / build (true, test, 20, Test Python 3.14 freethread, ubuntu-24.04, 3.14) (push) Has been cancelled
Build and test GAM / publish (push) Has been cancelled
Check for Google Root CA Updates / check-certs (push) Has been cancelled
2026-03-18 07:28:02 -07:00
9 changed files with 340 additions and 144 deletions

View File

@@ -4926,10 +4926,10 @@ gam print schema|schemas [todrive <ToDriveAttribute>*]
gam sendemail [recipient|to] <RecipientEntity>
[from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
[subject <String>] [<MessageContent>]
[subject <String>] [<MessageContent>] [html [<Boolean>]]
(replace <Tag> <String>)*
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
(attach <FileName> [charset <Charset>])*
(embedimage <FileName> <String>)*
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
@@ -4937,10 +4937,10 @@ gam sendemail [recipient|to] <RecipientEntity>
gam <UserTypeEntity> sendemail recipient|to <RecipientEntity>
[replyto <EmailAddress>]
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
[subject <String>] [<MessageContent>]
[subject <String>] [<MessageContent>] [html [<Boolean>]]
(replace <Tag> <String>)*
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
(attach <FileName> [charset <Charset>])*
(embedimage <FileName> <String>)*
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
@@ -4948,14 +4948,21 @@ gam <UserTypeEntity> sendemail recipient|to <RecipientEntity>
gam <UserTypeEntity> sendemail from <EmailAddress>
[replyto <EmailAddress>]
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
[subject <String>] [<MessageContent>]
[subject <String>] [<MessageContent>] [html [<Boolean>]]
(replace <Tag> <String>)*
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
[html [<Boolean>]] (attach <FileName> [charset <Charset>])*
(attach <FileName> [charset <Charset>])*
(embedimage <FileName> <String>)*
[newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
[threadid <String>]
gam <UserTypeEntity> sendreply
(((query <QueryGmail> [querytime<String> <Date>]*) [or|and])+) | (ids <MessageIDEntity>)
[replyto <EmailAddress>]
[subject <String>] [<MessageContent>] [html [<Boolean>]]
(attach <FileName> [charset <CharSet>])*
(embedimage <FileName> <String>)*
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
# Shared Drives - Administrator
@@ -8907,17 +8914,17 @@ gam <UserTypeEntity> delete tokens clientid <ClientID>
gam <UserTypeEntity> print tokens|token [todrive <ToDriveAttribute>*] [clientid <ClientID>]
[usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)]
[delimiter <Character>]
[delimiter <Character>] [gcpdetails]
gam <UserTypeEntity> show tokens|token|3lo|oauth [clientid <ClientID>]
[usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)]
[delimiter <Character>]
[delimiter <Character>] [gcpdetails]
gam print tokens|token [todrive <ToDriveAttribute>*] [clientid <ClientID>]
[usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)]
[delimiter <Character>]
[delimiter <Character>] [gcpdetails]
[<UserTypeEntity>]
gam show tokens|token [clientid <ClientID>]
[usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)]
[delimiter <Character>]
[delimiter <Character>] [gcpdetails]
[<UserTypeEntity>]
# Users - YouTube

View File

@@ -1,3 +1,33 @@
7.37.00
Added new client access scopes used by `gam print tokens`.
```
[*] 52) Resource Manager API - Organizations readonly
[*] 53) Resource Manager API - Projects readonly
```
Added option `gcpdetails` to `gam print tokens` that uses these scopes to get additional project information.
7.36.03
Added command to send email replies that causes Gmail to recognize the message
in conversation mode for the user sending the reply and the user receiving the reply;
GAM supplies the necessary headers and options.
```
gam <UserTypeEntity> sendreply
(((query <QueryGmail> [querytime<String> <Date>]*) [or|and])+) | (ids <MessageIDEntity>)
[replyto <EmailAddress>]
[subject <String>] [<MessageContent>] [html [<Boolean>]]
(attach <FileName> [charset <CharSet>])*
(embedimage <FileName> <String>)*
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
gam user user@domain.com sendreply query "rfc822MsgId:<CAAMmEdqj43...1OsQ@mail.gmail.com>" textmessage "Thanks for the information"
gam user user@domain.com sendreply ids 19cfc3506c02c22b textmessage "Thanks for the information"
```
* See: https://github.com/GAM-team/GAM/wiki/Send-Email#conversation-mode
7.36.02
Added option `threadid <String>` to `gam [<UserTypeEntity>] sendemail` that causes Gmail to recognize the message

View File

@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
"""
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
__version__ = '7.36.02'
__version__ = '7.37.00'
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
# pylint: disable=wrong-import-position
@@ -5681,15 +5681,9 @@ def buildGAPIObject(api, credentials=None):
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(cache=GM.Globals[GM.CACHE_DIR]))
service = getService(api, httpObj)
if not GC.Values[GC.ENABLE_DASA]:
try:
API_Scopes = set(list(service._rootDesc['auth']['oauth2']['scopes']))
except KeyError:
if api == API.VAULT:
API_Scopes = set(API.VAULT_SCOPES)
elif api == API.BUSINESSACCOUNTMANAGEMENT:
API_Scopes = {API.BUSINESSACCOUNTMANAGEMENT_SCOPE}
else:
API_Scopes = set()
discovery_scopes = list(service._rootDesc.get('auth', {}).get('oauth2', {}).get('scopes', {}).keys())
extra_scopes = API.EXTRA_SCOPES.get(api, [])
API_Scopes = set(discovery_scopes + extra_scopes)
GM.Globals[GM.CURRENT_CLIENT_API] = api
GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = API_Scopes.intersection(GM.Globals[GM.CREDENTIALS_SCOPES])
if api not in API.SCOPELESS_APIS and not GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]:
@@ -7347,7 +7341,8 @@ def _addEmbeddedImagesToMessage(message, embeddedImages):
# Send an email
def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msgFrom=None, msgReplyTo=None,
html=False, charset=UTF8, attachments=None, embeddedImages=None,
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None, threadId=None):
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None, threadId=None,
action=Act.SENDEMAIL):
def checkResult(entityType, recipients):
if not recipients:
return
@@ -7403,12 +7398,13 @@ def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msg
if mailBox is None:
mailBox = msgFromAddr
_, mailBoxAddr = cleanAddr(mailBox)
action = Act.Get()
Act.Set(Act.SENDEMAIL)
parentAction = Act.Get()
Act.Set(action)
if not GC.Values[GC.SMTP_HOST]:
if not clientAccess:
userId, gmail = buildGAPIServiceObject(API.GMAIL, mailBoxAddr)
if not gmail:
Act.Set(parentAction)
return
else:
userId = mailBoxAddr
@@ -7450,7 +7446,7 @@ def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msg
server.quit()
except Exception:
pass
Act.Set(action)
Act.Set(parentAction)
def addFieldToFieldsList(fieldName, fieldsChoiceMap, fieldsList):
fields = fieldsChoiceMap[fieldName.lower()]
@@ -10820,14 +10816,14 @@ def getScopesFromUser(scopesList, clientAccess, currentScopes=None):
numScopes = len(scopesList)
for a_scope in scopesList:
oauth2_menu += f"[%%s] %2d) {a_scope['name']}"
if a_scope['subscopes']:
if a_scope.get('subscopes'):
oauth2_menu += f' (supports {" and ".join(a_scope["subscopes"])})'
oauth2_menu += '\n'
oauth2_menu += '''
Select an unselected scope [ ] by entering a number; yields [*]
For scopes that support readonly, enter a number and an 'r' to grant read-only access; yields [R]
For scopes that support action, enter a number and an 'a' to grant action-only access; yields [A]
Clear read-only access [R] or action-only access [A] from a scope by entering a number; yields [*]
For scopes that optionally support readonly, enter a number and an 'r' to grant readonly access; yields [R]
For scopes that optionally support actiononly, enter a number and an 'a' to grant actiononly access; yields [A]
Clear readonly access [R] or actiononly access [A] from a scope by entering a number; yields [*]
Unselect a selected scope [*] by entering a number; yields [ ]
Select all default scopes by entering an 's'; yields [*] for default scopes, [ ] for others
Unselect all scopes by entering a 'u'; yields [ ] for all scopes
@@ -10848,15 +10844,16 @@ Continue to authorization by entering a 'c'
for a_scope in scopesList:
selectedScopes[i] = ' '
possibleScope = a_scope['scope']
subScopes = a_scope.get('subscopes', [])
for currentScope in currentScopes:
if currentScope == possibleScope:
selectedScopes[i] = '*'
break
if 'readonly' in a_scope['subscopes']:
if 'readonly' in subScopes:
if currentScope == possibleScope+'.readonly':
selectedScopes[i] = 'R'
break
if 'action' in a_scope['subscopes']:
if 'actiononly' in subScopes:
if currentScope == possibleScope+'.action':
selectedScopes[i] = 'A'
break
@@ -10867,13 +10864,14 @@ Continue to authorization by entering a 'c'
selectedScopes[i] = ' '
api = a_scope['api']
possibleScope = a_scope['scope']
subScopes = a_scope.get('subscopes', [])
if api in currentScopes:
if not isinstance(possibleScope, list):
for scope in currentScopes[api]:
if scope == possibleScope:
selectedScopes[i] = '*'
break
if 'readonly' in a_scope['subscopes']:
if 'readonly' in subScopes:
if (scope == possibleScope+'.readonly') or (scope == a_scope.get('roscope')):
selectedScopes[i] = 'R'
break
@@ -10914,12 +10912,12 @@ Continue to authorization by entering a 'c'
selection = int(selection)
if isinstance(selection, int) and selection < numScopes:
if mode == 'R':
if 'readonly' not in scopesList[selection]['subscopes']:
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support read-only mode!\n')
if 'readonly' not in scopesList[selection].get('subscopes',[]):
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support readonly mode!\n')
continue
elif mode == 'A':
if 'action' not in scopesList[selection]['subscopes']:
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support action-only mode!\n')
if 'actiononly' not in scopesList[selection].get('subscopes', []):
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support actiononly mode!\n')
continue
elif selectedScopes[selection] != '*':
mode = '*'
@@ -11361,9 +11359,10 @@ def doOAuthCreate():
if uscope in {'openid', 'email', API.USERINFO_EMAIL_SCOPE, 'profile', API.USERINFO_PROFILE_SCOPE}:
continue
for scope in scopesList:
subScopes = scope.get('subscopes', [])
if ((uscope == scope['scope']) or
(uscope.endswith('.action') and 'action' in scope['subscopes']) or
(uscope.endswith('.readonly') and 'readonly' in scope['subscopes'])):
(uscope.endswith('.action') and 'actiononly' in subScopes) or
(uscope.endswith('.readonly') and 'readonly' in subScopes)):
scopes.append(uscope)
break
else:
@@ -11996,12 +11995,13 @@ def getGCPOrg(crm, login_hint, login_domain):
try:
getorg = callGAPI(crm.organizations(), 'search',
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
query=f'domain:{login_domain}')
query=f'domain:{login_domain}',
pageSize=1, fields='organizations/name')
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
entityActionFailedExit([Ent.USER, login_hint, Ent.DOMAIN, login_domain], str(e))
try:
organization = getorg['organizations'][0]['name']
sys.stdout.write(Msg.YOUR_ORGANIZATION_NAME_IS.format(organization))
# sys.stdout.write(Msg.YOUR_ORGANIZATION_NAME_IS.format(organization))
return organization
except (KeyError, IndexError):
systemErrorExit(3, Msg.YOU_HAVE_NO_RIGHTS_TO_CREATE_PROJECTS_AND_YOU_ARE_NOT_A_SUPER_ADMIN)
@@ -15391,32 +15391,35 @@ def getRecipients():
return [normalizeEmailAddressOrUID(emailAddress, noUid=True, noLower=True) for emailAddress in recipients]
return getNormalizedEmailAddressEntity(shlexSplit=True, noLower=True)
# gam sendemail [recipient|to] <RecipientEntity> [from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
# gam sendemail [recipient|to] <RecipientEntity>
# [from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
# [subject <String>] [<MessageContent>]
# [subject <String>] [<MessageContent>] [html [<Boolean>]]
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
# (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
# [threadid <String>]
# gam <UserTypeEntity> sendemail recipient|to <RecipientEntity> [replyto <EmailAddress>]
# gam <UserTypeEntity> sendemail recipient|to <RecipientEntity>
# [replyto <EmailAddress>]
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
# [subject <String>] [<MessageContent>]
# [subject <String>] [<MessageContent>] [html [<Boolean>]]
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
# (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
# [threadid <String>]
# gam <UserTypeEntity> sendemail from <EmailAddress> [replyto <EmailAddress>]
# gam <UserTypeEntity> sendemail from <EmailAddress>
# [replyto <EmailAddress>]
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
# [subject <String>] [<MessageContent>]
# [subject <String>] [<MessageContent> ][html [<Boolean>]]
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
# (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
@@ -15537,6 +15540,125 @@ def doSendEmail(users=None):
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, mailBox=mailBox, threadId=threadId)
Ind.Decrement()
# gam <UserTypeEntity> sendreply
# (((query <QueryGmail> [querytime<String> <Date>]*) [or|and])+) | (ids <MessageIDEntity>)
# [replyto <EmailAddress>]
# [subject <String>] [<MessageContent>] [html [<Boolean>]]
# (attach <FileName> [charset <CharSet>])*
# (embedimage <FileName> <String>)*
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
def doSendReply(users):
def _getHeaderValue(name):
for header in messageInfo['payload']['headers']:
if name == header['name']:
return _decodeHeader(header['value'])
return ''
notify = {'subject': '', 'message': '', 'html': False, 'charset': UTF8}
query = ''
queryTimes = {}
messageIds = []
msgHeaders = {}
msgReplyTo = None
attachments = []
embeddedImages = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'query':
selectLocation = Cmd.Location()
if query:
query += ' '
query += f'({getString(Cmd.OB_QUERY)})'
elif myarg.startswith('querytime'):
queryTimes[myarg] = getDateOrDeltaFromNow().replace('-', '/')
elif myarg in {'or', 'and'}:
if query:
query += f' {myarg.upper()}'
elif myarg == 'ids':
selectLocation = Cmd.Location()
messageIds = getEntityList(Cmd.OB_MESSAGE_ID)
elif myarg == 'subject':
notify['subject'] = getString(Cmd.OB_STRING)
elif myarg in SORF_MSG_FILE_ARGUMENTS:
notify['message'], notify['charset'], notify['html'] = getStringOrFile(myarg)
elif myarg == 'replyto':
msgReplyTo = getString(Cmd.OB_EMAIL_ADDRESS)
elif myarg == 'html':
notify['html'] = getBoolean()
elif myarg == 'attach':
attachments.append((getFilename(), getCharSet()))
elif myarg == 'embedimage':
embeddedImages.append((getFilename(), getString(Cmd.OB_STRING)))
elif myarg in SMTP_HEADERS_MAP:
if myarg in SMTP_DATE_HEADERS:
msgDate, _, _ = getTimeOrDeltaFromNow(True)
msgHeaders[SMTP_HEADERS_MAP[myarg]] = formatdate(time.mktime(msgDate.timetuple()) + msgDate.microsecond/1E6, True)
else:
msgHeaders[SMTP_HEADERS_MAP[myarg]] = getString(Cmd.OB_STRING)
elif myarg == 'header':
header = getString(Cmd.OB_STRING, minLen=1)
msgHeaders[SMTP_HEADERS_MAP.get(header.lower(), header)] = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
if query and messageIds:
Cmd.SetLocation(selectLocation-1)
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('query <QueryGmail>', 'ids <MessageIDEntity>'))
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '\n')
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
if not gmail:
continue
try:
if query:
printGettingAllEntityItemsForWhom(Ent.MESSAGE, user, i, count, query=query)
listResult = callGAPIpages(gmail.users().messages(), 'list', 'messages',
pageMessage=getPageMessageForWhom(),
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
userId='me', q=query, fields='nextPageToken,messages(id)',
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
messageIds = [message['id'] for message in listResult]
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
continue
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
continue
jcount = len(messageIds)
if jcount == 0:
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], Ent.MESSAGE, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(Ent.MESSAGE)), i, count)
setSysExitRC(NO_ENTITIES_FOUND_RC)
continue
entityPerformActionModifierNumItems([Ent.USER, user], Act.MODIFIER_TO, jcount, Ent.RECIPIENT, i, count)
Ind.Increment()
j = 0
for messageId in messageIds:
j += 1
try:
messageInfo = callGAPI(gmail.users().messages(), 'get',
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
userId='me', id=messageId, fields='id,threadId,payload(headers)')
threadId = messageInfo['threadId']
msgHeaders['References'] = msgHeaders['In-Reply-To'] = _getHeaderValue('Message-ID')
msgSubject = notify['subject'] if notify['subject'] else f"Re: {_getHeaderValue('Subject')}"
recipient = _getHeaderValue('From')
send_email(msgSubject, notify['message'], recipient, j, jcount,
msgFrom=user, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, threadId=threadId,
action=Act.SENDREPLY)
except GAPI.notFound:
entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], Msg.DOES_NOT_EXIST, j, jcount)
except GAPI.invalidMessageId:
entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], Msg.INVALID_MESSAGE_ID, j, jcount)
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
break
except GAPI.serviceNotAvailable:
userGmailServiceNotEnabledWarning(user, i, count)
break
Ind.Decrement()
ADDRESS_FIELDS_PRINT_ORDER = ['contactName', 'organizationName', 'addressLine1', 'addressLine2', 'addressLine3', 'locality', 'region', 'postalCode', 'countryCode']
def _showCustomerAddressPhoneNumber(customerInfo):
@@ -72321,6 +72443,29 @@ def _printShowTokens(entityType, users):
Ind.Decrement()
Ind.Decrement()
def project_from_client_id(client_id):
match = re.search(r'^\d+', client_id)
return match.group()
def get_gcp_info(results):
for result in results:
result['project'] = project_from_client_id(result.get('clientId'))
if result['project'] in internal_projects:
result['internal'] = True
continue
try:
results = callGAPI(crm1.projects(), 'getAncestry',
throwReasons=[GAPI.PERMISSION_DENIED],
projectId=result['project'])
for ancestor in results.get('ancestor', []):
if ancestor.get('resourceId', {}).get('type') == 'organization' and ancestor.get('resourceId', {}).get('id') == org_id:
result['internal'] = True
internal_projects.add(result['project'])
except GAPI.permissionDenied:
# we don't have permission to get project. This might be an external project
# or it might be an internal project we don't have rights to get.
pass
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile() if Act.csvFormat() else None
clientId = None
@@ -72329,6 +72474,8 @@ def _printShowTokens(entityType, users):
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
aggregateTokensById = {}
tokenNameIdMap = None
getGCPDetails = False
extra_titles = []
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if csvPF and myarg == 'todrive':
@@ -72345,6 +72492,9 @@ def _printShowTokens(entityType, users):
aggregateUsersBy = 'user'
elif myarg == 'delimiter':
delimiter = getCharacter()
elif myarg == 'gcpdetails':
getGCPDetails = True
extra_titles = ['project', 'internal']
elif not entityType:
Cmd.Backup()
entityType, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
@@ -72354,9 +72504,9 @@ def _printShowTokens(entityType, users):
users = getItemsToModify(Cmd.ENTITY_ALL_USERS_NS, None)
if csvPF:
if not aggregateUsersBy:
csvPF.SetTitles(['user']+TOKENS_FIELDS_TITLES)
csvPF.SetTitles(['user'] + TOKENS_FIELDS_TITLES + extra_titles)
elif aggregateUsersBy != 'user':
csvPF.SetTitles(TOKENS_AGGREGATE_FIELDS_TITLES)
csvPF.SetTitles(TOKENS_AGGREGATE_FIELDS_TITLES + extra_titles)
else:
csvPF.SetTitles(['user', 'tokenCount'])
else:
@@ -72364,6 +72514,13 @@ def _printShowTokens(entityType, users):
tokenTitle = TOKENS_TITLE_MAP[orderBy]
else:
tokenTitle = TOKENS_TITLE_MAP[aggregateUsersBy]
if getGCPDetails:
internal_projects = set() # cache
crm = buildGAPIObject('cloudresourcemanager')
crm1 = buildGAPIObject('cloudresourcemanagerv1')
admin_email = _getAdminEmail()
admin_domain = getEmailAddressDomain(admin_email)
org_id = getGCPOrg(crm, admin_email, admin_domain).split('/')[1]
fields = ','.join(TOKENS_FIELDS_TITLES)
i, count, users = getEntityArgument(users)
for user in users:
@@ -72385,6 +72542,8 @@ def _printShowTokens(entityType, users):
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.BAD_REQUEST,
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
userKey=user, fields=f'items({fields})')
if getGCPDetails:
get_gcp_info(results)
if not aggregateUsersBy:
if not csvPF:
jcount = len(results)
@@ -80845,6 +81004,7 @@ USER_COMMANDS = {
'profile': (Act.SET, setProfile),
'sendas': (Act.ADD, createUpdateSendAs),
'sendemail': (Act.SENDEMAIL, doSendEmail),
'sendreply': (Act.SENDREPLY, doSendReply),
'signature': (Act.SET, setSignature),
'signout': (Act.SIGNOUT, signoutTurnoff2SVUsers),
'turnoff2sv': (Act.TURNOFF2SV, signoutTurnoff2SVUsers),

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2024 Ross Scroggs All Rights Reserved.
# Copyright (C) 2026 Ross Scroggs All Rights Reserved.
#
# All Rights Reserved.
#
@@ -107,6 +107,7 @@ class GamAction():
SAVE = 'save'
SEND = 'send'
SENDEMAIL = 'snem'
SENDREPLY = 'sner'
SET = 'set '
SETUP = 'setu'
SHARE = 'shar'
@@ -225,6 +226,7 @@ class GamAction():
SAVE: ['Saved', 'Save'],
SEND: ['Sent', 'Send'],
SENDEMAIL: ['Email Sent', 'Send Email'],
SENDREPLY: ['Reply Sent', 'Send Reply'],
SET: ['Set', 'Set'],
SETUP: ['Set Up', 'Set Up'],
SHARE: ['Shared', 'Share'],

View File

@@ -54,6 +54,7 @@ CLOUDIDENTITY_POLICY = 'cloudidentitypolicy'
CLOUDIDENTITY_POLICY_BETA = 'cloudidentitypolicybeta'
CLOUDIDENTITY_USERINVITATIONS = 'cloudidentityuserinvitations'
CLOUDRESOURCEMANAGER = 'cloudresourcemanager'
CLOUDRESOURCEMANAGERV1 = 'cloudresourcemanagerv1'
CONTACTS = 'contacts'
CONTACTDELEGATION = 'contactdelegation'
DATATRANSFER = 'datatransfer'
@@ -103,7 +104,6 @@ TASKS = 'tasks'
VAULT = 'vault'
YOUTUBE = 'youtube'
#
BUSINESSACCOUNTMANAGEMENT_SCOPE = 'https://www.googleapis.com/auth/business.manage'
CHROMEVERSIONHISTORY_URL = 'https://versionhistory.googleapis.com/v1/chrome/platforms'
DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive'
DRIVE_FILE_SCOPE = 'https://www.googleapis.com/auth/drive.file'
@@ -119,7 +119,6 @@ STORAGE_READONLY_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_only'
STORAGE_READWRITE_SCOPE = 'https://www.googleapis.com/auth/devstorage.read_write'
USERINFO_EMAIL_SCOPE = 'https://www.googleapis.com/auth/userinfo.email' # email
USERINFO_PROFILE_SCOPE = 'https://www.googleapis.com/auth/userinfo.profile' # profile
VAULT_SCOPES = ['https://www.googleapis.com/auth/ediscovery', 'https://www.googleapis.com/auth/ediscovery.readonly']
REQUIRED_SCOPES = [USERINFO_EMAIL_SCOPE, USERINFO_PROFILE_SCOPE]
REQUIRED_SCOPES_SET = set(REQUIRED_SCOPES)
NUM_CLIENT_SCOPES_ERROR_LIMIT = 48
@@ -138,6 +137,21 @@ SCOPELESS_APIS = {
SERVICEACCOUNTLOOKUP,
}
#
# Scopes not in the discovery doc that are still valid for the API.
EXTRA_SCOPES = {
BUSINESSACCOUNTMANAGEMENT: ['https://www.googleapis.com/auth/business.manage'],
CLOUDRESOURCEMANAGER: ['https://www.googleapis.com/auth/cloudplatformfolders',
'https://www.googleapis.com/auth/cloudplatformfolders.readonly',
'https://www.googleapis.com/auth/cloudplatformprojects',
'https://www.googleapis.com/auth/cloudplatformprojects.readonly',
'https://www.googleapis.com/auth/cloudplatformorganizations',
'https://www.googleapis.com/auth/cloudplatformorganizations.readonly',
],
VAULT: ['https://www.googleapis.com/auth/ediscovery', 'https://www.googleapis.com/auth/ediscovery.readonly'],
}
EXTRA_SCOPES[CLOUDRESOURCEMANAGERV1] = EXTRA_SCOPES[CLOUDRESOURCEMANAGER]
APIS_NEEDING_ACCESS_TOKEN = {
CBCM: ['https://www.googleapis.com/auth/admin.directory.device.chromebrowsers']
}
@@ -250,7 +264,8 @@ _INFO = {
CLOUDIDENTITY_POLICY: {'name': 'Cloud Identity API - Policy', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDIDENTITY_POLICY_BETA: {'name': 'Cloud Identity API - Policy Beta', 'version': 'v1beta1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDIDENTITY_USERINVITATIONS: {'name': 'Cloud Identity API - User Invitations', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'},
CLOUDRESOURCEMANAGER: {'name': 'Cloud Resource Manager API v3', 'version': 'v3', 'v2discovery': True},
CLOUDRESOURCEMANAGER: {'name': 'Resource Manager API v3', 'version': 'v3', 'v2discovery': True},
CLOUDRESOURCEMANAGERV1: {'name': 'Resource Manager API v1', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudresourcemanager'},
CONTACTS: {'name': 'Contacts API', 'version': 'v3', 'v2discovery': False},
CONTACTDELEGATION: {'name': 'Contact Delegation API', 'version': 'v1', 'v2discovery': True, 'localjson': True},
DATATRANSFER: {'name': 'Data Transfer API', 'version': 'datatransfer_v1', 'v2discovery': True, 'mappedAPI': 'admin'},
@@ -305,9 +320,8 @@ READONLY = ['readonly',]
_CLIENT_SCOPES = [
{'name': 'Business Account Management API',
'api': BUSINESSACCOUNTMANAGEMENT,
'subscopes': [],
'offByDefault': True,
'scope': BUSINESSACCOUNTMANAGEMENT_SCOPE},
'scope': EXTRA_SCOPES[BUSINESSACCOUNTMANAGEMENT]},
{'name': 'Calendar API',
'api': CALENDAR,
'subscopes': READONLY,
@@ -316,21 +330,18 @@ _CLIENT_SCOPES = [
'api': CBCM,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/admin.directory.device.chromebrowsers'},
{'name': 'Chrome Management API - read only',
{'name': 'Chrome Management API - readonly',
'api': CHROMEMANAGEMENT,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/chrome.management.reports.readonly'},
{'name': 'Chrome Management API - AppDetails read only',
{'name': 'Chrome Management API - AppDetails readonly',
'api': CHROMEMANAGEMENT_APPDETAILS,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/chrome.management.appdetails.readonly'},
{'name': 'Chrome Management API - Profiles',
'api': CHROMEMANAGEMENT_CHROMEPROFILES,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/chrome.management.profiles'},
{'name': 'Chrome Management API - Telemetry read only',
{'name': 'Chrome Management API - Telemetry readonly',
'api': CHROMEMANAGEMENT_TELEMETRY,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/chrome.management.telemetry.readonly'},
{'name': 'Chrome Policy API',
'api': CHROMEPOLICY,
@@ -342,7 +353,6 @@ _CLIENT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/admin.chrome.printers'},
{'name': 'Chrome Version History API',
'api': CHROMEVERSIONHISTORY,
'subscopes': [],
'scope': ''},
{'name': 'Classroom API - Courses',
'api': CLASSROOM,
@@ -370,11 +380,9 @@ _CLIENT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/classroom.guardianlinks.students'},
{'name': 'Classroom API - Profile Emails',
'api': CLASSROOM,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/classroom.profile.emails'},
{'name': 'Classroom API - Profile Photos',
'api': CLASSROOM,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/classroom.profile.photos'},
{'name': 'Classroom API - Rosters',
'api': CLASSROOM,
@@ -404,7 +412,6 @@ _CLIENT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/cloud-identity.policies'},
{'name': 'Cloud Identity API - Policy Beta',
'api': CLOUDIDENTITY_POLICY_BETA,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/cloud-identity.policies'},
{'name': 'Cloud Identity API - User Invitations',
@@ -413,17 +420,14 @@ _CLIENT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/cloud-identity.userinvitations'},
{'name': 'Cloud Storage API (Read Only, Vault/Takeout Download, Cloud Storage)',
'api': STORAGEREAD,
'subscopes': [],
'offByDefault': True,
'scope': STORAGE_READONLY_SCOPE},
{'name': 'Cloud Storage API (Read/Write, Vault/Takeout Copy/Download, Cloud Storage)',
'api': STORAGEWRITE,
'subscopes': [],
'offByDefault': True,
'scope': STORAGE_READWRITE_SCOPE},
{'name': 'Contacts API - Domain Shared Contacts',
'api': CONTACTS,
'subscopes': [],
'scope': 'https://www.google.com/m8/feeds'},
{'name': 'Contact Delegation API',
'api': CONTACTDELEGATION,
@@ -451,7 +455,7 @@ _CLIENT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/admin.directory.group'},
{'name': 'Directory API - Mobile Devices Directory',
'api': DIRECTORY,
'subscopes': ['readonly', 'action'],
'subscopes': ['readonly', 'actiononly'],
'scope': 'https://www.googleapis.com/auth/admin.directory.device.mobile'},
{'name': 'Directory API - Organizational Units',
'api': DIRECTORY,
@@ -471,7 +475,6 @@ _CLIENT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/admin.directory.userschema'},
{'name': 'Directory API - User Security',
'api': DIRECTORY,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/admin.directory.user.security'},
{'name': 'Directory API - Users',
'api': DIRECTORY,
@@ -479,24 +482,19 @@ _CLIENT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/admin.directory.user'},
{'name': 'Email Audit API',
'api': EMAIL_AUDIT,
'subscopes': [],
'offByDefault': True,
'scope': 'https://apps-apis.google.com/a/feeds/compliance/audit/'},
{'name': 'Groups Migration API',
'api': GROUPSMIGRATION,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/apps.groups.migration'},
{'name': 'Groups Settings API',
'api': GROUPSSETTINGS,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/apps.groups.settings'},
{'name': 'License Manager API',
'api': LICENSING,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/apps.licensing'},
{'name': 'People Directory API - read only',
{'name': 'People Directory API - readonly',
'api': PEOPLE_DIRECTORY,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/directory.readonly'},
{'name': 'People API',
'api': PEOPLE,
@@ -504,29 +502,31 @@ _CLIENT_SCOPES = [
'scope': PEOPLE_SCOPE},
{'name': 'Pub / Sub API',
'api': PUBSUB,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/pubsub'},
{'name': 'Reports API - Audit Reports',
{'name': 'Reports API - Audit Reports readonly',
'api': REPORTS,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/admin.reports.audit.readonly'},
{'name': 'Reports API - Usage Reports',
{'name': 'Reports API - Usage Reports readonly',
'api': REPORTS,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/admin.reports.usage.readonly'},
{'name': 'Reseller API',
'api': RESELLER,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/apps.order'},
{'name': 'Resource Manager API - Organizations readonly',
'api': CLOUDRESOURCEMANAGER,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/cloudplatformorganizations.readonly'},
{'name': 'Resource Manager API - Projects readonly',
'api': CLOUDRESOURCEMANAGER,
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/cloudplatformprojects.readonly'},
{'name': 'Service Account Lookup pseudo-API',
'api': SERVICEACCOUNTLOOKUP,
'subscopes': [],
'scope': ''},
{'name': 'Site Verification API',
'api': SITEVERIFICATION,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/siteverification'},
{'name': 'Vault API',
@@ -538,30 +538,24 @@ _CLIENT_SCOPES = [
_COMMANDDATA_CLIENT_SCOPES = [
{'name': 'Drive API - commanddata_clientaccess',
'api': DRIVE3,
'subscopes': [],
'scope': DRIVE_READONLY_SCOPE},
{'name': 'Sheets API - commanddata_clientaccess',
{'name': 'Sheets API - commanddata_clientaccess readonly',
'api': SHEETS,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/spreadsheets.readonly'},
]
_TODRIVE_CLIENT_SCOPES = [
{'name': 'Drive API - todrive_clientaccess',
'api': DRIVE3,
'subscopes': [],
'scope': DRIVE_SCOPE},
{'name': 'Drive File API - todrive_clientaccess',
'api': DRIVE3,
'subscopes': [],
'scope': DRIVE_FILE_SCOPE},
{'name': 'Gmail API - todrive_clientaccess',
'api': GMAIL,
'subscopes': [],
'scope': GMAIL_SEND_SCOPE},
{'name': 'Sheets API - todrive_clientaccess',
'api': SHEETS,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/spreadsheets'},
]
@@ -570,11 +564,9 @@ OAUTH2SA_SCOPES = 'us_scopes'
_SVCACCT_SCOPES = [
{'name': 'AlertCenter API',
'api': ALERTCENTER,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/apps.alerts'},
{'name': 'Analytics Admin API - read only',
{'name': 'Analytics Admin API - readonly',
'api': ANALYTICS_ADMIN,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/analytics.readonly'},
{'name': 'Calendar API',
'api': CALENDAR,
@@ -611,11 +603,9 @@ _SVCACCT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/chat.admin.spaces'},
{'name': 'Chat API - Spaces Delete',
'api': CHAT_SPACES_DELETE,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/chat.delete'},
{'name': 'Chat API - Spaces Delete Admin',
'api': CHAT_SPACES_DELETE_ADMIN,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/chat.admin.delete'},
{'name': 'Classroom API - Course Announcements',
'api': CLASSROOM,
@@ -635,11 +625,9 @@ _SVCACCT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/classroom.coursework.students'},
{'name': 'Classroom API - Profile Emails',
'api': CLASSROOM,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/classroom.profile.emails'},
{'name': 'Classroom API - Profile Photos',
'api': CLASSROOM,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/classroom.profile.photos'},
{'name': 'Classroom API - Rosters',
'api': CLASSROOM,
@@ -656,7 +644,6 @@ _SVCACCT_SCOPES = [
# 'scope': 'https://www.googleapis.com/auth/cloud-identity.policies'},
# {'name': 'Cloud Identity API - Policy Beta',
# 'api': CLOUDIDENTITY_POLICY_BETA,
# 'subscopes': [],
# 'offByDefault': True,
# 'scope': 'https://www.googleapis.com/auth/cloud-identity.policies'},
# {'name': 'Cloud Identity User Invitations API',
@@ -665,7 +652,6 @@ _SVCACCT_SCOPES = [
# 'scope': 'https://www.googleapis.com/auth/cloud-identity'},
# {'name': 'Contacts API - Users',
# 'api': CONTACTS,
# 'subscopes': [],
# 'scope': 'https://www.google.com/m8/feeds'},
{'name': 'Drive API',
'api': DRIVE3,
@@ -673,7 +659,6 @@ _SVCACCT_SCOPES = [
'scope': DRIVE_SCOPE},
{'name': 'Drive Activity API v2 - must pair with Drive API',
'api': DRIVEACTIVITY,
'subscopes': [],
'scope': [DRIVE_READONLY_SCOPE,
'https://www.googleapis.com/auth/drive.activity']},
{'name': 'Drive Labels API - Admin',
@@ -690,30 +675,24 @@ _SVCACCT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/documents'},
{'name': 'Forms API - must pair with Drive API',
'api': FORMS,
'subscopes': [],
'scope': [DRIVE_READONLY_SCOPE,
'https://www.googleapis.com/auth/forms.body',
'https://www.googleapis.com/auth/forms.responses.readonly']},
{'name': 'Gmail API - Full Access (Labels, Messages)',
'api': GMAIL,
'subscopes': [],
'scope': 'https://mail.google.com/'},
{'name': 'Gmail API - Full Access (Labels, Messages) except delete message',
'api': GMAIL,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/gmail.modify'},
{'name': 'Gmail API - Basic Settings (Filters, IMAP, Language, POP, Vacation) - read/write, Sharing Settings (Delegates, Forwarding, SendAs) - read',
'api': GMAIL,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/gmail.settings.basic'},
{'name': 'Gmail API - Sharing Settings (Delegates, Forwarding, SendAs) - write',
'api': GMAIL,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/gmail.settings.sharing'},
# {'name': 'Identity and Access Management API',
# 'api': IAM,
# 'offByDefault': True,
# 'subscopes': [],
# 'scope': CLOUD_PLATFORM_SCOPE},
{'name': 'Keep API',
'api': KEEP,
@@ -725,32 +704,26 @@ _SVCACCT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/datastudio'},
{'name': 'Meet API - Manage/Display Meeting Spaces',
'api': MEET_SPACES,
'subscopes': [],
'scope': ['https://www.googleapis.com/auth/meetings.space.created',
'https://www.googleapis.com/auth/meetings.space.settings']},
{'name': 'Meet API - Read Meeting Spaces metadata',
{'name': 'Meet API - Read Meeting Spaces metadata readonly',
'api': MEET_READONLY,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/meetings.space.readonly'},
{'name': 'OAuth2 API',
'api': OAUTH2,
'subscopes': [],
'scope': USERINFO_PROFILE_SCOPE},
{'name': 'People API',
'api': PEOPLE,
'subscopes': READONLY,
'scope': PEOPLE_SCOPE},
{'name': 'People Directory API - read only',
{'name': 'People Directory API - readonly',
'api': PEOPLE_DIRECTORY,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/directory.readonly'},
{'name': 'People API - Other Contacts - read only',
{'name': 'People API - Other Contacts - readonly',
'api': PEOPLE_OTHERCONTACTS,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/contacts.other.readonly'},
{'name': 'Search Console API - read only',
{'name': 'Search Console API - readonly',
'api': SEARCHCONSOLE,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/webmasters.readonly'},
{'name': 'Sheets API',
@@ -759,26 +732,22 @@ _SVCACCT_SCOPES = [
'scope': 'https://www.googleapis.com/auth/spreadsheets'},
{'name': 'Site Verification API',
'api': SITEVERIFICATION,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/siteverification'},
{'name': 'Tag Manager API - Accounts, Containers, Workspaces, Tags - read only',
{'name': 'Tag Manager API - Accounts, Containers, Workspaces, Tags - readonly',
'api': TAGMANAGER,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/tagmanager.readonly'},
{'name': 'Tag Manager API - Users',
'api': TAGMANAGER_USERS,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/tagmanager.manage.users'},
{'name': 'Tasks API',
'api': TASKS,
'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/tasks'},
{'name': 'Youtube API - read only',
{'name': 'Youtube API - readonly',
'api': YOUTUBE,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/youtube.readonly'},
]
@@ -786,30 +755,25 @@ _SVCACCT_SCOPES = [
_SVCACCT_SPECIAL_SCOPES = [
{'name': 'Drive API - write todrive data - has access to all Drive',
'api': DRIVETD,
'subscopes': [],
'offByDefault': True,
'scope': DRIVE_SCOPE},
{'name': 'Gmail API - Full Access - read only',
{'name': 'Gmail API - Full Access - readonly',
'api': GMAIL,
'subscopes': [],
'offByDefault': True,
'scope': 'https://www.googleapis.com/auth/gmail.readonly'},
{'name': 'Gmail API - Send Messages - including todrive',
'api': GMAIL,
'subscopes': [],
'offByDefault': True,
'scope': GMAIL_SEND_SCOPE},
{'name': 'Sheets API - write todrive data - has access to all Sheets',
'api': SHEETSTD,
'offByDefault': True,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/spreadsheets'},
]
_USER_SVCACCT_ONLY_SCOPES = [
{'name': 'Groups Migration API',
'api': GROUPSMIGRATION,
'subscopes': [],
'scope': 'https://www.googleapis.com/auth/apps.groups.migration'},
]
@@ -849,7 +813,7 @@ def getClientScopesURLs(commanddataClientAccess, todriveClientAccess):
def getSvcAcctScopeAPI(uscope):
for scope in _SVCACCT_SCOPES:
if uscope == scope['scope'] or (uscope.endswith('.readonly') and 'readonly' in scope['subscopes']):
if uscope == scope['scope'] or (uscope.endswith('.readonly') and 'readonly' in scope.get('subscopes', [])):
return scope['api']
return None
@@ -877,11 +841,11 @@ def findAPIforScope(scopesList):
if cscope['scope'] == scope:
requiredAPIs.append(cscope['name'])
return True
if cscope['subscopes'] == READONLY and cscope['scope']+'.readonly' == scope:
if 'readonly' in cscope.get('subscopes', []) and cscope['scope']+'.readonly' == scope:
requiredAPIs.append(cscope['name']+' (supports readonly)')
return True
return False
requiredAPIs = []
for scope in scopesList:
for cscope in _CLIENT_SCOPES:

View File

@@ -10,6 +10,26 @@ Add the `-s` option to the end of the above commands to suppress creating the `g
See [Downloads-Installs-GAM7](https://github.com/GAM-team/GAM/wiki/Downloads-Installs) for Windows or other options, including manual installation
### 7.36.03
Added command to send email replies that causes Gmail to recognize the message
in conversation mode for the user sending the reply and the user receiving the reply;
GAM supplies the necessary headers and options.
```
gam <UserTypeEntity> sendreply
(((query <QueryGmail> [querytime<String> <Date>]*) [or|and])+) | (ids <MessageIDEntity>)
[replyto <EmailAddress>]
[subject <String>] [<MessageContent>] [html [<Boolean>]]
(attach <FileName> [charset <CharSet>])*
(embedimage <FileName> <String>)*
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
gam user user@domain.com sendreply query "rfc822MsgId:<CAAMmEdqj43...1OsQ@mail.gmail.com>" textmessage "Thanks for the information"
gam user user@domain.com sendreply ids 19cfc3506c02c22b textmessage "Thanks for the information"
```
* See: https://github.com/GAM-team/GAM/wiki/Send-Email#conversation-mode
### 7.36.02
Added option `threadid <String>` to `gam [<UserTypeEntity>] sendemail` that causes Gmail to recognize the message

View File

@@ -251,7 +251,7 @@ writes the credentials into the file oauth2.txt.
```
gamteam@server:/Users/gamteam$ rm -f /Users/gamteam/GAMConfig/oauth2.txt
gamteam@server:/Users/gamteam$ gam version
GAM 7.36.02 - https://github.com/GAM-team/GAM - pyinstaller
GAM 7.36.03 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.3 64-bit final
macOS Tahoe 26.3.1 arm64
@@ -1034,7 +1034,7 @@ writes the credentials into the file oauth2.txt.
```
C:\>del C:\GAMConfig\oauth2.txt
C:\>gam version
GAM 7.36.02 - https://github.com/GAM-team/GAM - pythonsource
GAM 7.36.03 - https://github.com/GAM-team/GAM - pythonsource
GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.3 64-bit final
Windows 11 10.0.26200 AMD64

View File

@@ -465,15 +465,28 @@ gam user recipient@domain.com sendemail to sender@domain.com references "<CAAMab
If you want to have Gmail recognize the reply in conversation mode in the Sent folder of the original recipient,
you must include `threadid <String>`; you can get the 'threadId` with:
```
gam user recipient@domain.com show messages query "rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>"
gam user recipient@domain.com show threads query "rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>"
Getting all Messages that match query ((rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>)) for recipient@domain.com
Got 1 Message that matched query ((rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>)) for recipient@domain.com...
User: recipient@domain.com, Show 1 Message
Message: 19cfd414fe48430d
User: recipient@domain.com, Show 1 Thread
Thread: 19cfd414fe48430d
Message: 19cfd414fe48430d
...
gam user recipient@domain.com sendemail to sender@domain.com references "<CAAMabc...XYZQ@mail.gmail.com>" in-reply-to "<CAAMabc...XYZQ@mail.gmail.com>" subject "Re: Original subject" textmessage "Reply text" threadid 19cfd414fe48430d
```
As of version 7.36.03, GAM has a command to simplify this process.
```
gam <UserTypeEntity> sendreply
(((query <QueryGmail> [querytime<String> <Date>]*) [or|and])+) | (ids <MessageIDEntity>)
[replyto <EmailAddress>]
[subject <String>] [<MessageContent>] [html [<Boolean>]]
(attach <FileName> [charset <CharSet>])*
(embedimage <FileName> <String>)*
(<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
gam user recipient@domain.com sendreply query "rfc822MsgId:<CAAMabc...XYZQ@mail.gmail.com>" textmessage "Reply text"
```

View File

@@ -3,7 +3,7 @@
Print the current version of Gam with details
```
gam version
GAM 7.36.02 - https://github.com/GAM-team/GAM - pyinstaller
GAM 7.36.03 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.3 64-bit final
macOS Tahoe 26.3.1 arm64
@@ -15,7 +15,7 @@ Time: 2026-02-15T07:51:00-08:00
Print the current version of Gam with details and time offset information
```
gam version timeoffset
GAM 7.36.02 - https://github.com/GAM-team/GAM - pyinstaller
GAM 7.36.03 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.3 64-bit final
macOS Tahoe 26.3.1 arm64
@@ -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
GAM 7.36.02 - https://github.com/GAM-team/GAM - pyinstaller
GAM 7.36.03 - https://github.com/GAM-team/GAM - pyinstaller
GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.3 64-bit final
macOS Tahoe 26.3.1 arm64
@@ -68,7 +68,7 @@ MacOS High Sierra 10.13.6 x86_64
Path: /Users/gamteam/bin/gam7
Version Check:
Current: 5.35.08
Latest: 7.36.02
Latest: 7.36.03
echo $?
1
```
@@ -76,7 +76,7 @@ echo $?
Print the current version number without details
```
gam version simple
7.36.02
7.36.03
```
In Linux/MacOS you can do:
```
@@ -86,7 +86,7 @@ echo $VER
Print the current version of Gam and address of this Wiki
```
gam help
GAM 7.36.02 - https://github.com/GAM-team/GAM
GAM 7.36.03 - https://github.com/GAM-team/GAM
GAM Team <google-apps-manager@googlegroups.com>
Python 3.14.3 64-bit final
macOS Tahoe 26.3.1 arm64