Improve conversation mode in gam sendmail
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

This commit is contained in:
Ross Scroggs
2026-03-17 14:26:07 -07:00
parent 8e71e18aaa
commit ce73c62e81
3 changed files with 65 additions and 41 deletions

View File

@@ -4926,36 +4926,36 @@ 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>]
(replace <Tag> <String>)*
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
[html [<Boolean>]] (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>]
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
[subject <String>]
[<MessageContent>]
[subject <String>] [<MessageContent>]
(replace <Tag> <String>)*
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
[html [<Boolean>]] (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>]
[cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
[subject <String>]
[<MessageContent>]
[subject <String>] [<MessageContent>]
(replace <Tag> <String>)*
(replaceregex <RESearchPattern> <RESubstitution> <Tag> <String>)*
[html [<Boolean>]] (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>]
# Shared Drives - Administrator

View File

@@ -1,3 +1,10 @@
7.36.02
Added option `threadid <String>` to `gam [<UserTypeEntity>] sendemail` that causes Gmail to recognize the message
in conversation mode in for the user sending the message.
* See: https://github.com/GAM-team/GAM/wiki/Send-Email#conversation-mode
7.36.01
Fixed bug in `gam info|print|show policies` where the `policyQuery/query` field was not displayed.

View File

@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
"""
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
__version__ = '7.36.01'
__version__ = '7.36.02'
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
# pylint: disable=wrong-import-position
@@ -7347,7 +7347,7 @@ 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):
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None, threadId=None):
def checkResult(entityType, recipients):
if not recipients:
return
@@ -7414,11 +7414,14 @@ def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msg
userId = mailBoxAddr
gmail = buildGAPIObject(API.GMAIL)
message['To'] = msgTo if msgTo else userId
body = {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}
if threadId is not None:
body['threadId'] = threadId
try:
result = callGAPI(gmail.users().messages(), 'send',
throwReasons=[GAPI.SERVICE_NOT_AVAILABLE, GAPI.AUTH_ERROR, GAPI.DOMAIN_POLICY,
GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
userId=userId, body={'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}, fields='id')
userId=userId, body=body, fields='id')
entityActionPerformedMessage([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], f"{result['id']}", i, count)
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
GAPI.invalid, GAPI.invalidArgument, GAPI.forbidden, GAPI.permissionDenied) as e:
@@ -15390,34 +15393,34 @@ def getRecipients():
# gam sendemail [recipient|to] <RecipientEntity> [from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
# [subject <String>]
# [<MessageContent>]
# [subject <String>] [<MessageContent>]
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] (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>]
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
# [subject <String>]
# [<MessageContent>]
# [subject <String>] [<MessageContent>]
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] (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>]
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
# [subject <String>]
# [<MessageContent>]
# [subject <String>] [<MessageContent>]
# (replace <Tag> <String>)*
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
# [html [<Boolean>]] (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>]
def doSendEmail(users=None):
body = {}
notify = {'subject': '', 'message': '', 'html': False, 'charset': UTF8, 'password': ''}
@@ -15442,6 +15445,7 @@ def doSendEmail(users=None):
bccRecipients = []
mailBox = None
msgReplyTo = None
threadId = None
singleMessage = False
tagReplacements = _initTagReplacements()
attachments = []
@@ -15492,6 +15496,8 @@ def doSendEmail(users=None):
elif myarg == 'header':
header = getString(Cmd.OB_STRING, minLen=1)
msgHeaders[SMTP_HEADERS_MAP.get(header.lower(), header)] = getString(Cmd.OB_STRING)
elif myarg == 'threadid':
threadId = getString(Cmd.OB_STRING)
else:
unknownArgumentExit()
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '\n')
@@ -15519,7 +15525,7 @@ def doSendEmail(users=None):
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders,
ccRecipients=','.join(ccRecipients), bccRecipients=','.join(bccRecipients),
mailBox=mailBox)
mailBox=mailBox, threadId=threadId)
else:
entityPerformActionModifierNumItems([Ent.USER, msgFrom], Act.MODIFIER_TO, jcount, Ent.RECIPIENT, i, count)
Ind.Increment()
@@ -15528,7 +15534,7 @@ def doSendEmail(users=None):
j += 1
send_email(notify['subject'], notify['message'], recipient, j, jcount,
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, mailBox=mailBox)
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, mailBox=mailBox, threadId=threadId)
Ind.Decrement()
ADDRESS_FIELDS_PRINT_ORDER = ['contactName', 'organizationName', 'addressLine1', 'addressLine2', 'addressLine3', 'locality', 'region', 'postalCode', 'countryCode']
@@ -37780,7 +37786,14 @@ def _cleanPolicy(policy, add_warnings, no_appnames, no_idmapping,
cd, groups_ci):
# convert any wordlists into spaced strings to reduce output complexity
if policy['setting']['type'] == 'settings/detector.word_list':
policy['setting']['value']['wordList'] = ' '.join(policy['setting']['value']['wordList']['words'])
wordList = ''
for word in policy['setting']['value']['wordList']['words']:
wordList += "'"
wordList += word.replace("'", r"\'")
wordList += "',"
if wordList:
wordList = wordList[:-1]
policy['setting']['value']['wordList'] = wordList
# get application name for application id
if policy['setting']['type'] == 'settings/workspace_marketplace.apps_allowlist' and not no_appnames:
httpObj = getHttpObj(timeout=10)
@@ -37867,51 +37880,55 @@ def doCreateUpdateCIPolicy():
updateCmd = Act.Get() == Act.UPDATE
groupEmail = orgUnit = None
checkArgumentPresent('json', True)
jsonData = getJSON(['customer', 'type'])
policy = getJSON(['customer', 'type'])
if updateCmd:
pname = jsonData.pop('name', None)
pname = policy.pop('name', None)
else:
jsonData.pop('name', None)
policy.pop('name', None)
pname = 'New Policy'
if 'policyQuery' in jsonData:
jsonData['policyQuery'].pop('orgUnitPath', None)
jsonData['policyQuery'].pop('groupEmail', None)
jsonData['policyQuery'].pop('sortOrder', None)
if 'setting' in jsonData:
if 'value' in jsonData['setting']:
jsonData['setting']['value'].pop('createTime', None)
jsonData['setting']['value'].pop('deleteTime', None)
jsonData['setting']['value'].pop('updateTime', None)
if 'policyQuery' in policy:
policy['policyQuery'].pop('orgUnitPath', None)
policy['policyQuery'].pop('groupEmail', None)
policy['policyQuery'].pop('sortOrder', None)
if 'setting' in policy:
if 'value' in policy['setting']:
policy['setting']['value'].pop('createTime', None)
policy['setting']['value'].pop('deleteTime', None)
policy['setting']['value'].pop('updateTime', None)
if policy['setting']['type'] == 'settings/detector.word_list':
if isinstance(policy['setting']['value']['wordList'], str):
wordList = policy['setting']['value'].pop('wordList')
policy['setting']['value']['wordList']['words'] = shlexSplitList(wordList, dataDelimiter=',')
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'ou', 'org', 'orgunit'}:
orgUnit, targetResource = _getCIPolicyOrgUnitTarget(cd, myarg, groupEmail)
jsonData.setdefault('policyQuery', {})
jsonData['policyQuery'].pop('group', None)
jsonData['policyQuery']['orgUnit'] = f"orgUnits/{targetResource}"
jsonData['policyQuery']['query'] = f"entity.org_units.exists(org_unit, org_unit.org_unit_id == orgUnitId('{targetResource}'))"
policy.setdefault('policyQuery', {})
policy['policyQuery'].pop('group', None)
policy['policyQuery']['orgUnit'] = f"orgUnits/{targetResource}"
policy['policyQuery']['query'] = f"entity.org_units.exists(org_unit, org_unit.org_unit_id == orgUnitId('{targetResource}'))"
elif myarg == 'group':
groupEmail, targetResource = _getCIPolicyGroupTarget(cd, myarg, orgUnit)
jsonData.setdefault('policyQuery', {})
jsonData['policyQuery'].pop('orgUnit', None)
jsonData['policyQuery']['group'] = f"groups/{targetResource}"
jsonData['policyQuery']['query'] = f"entity.groups.exists(group, group.group_id == groupId('{targetResource}'))"
policy.setdefault('policyQuery', {})
policy['policyQuery'].pop('orgUnit', None)
policy['policyQuery']['group'] = f"groups/{targetResource}"
policy['policyQuery']['query'] = f"entity.groups.exists(group, group.group_id == groupId('{targetResource}'))"
else:
unknownArgumentExit()
jsonData['customer'] = _getCustomersCustomerIdWithC()
policy['customer'] = _getCustomersCustomerIdWithC()
try:
if updateCmd:
result = callGAPI(ci.policies(), 'patch',
bailOnInternalError=True,
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.UNIMPLEMENTED_ERROR,
GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=pname, body=jsonData)
name=pname, body=policy)
else:
result = callGAPI(ci.policies(), 'create',
bailOnInternalError=True,
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.UNIMPLEMENTED_ERROR,
GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
body=jsonData)
body=policy)
if result['done']:
if 'error' not in result:
if not updateCmd: